blob: a1874b84dc78cc049003b41c8102f39d844dfed8 [file] [log] [blame]
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05301from collections import defaultdict
2from typing import List
Rohit Waghchauref1b59662023-03-06 12:08:28 +05303
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05304import frappe
5from frappe import _, bold
6from frappe.model.naming import make_autoname
Rohit Waghchaure86da3062023-03-20 14:15:34 +05307from frappe.query_builder.functions import CombineDatetime, Sum
Rohit Waghchauref968f0f2023-06-14 23:22:22 +05308from frappe.utils import cint, flt, get_link_to_form, now, nowtime, today
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05309
10from erpnext.stock.deprecated_serial_batch import (
11 DeprecatedBatchNoValuation,
12 DeprecatedSerialNoValuation,
13)
Rohit Waghchauref1b59662023-03-06 12:08:28 +053014from erpnext.stock.valuation import round_off_if_near_zero
15
16
17class SerialBatchBundle:
18 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053019 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +053020 setattr(self, key, value)
21
22 self.set_item_details()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053023 self.process_serial_and_batch_bundle()
24 if self.sle.is_cancelled:
25 self.delink_serial_and_batch_bundle()
26
27 self.post_process()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053028
29 def process_serial_and_batch_bundle(self):
30 if self.item_details.has_serial_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053031 self.process_serial_no()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053032 elif self.item_details.has_batch_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053033 self.process_batch_no()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053034
35 def set_item_details(self):
36 fields = [
37 "has_batch_no",
38 "has_serial_no",
39 "item_name",
40 "item_group",
41 "serial_no_series",
42 "create_new_batch",
43 "batch_number_series",
44 ]
45
46 self.item_details = frappe.get_cached_value("Item", self.sle.item_code, fields, as_dict=1)
47
48 def process_serial_no(self):
49 if (
50 not self.sle.is_cancelled
51 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +053052 and self.item_details.has_serial_no == 1
Rohit Waghchauref1b59662023-03-06 12:08:28 +053053 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053054 self.make_serial_batch_no_bundle()
55 elif not self.sle.is_cancelled:
56 self.validate_item_and_warehouse()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053057
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053058 def make_serial_batch_no_bundle(self):
Rohit Waghchaure648efca2023-03-28 12:16:27 +053059 self.validate_item()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053060
Rohit Waghchaure648efca2023-03-28 12:16:27 +053061 sn_doc = SerialBatchCreation(
62 {
63 "item_code": self.item_code,
64 "warehouse": self.warehouse,
65 "posting_date": self.sle.posting_date,
66 "posting_time": self.sle.posting_time,
67 "voucher_type": self.sle.voucher_type,
68 "voucher_no": self.sle.voucher_no,
69 "voucher_detail_no": self.sle.voucher_detail_no,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +053070 "qty": self.sle.actual_qty,
Rohit Waghchaure648efca2023-03-28 12:16:27 +053071 "avg_rate": self.sle.incoming_rate,
72 "total_amount": flt(self.sle.actual_qty) * flt(self.sle.incoming_rate),
73 "type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward",
74 "company": self.company,
75 "is_rejected": self.is_rejected_entry(),
76 }
77 ).make_serial_and_batch_bundle()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053078
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053079 self.set_serial_and_batch_bundle(sn_doc)
Rohit Waghchauref1b59662023-03-06 12:08:28 +053080
Rohit Waghchaure42b22942023-05-27 19:18:03 +053081 def validate_actual_qty(self, sn_doc):
Rohit Waghchauref968f0f2023-06-14 23:22:22 +053082 link = get_link_to_form("Serial and Batch Bundle", sn_doc.name)
83
84 condition = {
85 "Inward": self.sle.actual_qty > 0,
86 "Outward": self.sle.actual_qty < 0,
87 }.get(sn_doc.type_of_transaction)
88
89 if not condition:
90 correct_type = "Inward"
91 if sn_doc.type_of_transaction == "Inward":
92 correct_type = "Outward"
93
94 msg = f"The type of transaction of Serial and Batch Bundle {link} is {bold(sn_doc.type_of_transaction)} but as per the Actual Qty {self.sle.actual_qty} for the item {bold(self.sle.item_code)} in the {self.sle.voucher_type} {self.sle.voucher_no} the type of transaction should be {bold(correct_type)}"
95 frappe.throw(_(msg), title=_("Incorrect Type of Transaction"))
96
Rohit Waghchaure42b22942023-05-27 19:18:03 +053097 precision = sn_doc.precision("total_qty")
98 if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision):
Rohit Waghchauref968f0f2023-06-14 23:22:22 +053099 msg = f"Total qty {flt(sn_doc.total_qty, precision)} of Serial and Batch Bundle {link} is not equal to Actual Qty {flt(self.sle.actual_qty, precision)} in the {self.sle.voucher_type} {self.sle.voucher_no}"
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530100 frappe.throw(_(msg))
101
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530102 def validate_item(self):
103 msg = ""
104 if self.sle.actual_qty > 0:
105 if not self.item_details.has_batch_no and not self.item_details.has_serial_no:
106 msg = f"Item {self.item_code} is not a batch or serial no item"
107
108 if self.item_details.has_serial_no and not self.item_details.serial_no_series:
109 msg += f". If you want auto pick serial bundle, then kindly set Serial No Series in Item {self.item_code}"
110
111 if (
112 self.item_details.has_batch_no
113 and not self.item_details.batch_number_series
114 and not frappe.db.get_single_value("Stock Settings", "naming_series_prefix")
115 ):
116 msg += f". If you want auto pick batch bundle, then kindly set Batch Number Series in Item {self.item_code}"
117
118 elif self.sle.actual_qty < 0:
119 if not frappe.db.get_single_value(
120 "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
121 ):
122 msg += ". If you want auto pick serial/batch bundle, then kindly enable 'Auto Create Serial and Batch Bundle' in Stock Settings."
123
124 if msg:
125 error_msg = (
126 f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}."
127 + msg
128 )
129 frappe.throw(_(error_msg))
130
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530131 def set_serial_and_batch_bundle(self, sn_doc):
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530132 self.sle.db_set(
133 {"serial_and_batch_bundle": sn_doc.name, "auto_created_serial_and_batch_bundle": 1}
134 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530135
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530136 if sn_doc.is_rejected:
137 frappe.db.set_value(
138 self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name
139 )
140 else:
141 frappe.db.set_value(
142 self.child_doctype, self.sle.voucher_detail_no, "serial_and_batch_bundle", sn_doc.name
143 )
144
145 @property
146 def child_doctype(self):
147 child_doctype = self.sle.voucher_type + " Item"
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530148
149 if (
150 self.sle.voucher_type == "Subcontracting Receipt" and self.sle.dependant_sle_voucher_detail_no
151 ):
152 child_doctype = "Subcontracting Receipt Supplied Item"
153
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530154 if self.sle.voucher_type == "Stock Entry":
155 child_doctype = "Stock Entry Detail"
156
Rohit Waghchaure26b39ac2023-04-06 01:36:18 +0530157 if self.sle.voucher_type == "Asset Capitalization":
158 child_doctype = "Asset Capitalization Stock Item"
159
160 if self.sle.voucher_type == "Asset Repair":
161 child_doctype = "Asset Repair Consumed Item"
162
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530163 return child_doctype
164
165 def is_rejected_entry(self):
166 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
167
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530168 def process_batch_no(self):
169 if (
170 not self.sle.is_cancelled
171 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530172 and self.item_details.has_batch_no == 1
173 and self.item_details.create_new_batch
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530174 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530175 self.make_serial_batch_no_bundle()
176 elif not self.sle.is_cancelled:
177 self.validate_item_and_warehouse()
178
179 def validate_item_and_warehouse(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530180 if self.sle.serial_and_batch_bundle and not frappe.db.exists(
181 "Serial and Batch Bundle",
182 {
183 "name": self.sle.serial_and_batch_bundle,
184 "item_code": self.item_code,
185 "warehouse": self.warehouse,
186 "voucher_no": self.sle.voucher_no,
187 },
188 ):
189 msg = f"""
190 The Serial and Batch Bundle
191 {bold(self.sle.serial_and_batch_bundle)}
192 does not belong to Item {bold(self.item_code)}
193 or Warehouse {bold(self.warehouse)}
194 or {self.sle.voucher_type} no {bold(self.sle.voucher_no)}
195 """
196
197 frappe.throw(_(msg))
198
199 def delink_serial_and_batch_bundle(self):
200 update_values = {
201 "serial_and_batch_bundle": "",
202 }
203
204 if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse):
205 update_values["rejected_serial_and_batch_bundle"] = ""
206
207 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values)
208
209 frappe.db.set_value(
210 "Serial and Batch Bundle",
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530211 {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530212 {"is_cancelled": 1, "voucher_no": ""},
213 )
214
Rohit Waghchauref79f2a32023-04-04 11:50:38 +0530215 if self.sle.serial_and_batch_bundle:
216 frappe.get_cached_doc(
217 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle
218 ).validate_serial_and_batch_inventory()
219
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530220 def post_process(self):
rohitwaghchaure07432892023-12-17 12:42:07 +0530221 if not self.sle.serial_and_batch_bundle and not self.sle.serial_no and not self.sle.batch_no:
Rohit Waghchaure674bd3e2023-03-17 16:42:59 +0530222 return
223
rohitwaghchaure07432892023-12-17 12:42:07 +0530224 if self.sle.serial_and_batch_bundle:
225 docstatus = frappe.get_cached_value(
226 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
227 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530228
rohitwaghchaure07432892023-12-17 12:42:07 +0530229 if docstatus != 1:
230 self.submit_serial_and_batch_bundle()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530231
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530232 if self.item_details.has_serial_no == 1:
233 self.set_warehouse_and_status_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530234
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530235 if (
236 self.sle.actual_qty > 0
237 and self.item_details.has_serial_no == 1
238 and self.item_details.has_batch_no == 1
239 ):
240 self.set_batch_no_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530241
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530242 if self.item_details.has_batch_no == 1:
243 self.update_batch_qty()
244
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530245 def submit_serial_and_batch_bundle(self):
246 doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530247 self.validate_actual_qty(doc)
248
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530249 doc.flags.ignore_voucher_validation = True
250 doc.submit()
251
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530252 def set_warehouse_and_status_in_serial_nos(self):
rohitwaghchaure07432892023-12-17 12:42:07 +0530253 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
254
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530255 serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
rohitwaghchaure07432892023-12-17 12:42:07 +0530256 if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
257 serial_nos = get_parsed_serial_nos(self.sle.serial_no)
258
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530259 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
260
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530261 if not serial_nos:
262 return
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530263
rohitwaghchaure592fc812023-11-28 18:28:48 +0530264 status = "Inactive"
265 if self.sle.actual_qty < 0:
266 status = "Delivered"
267
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530268 sn_table = frappe.qb.DocType("Serial No")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530269 (
270 frappe.qb.update(sn_table)
271 .set(sn_table.warehouse, warehouse)
rohitwaghchaure07432892023-12-17 12:42:07 +0530272 .set(
273 sn_table.status,
274 "Active"
275 if warehouse
276 else status
277 if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
278 else "Inactive",
279 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530280 .where(sn_table.name.isin(serial_nos))
281 ).run()
282
283 def set_batch_no_in_serial_nos(self):
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530284 entries = frappe.get_all(
285 "Serial and Batch Entry",
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530286 fields=["serial_no", "batch_no"],
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530287 filters={"parent": self.sle.serial_and_batch_bundle},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530288 )
289
290 batch_serial_nos = {}
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530291 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530292 batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no)
293
294 for batch_no, serial_nos in batch_serial_nos.items():
295 sn_table = frappe.qb.DocType("Serial No")
296 (
297 frappe.qb.update(sn_table)
298 .set(sn_table.batch_no, batch_no)
299 .where(sn_table.name.isin(serial_nos))
300 ).run()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530301
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530302 def update_batch_qty(self):
303 from erpnext.stock.doctype.batch.batch import get_available_batches
304
305 batches = get_batch_nos(self.sle.serial_and_batch_bundle)
rohitwaghchaure07432892023-12-17 12:42:07 +0530306 if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
307 batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530308
309 batches_qty = get_available_batches(
310 frappe._dict(
311 {"item_code": self.item_code, "warehouse": self.warehouse, "batch_no": list(batches.keys())}
312 )
313 )
314
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530315 for batch_no in batches:
316 frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0))
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530317
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530318
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530319def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530320 if not serial_and_batch_bundle:
321 return []
322
323 filters = {"parent": serial_and_batch_bundle, "serial_no": ("is", "set")}
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530324 if isinstance(serial_and_batch_bundle, list):
325 filters = {"parent": ("in", serial_and_batch_bundle)}
326
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530327 if serial_nos:
328 filters["serial_no"] = ("in", serial_nos)
329
rohitwaghchaure07432892023-12-17 12:42:07 +0530330 entries = frappe.get_all(
331 "Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx"
332 )
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530333 if not entries:
334 return []
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530335
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530336 return [d.serial_no for d in entries if d.serial_no]
337
338
rohitwaghchaure07432892023-12-17 12:42:07 +0530339def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
340 if not serial_and_batch_bundle:
341 return []
342
343 filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")}
344 if isinstance(serial_and_batch_bundle, list):
345 filters = {"parent": ("in", serial_and_batch_bundle)}
346
347 if batches:
348 filters["batch_no"] = ("in", batches)
349
350 entries = frappe.get_all(
351 "Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1
352 )
353 if not entries:
354 return frappe._dict({})
355
356 return frappe._dict(entries)
357
358
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530359def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
360 return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530361
362
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530363def get_serial_or_batch_nos(bundle):
Rohit Waghchaure9cf645e2023-06-26 16:00:53 +0530364 # For print format
365
366 bundle_data = frappe.get_cached_value(
367 "Serial and Batch Bundle", bundle, ["has_serial_no", "has_batch_no"], as_dict=True
368 )
369
370 fields = []
371 if bundle_data.has_serial_no:
372 fields.append("serial_no")
373
374 if bundle_data.has_batch_no:
375 fields.extend(["batch_no", "qty"])
376
377 data = frappe.get_all("Serial and Batch Entry", fields=fields, filters={"parent": bundle})
378
379 if bundle_data.has_serial_no and not bundle_data.has_batch_no:
380 return ", ".join([d.serial_no for d in data])
381
382 elif bundle_data.has_batch_no:
383 html = "<table class= 'table table-borderless' style='margin-top: 0px;margin-bottom: 0px;'>"
384 for d in data:
385 if d.serial_no:
386 html += f"<tr><td>{d.batch_no}</th><th>{d.serial_no}</th ><th>{abs(d.qty)}</th></tr>"
387 else:
388 html += f"<tr><td>{d.batch_no}</td><td>{abs(d.qty)}</td></tr>"
389
390 html += "</table>"
391
392 return html
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530393
394
Rohit Waghchaure46704642023-03-23 11:41:20 +0530395class SerialNoValuation(DeprecatedSerialNoValuation):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530396 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530397 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530398 setattr(self, key, value)
399
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530400 self.calculate_stock_value_change()
401 self.calculate_valuation_rate()
402
403 def calculate_stock_value_change(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530404 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530405 self.stock_value_change = frappe.get_cached_value(
406 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
407 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530408
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530409 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530410 entries = self.get_serial_no_ledgers()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530411
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530412 self.serial_no_incoming_rate = defaultdict(float)
413 self.stock_value_change = 0.0
414
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530415 for ledger in entries:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530416 self.stock_value_change += ledger.incoming_rate
417 self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530418
419 self.calculate_stock_value_from_deprecarated_ledgers()
420
421 def get_serial_no_ledgers(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530422 serial_nos = self.get_serial_nos()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530423 bundle = frappe.qb.DocType("Serial and Batch Bundle")
424 bundle_child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530425
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530426 query = (
427 frappe.qb.from_(bundle)
428 .inner_join(bundle_child)
429 .on(bundle.name == bundle_child.parent)
430 .select(
431 bundle.name,
432 bundle_child.serial_no,
433 (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"),
434 )
435 .where(
436 (bundle.is_cancelled == 0)
437 & (bundle.docstatus == 1)
438 & (bundle_child.serial_no.isin(serial_nos))
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530439 & (bundle.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530440 & (bundle.item_code == self.sle.item_code)
441 & (bundle_child.warehouse == self.sle.warehouse)
442 )
443 .orderby(bundle.posting_date, bundle.posting_time, bundle.creation)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530444 )
445
rohitwaghchaure5e9016f2023-12-01 21:42:22 +0530446 # Important to exclude the current voucher to calculate correct the stock value difference
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530447 if self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530448 query = query.where(bundle.voucher_no != self.sle.voucher_no)
449
450 if self.sle.posting_date:
451 if self.sle.posting_time is None:
452 self.sle.posting_time = nowtime()
453
454 timestamp_condition = CombineDatetime(
455 bundle.posting_date, bundle.posting_time
456 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
457
458 query = query.where(timestamp_condition)
459
460 return query.run(as_dict=True)
461
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530462 def get_serial_nos(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530463 if self.sle.get("serial_nos"):
464 return self.sle.serial_nos
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530465
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530466 return get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530467
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530468 def calculate_valuation_rate(self):
469 if not hasattr(self, "wh_data"):
470 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530471
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530472 new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530473
474 if new_stock_qty > 0:
475 new_stock_value = (
476 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530477 ) + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530478 if new_stock_value >= 0:
479 # calculate new valuation rate only if stock value is positive
480 # else it remains the same as that of previous entry
481 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
482
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530483 if (
484 not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry()
485 ):
486 allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate(
487 self.sle.voucher_type, self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530488 )
489 if not allow_zero_rate:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530490 self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530491
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530492 self.wh_data.qty_after_transaction += self.sle.actual_qty
493 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
494 self.wh_data.valuation_rate
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530495 )
496
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530497 def is_rejected_entry(self):
498 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530499
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530500 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530501 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530502
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530503 def get_incoming_rate_of_serial_no(self, serial_no):
504 return self.serial_no_incoming_rate.get(serial_no, 0.0)
505
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530506
507def is_rejected(voucher_type, voucher_detail_no, warehouse):
508 if voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
509 return warehouse == frappe.get_cached_value(
510 voucher_type + " Item", voucher_detail_no, "rejected_warehouse"
511 )
512
513 return False
514
515
Rohit Waghchaure46704642023-03-23 11:41:20 +0530516class BatchNoValuation(DeprecatedBatchNoValuation):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530517 def __init__(self, **kwargs):
518 for key, value in kwargs.items():
519 setattr(self, key, value)
520
521 self.batch_nos = self.get_batch_nos()
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530522 self.prepare_batches()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530523 self.calculate_avg_rate()
524 self.calculate_valuation_rate()
525
526 def calculate_avg_rate(self):
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530527 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530528 self.stock_value_change = frappe.get_cached_value(
529 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530530 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530531 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530532 entries = self.get_batch_no_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530533 self.stock_value_change = 0.0
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530534 self.batch_avg_rate = defaultdict(float)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530535 self.available_qty = defaultdict(float)
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530536 self.stock_value_differece = defaultdict(float)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530537
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530538 for ledger in entries:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530539 self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530540 self.available_qty[ledger.batch_no] += flt(ledger.qty)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530541
542 self.calculate_avg_rate_from_deprecarated_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530543 self.calculate_avg_rate_for_non_batchwise_valuation()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530544 self.set_stock_value_difference()
545
546 def get_batch_no_ledgers(self) -> List[dict]:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530547 if not self.batchwise_valuation_batches:
548 return []
549
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530550 parent = frappe.qb.DocType("Serial and Batch Bundle")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530551 child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530552
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530553 timestamp_condition = ""
554 if self.sle.posting_date and self.sle.posting_time:
555 timestamp_condition = CombineDatetime(
556 parent.posting_date, parent.posting_time
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530557 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530558
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530559 query = (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530560 frappe.qb.from_(parent)
561 .inner_join(child)
562 .on(parent.name == child.parent)
563 .select(
564 child.batch_no,
565 Sum(child.stock_value_difference).as_("incoming_rate"),
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530566 Sum(child.qty).as_("qty"),
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530567 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530568 .where(
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530569 (child.batch_no.isin(self.batchwise_valuation_batches))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530570 & (parent.warehouse == self.sle.warehouse)
571 & (parent.item_code == self.sle.item_code)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530572 & (parent.docstatus == 1)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530573 & (parent.is_cancelled == 0)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530574 & (parent.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530575 )
576 .groupby(child.batch_no)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530577 )
578
rohitwaghchaure5e9016f2023-12-01 21:42:22 +0530579 # Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
580 if self.sle.voucher_detail_no:
581 query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no)
582 elif self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530583 query = query.where(parent.voucher_no != self.sle.voucher_no)
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530584
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530585 if timestamp_condition:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530586 query = query.where(timestamp_condition)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530587
588 return query.run(as_dict=True)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530589
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530590 def prepare_batches(self):
591 self.batches = self.batch_nos
592 if isinstance(self.batch_nos, dict):
593 self.batches = list(self.batch_nos.keys())
594
595 self.batchwise_valuation_batches = []
596 self.non_batchwise_valuation_batches = []
597
598 batches = frappe.get_all(
599 "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
600 )
601
602 for batch in batches:
603 self.batchwise_valuation_batches.append(batch.name)
604
605 self.non_batchwise_valuation_batches = list(
606 set(self.batches) - set(self.batchwise_valuation_batches)
607 )
608
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530609 def get_batch_nos(self) -> list:
610 if self.sle.get("batch_nos"):
611 return self.sle.batch_nos
612
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530613 return get_batch_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530614
615 def set_stock_value_difference(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530616 for batch_no, ledger in self.batch_nos.items():
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530617 if batch_no in self.non_batchwise_valuation_batches:
618 continue
619
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530620 if not self.available_qty[batch_no]:
621 continue
622
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530623 self.batch_avg_rate[batch_no] = (
624 self.stock_value_differece[batch_no] / self.available_qty[batch_no]
625 )
626
627 # New Stock Value Difference
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530628 stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530629 self.stock_value_change += stock_value_change
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530630
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530631 frappe.db.set_value(
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530632 "Serial and Batch Entry",
633 ledger.name,
634 {
635 "stock_value_difference": stock_value_change,
636 "incoming_rate": self.batch_avg_rate[batch_no],
637 },
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530638 )
639
640 def calculate_valuation_rate(self):
641 if not hasattr(self, "wh_data"):
642 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530643
644 self.wh_data.stock_value = round_off_if_near_zero(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530645 self.wh_data.stock_value + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530646 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530647
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530648 self.wh_data.qty_after_transaction += self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530649 if self.wh_data.qty_after_transaction:
650 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
651
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530652 def get_incoming_rate(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530653 if not self.sle.actual_qty:
654 self.sle.actual_qty = self.get_actual_qty()
655
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530656 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530657
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530658 def get_actual_qty(self):
659 total_qty = 0.0
660 for batch_no in self.available_qty:
661 total_qty += self.available_qty[batch_no]
662
663 return total_qty
664
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530665
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530666def get_batch_nos(serial_and_batch_bundle):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530667 if not serial_and_batch_bundle:
668 return frappe._dict({})
669
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530670 entries = frappe.get_all(
671 "Serial and Batch Entry",
672 fields=["batch_no", "qty", "name"],
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530673 filters={"parent": serial_and_batch_bundle, "batch_no": ("is", "set")},
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530674 order_by="idx",
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530675 )
676
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530677 if not entries:
678 return frappe._dict({})
679
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530680 return {d.batch_no: d for d in entries}
681
682
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530683def get_empty_batches_based_work_order(work_order, item_code):
Rohit Waghchaure46704642023-03-23 11:41:20 +0530684 batches = get_batches_from_work_order(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530685 if not batches:
686 return batches
687
Rohit Waghchaure46704642023-03-23 11:41:20 +0530688 entries = get_batches_from_stock_entries(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530689 if not entries:
690 return batches
691
692 ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
693 if ids:
694 set_batch_details_from_package(ids, batches)
695
696 # Will be deprecated in v16
697 for d in entries:
698 if not d.batch_no:
699 continue
700
701 batches[d.batch_no] -= d.qty
702
703 return batches
704
705
Rohit Waghchaure46704642023-03-23 11:41:20 +0530706def get_batches_from_work_order(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530707 return frappe._dict(
708 frappe.get_all(
Rohit Waghchaure46704642023-03-23 11:41:20 +0530709 "Batch",
710 fields=["name", "qty_to_produce"],
711 filters={"reference_name": work_order, "item": item_code},
712 as_list=1,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530713 )
714 )
715
716
Rohit Waghchaure46704642023-03-23 11:41:20 +0530717def get_batches_from_stock_entries(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530718 entries = frappe.get_all(
719 "Stock Entry",
720 filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
721 fields=["name"],
722 )
723
724 return frappe.get_all(
725 "Stock Entry Detail",
726 fields=["batch_no", "qty", "serial_and_batch_bundle"],
727 filters={
728 "parent": ("in", [d.name for d in entries]),
729 "is_finished_item": 1,
Rohit Waghchaure46704642023-03-23 11:41:20 +0530730 "item_code": item_code,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530731 },
732 )
733
734
735def set_batch_details_from_package(ids, batches):
736 entries = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530737 "Serial and Batch Entry",
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530738 filters={"parent": ("in", ids), "is_outward": 0},
739 fields=["batch_no", "qty"],
740 )
741
742 for d in entries:
743 batches[d.batch_no] -= d.qty
Rohit Waghchaure46704642023-03-23 11:41:20 +0530744
745
746class SerialBatchCreation:
747 def __init__(self, args):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530748 self.set(args)
749 self.set_item_details()
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530750 self.set_other_details()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530751
752 def set(self, args):
753 self.__dict__ = {}
Rohit Waghchaure46704642023-03-23 11:41:20 +0530754 for key, value in args.items():
755 setattr(self, key, value)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530756 self.__dict__[key] = value
757
758 def get(self, key):
759 return self.__dict__.get(key)
760
761 def set_item_details(self):
762 fields = [
763 "has_batch_no",
764 "has_serial_no",
765 "item_name",
766 "item_group",
767 "serial_no_series",
768 "create_new_batch",
769 "batch_number_series",
770 "description",
771 ]
772
773 item_details = frappe.get_cached_value("Item", self.item_code, fields, as_dict=1)
774 for key, value in item_details.items():
775 setattr(self, key, value)
776
777 self.__dict__.update(item_details)
Rohit Waghchaure46704642023-03-23 11:41:20 +0530778
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530779 def set_other_details(self):
780 if not self.get("posting_date"):
781 setattr(self, "posting_date", today())
782 self.__dict__["posting_date"] = self.posting_date
783
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530784 if not self.get("actual_qty"):
785 qty = self.get("qty") or self.get("total_qty")
786
787 setattr(self, "actual_qty", qty)
788 self.__dict__["actual_qty"] = self.actual_qty
789
Rohit Waghchaure46704642023-03-23 11:41:20 +0530790 def duplicate_package(self):
791 if not self.serial_and_batch_bundle:
792 return
793
794 id = self.serial_and_batch_bundle
795 package = frappe.get_doc("Serial and Batch Bundle", id)
796 new_package = frappe.copy_doc(package)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530797
798 if self.get("returned_serial_nos"):
799 self.remove_returned_serial_nos(new_package)
800
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530801 new_package.docstatus = 0
Rohit Waghchaure46704642023-03-23 11:41:20 +0530802 new_package.type_of_transaction = self.type_of_transaction
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530803 new_package.returned_against = self.get("returned_against")
Rohit Waghchaure46704642023-03-23 11:41:20 +0530804 new_package.save()
805
806 self.serial_and_batch_bundle = new_package.name
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530807
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530808 def remove_returned_serial_nos(self, package):
809 remove_list = []
810 for d in package.entries:
811 if d.serial_no in self.returned_serial_nos:
812 remove_list.append(d)
813
814 for d in remove_list:
815 package.remove(d)
816
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530817 def make_serial_and_batch_bundle(self):
818 doc = frappe.new_doc("Serial and Batch Bundle")
819 valid_columns = doc.meta.get_valid_columns()
820 for key, value in self.__dict__.items():
821 if key in valid_columns:
822 doc.set(key, value)
823
824 if self.type_of_transaction == "Outward":
825 self.set_auto_serial_batch_entries_for_outward()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530826 elif self.type_of_transaction == "Inward":
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530827 self.set_auto_serial_batch_entries_for_inward()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530828 self.add_serial_nos_for_batch_item()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530829
830 self.set_serial_batch_entries(doc)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530831 if not doc.get("entries"):
832 return frappe._dict({})
833
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530834 doc.save()
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530835 self.validate_qty(doc)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530836
837 if not hasattr(self, "do_not_submit") or not self.do_not_submit:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530838 doc.flags.ignore_voucher_validation = True
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530839 doc.submit()
840
841 return doc
842
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530843 def add_serial_nos_for_batch_item(self):
844 if not (self.has_serial_no and self.has_batch_no):
845 return
846
847 if not self.get("serial_nos") and self.get("batches"):
848 batches = list(self.get("batches").keys())
849 if len(batches) == 1:
850 self.batch_no = batches[0]
851 self.serial_nos = self.get_auto_created_serial_nos()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530852
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530853 def update_serial_and_batch_entries(self):
854 doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
855 doc.type_of_transaction = self.type_of_transaction
856 doc.set("entries", [])
857 self.set_auto_serial_batch_entries_for_outward()
858 self.set_serial_batch_entries(doc)
859 if not doc.get("entries"):
860 return frappe._dict({})
861
862 doc.save()
863 return doc
864
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530865 def validate_qty(self, doc):
866 if doc.type_of_transaction == "Outward":
867 precision = doc.precision("total_qty")
868
869 total_qty = abs(flt(doc.total_qty, precision))
870 required_qty = abs(flt(self.actual_qty, precision))
871
872 if required_qty - total_qty > 0:
873 msg = f"For the item {bold(doc.item_code)}, the Avaliable qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse."
874 frappe.throw(msg, title=_("Insufficient Stock"))
875
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530876 def set_auto_serial_batch_entries_for_outward(self):
877 from erpnext.stock.doctype.batch.batch import get_available_batches
878 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
879
880 kwargs = frappe._dict(
881 {
882 "item_code": self.item_code,
883 "warehouse": self.warehouse,
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530884 "qty": abs(self.actual_qty) if self.actual_qty else 0,
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530885 "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
886 }
887 )
888
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530889 if self.get("ignore_serial_nos"):
890 kwargs["ignore_serial_nos"] = self.ignore_serial_nos
891
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530892 if self.has_serial_no and not self.get("serial_nos"):
893 self.serial_nos = get_serial_nos_for_outward(kwargs)
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530894 elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530895 self.batches = get_available_batches(kwargs)
896
897 def set_auto_serial_batch_entries_for_inward(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530898 if (self.get("batches") and self.has_batch_no) or (
899 self.get("serial_nos") and self.has_serial_no
900 ):
901 return
902
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530903 self.batch_no = None
904 if self.has_batch_no:
905 self.batch_no = self.create_batch()
906
907 if self.has_serial_no:
908 self.serial_nos = self.get_auto_created_serial_nos()
909 else:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530910 self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530911
912 def set_serial_batch_entries(self, doc):
913 if self.get("serial_nos"):
914 serial_no_wise_batch = frappe._dict({})
915 if self.has_batch_no:
s-aga-r2d8363a2023-09-02 11:02:24 +0530916 serial_no_wise_batch = get_serial_nos_batch(self.serial_nos)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530917
918 qty = -1 if self.type_of_transaction == "Outward" else 1
919 for serial_no in self.serial_nos:
920 doc.append(
921 "entries",
922 {
923 "serial_no": serial_no,
924 "qty": qty,
925 "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
926 "incoming_rate": self.get("incoming_rate"),
927 },
928 )
929
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530930 elif self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530931 for batch_no, batch_qty in self.batches.items():
932 doc.append(
933 "entries",
934 {
935 "batch_no": batch_no,
936 "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
937 "incoming_rate": self.get("incoming_rate"),
938 },
939 )
940
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530941 def create_batch(self):
942 from erpnext.stock.doctype.batch.batch import make_batch
943
944 return make_batch(
945 frappe._dict(
946 {
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530947 "item": self.get("item_code"),
948 "reference_doctype": self.get("voucher_type"),
949 "reference_name": self.get("voucher_no"),
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530950 }
951 )
952 )
953
954 def get_auto_created_serial_nos(self):
955 sr_nos = []
956 serial_nos_details = []
957
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530958 if not self.serial_no_series:
959 msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually."
960 frappe.throw(_(msg))
961
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530962 for i in range(abs(cint(self.actual_qty))):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530963 serial_no = make_autoname(self.serial_no_series, "Serial No")
964 sr_nos.append(serial_no)
965 serial_nos_details.append(
966 (
967 serial_no,
968 serial_no,
969 now(),
970 now(),
971 frappe.session.user,
972 frappe.session.user,
973 self.warehouse,
974 self.company,
975 self.item_code,
976 self.item_name,
977 self.description,
978 "Active",
979 self.batch_no,
980 )
981 )
982
983 if serial_nos_details:
984 fields = [
985 "name",
986 "serial_no",
987 "creation",
988 "modified",
989 "owner",
990 "modified_by",
991 "warehouse",
992 "company",
993 "item_code",
994 "item_name",
995 "description",
996 "status",
997 "batch_no",
998 ]
999
1000 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
1001
1002 return sr_nos
1003
1004
1005def get_serial_or_batch_items(items):
1006 serial_or_batch_items = frappe.get_all(
1007 "Item",
1008 filters={"name": ("in", [d.item_code for d in items])},
1009 or_filters={"has_serial_no": 1, "has_batch_no": 1},
1010 )
1011
1012 if not serial_or_batch_items:
1013 return
1014 else:
1015 serial_or_batch_items = [d.name for d in serial_or_batch_items]
1016
1017 return serial_or_batch_items
s-aga-r2d8363a2023-09-02 11:02:24 +05301018
1019
1020def get_serial_nos_batch(serial_nos):
1021 return frappe._dict(
1022 frappe.get_all(
1023 "Serial No",
1024 fields=["name", "batch_no"],
1025 filters={"name": ("in", serial_nos)},
1026 as_list=1,
1027 )
1028 )