blob: 72945e9abc33f8d97af10c4634cd8cc510cf0388 [file] [log] [blame]
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05301from collections import defaultdict
Rohit Waghchauref1b59662023-03-06 12:08:28 +05302
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05303import frappe
4from frappe import _, bold
5from frappe.model.naming import make_autoname
rohitwaghchaure63792382024-03-04 12:04:41 +05306from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp
rohitwaghchaure4b24fcd2024-02-20 23:45:07 +05307from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today
rohitwaghchaure63792382024-03-04 12:04:41 +05308from pypika import Order
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):
Akhil Narang3effaf22024-03-27 11:37:26 +0530132 self.sle.db_set({"serial_and_batch_bundle": sn_doc.name, "auto_created_serial_and_batch_bundle": 1})
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530133
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530134 if sn_doc.is_rejected:
135 frappe.db.set_value(
Akhil Narang3effaf22024-03-27 11:37:26 +0530136 self.child_doctype,
137 self.sle.voucher_detail_no,
138 "rejected_serial_and_batch_bundle",
139 sn_doc.name,
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530140 )
141 else:
rohitwaghchaure4b24fcd2024-02-20 23:45:07 +0530142 values_to_update = {
143 "serial_and_batch_bundle": sn_doc.name,
144 }
145
rohitwaghchaure08caa7c2024-02-26 18:49:56 +0530146 if not frappe.db.get_single_value(
147 "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"
148 ):
rohitwaghchaure4b24fcd2024-02-20 23:45:07 +0530149 if sn_doc.has_serial_no:
Rohit Waghchaure635174f2024-02-23 19:33:52 +0530150 values_to_update["serial_no"] = ",".join(cstr(d.serial_no) for d in sn_doc.entries)
rohitwaghchaure4b24fcd2024-02-20 23:45:07 +0530151 elif sn_doc.has_batch_no and len(sn_doc.entries) == 1:
152 values_to_update["batch_no"] = sn_doc.entries[0].batch_no
153
154 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530155
156 @property
157 def child_doctype(self):
158 child_doctype = self.sle.voucher_type + " Item"
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530159
Akhil Narang3effaf22024-03-27 11:37:26 +0530160 if self.sle.voucher_type == "Subcontracting Receipt" and self.sle.dependant_sle_voucher_detail_no:
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530161 child_doctype = "Subcontracting Receipt Supplied Item"
162
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530163 if self.sle.voucher_type == "Stock Entry":
164 child_doctype = "Stock Entry Detail"
165
Rohit Waghchaure26b39ac2023-04-06 01:36:18 +0530166 if self.sle.voucher_type == "Asset Capitalization":
167 child_doctype = "Asset Capitalization Stock Item"
168
169 if self.sle.voucher_type == "Asset Repair":
170 child_doctype = "Asset Repair Consumed Item"
171
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530172 return child_doctype
173
174 def is_rejected_entry(self):
175 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
176
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530177 def process_batch_no(self):
178 if (
179 not self.sle.is_cancelled
180 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530181 and self.item_details.has_batch_no == 1
182 and self.item_details.create_new_batch
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530183 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530184 self.make_serial_batch_no_bundle()
185 elif not self.sle.is_cancelled:
186 self.validate_item_and_warehouse()
187
188 def validate_item_and_warehouse(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530189 if self.sle.serial_and_batch_bundle and not frappe.db.exists(
190 "Serial and Batch Bundle",
191 {
192 "name": self.sle.serial_and_batch_bundle,
193 "item_code": self.item_code,
194 "warehouse": self.warehouse,
195 "voucher_no": self.sle.voucher_no,
196 },
197 ):
198 msg = f"""
199 The Serial and Batch Bundle
200 {bold(self.sle.serial_and_batch_bundle)}
201 does not belong to Item {bold(self.item_code)}
202 or Warehouse {bold(self.warehouse)}
203 or {self.sle.voucher_type} no {bold(self.sle.voucher_no)}
204 """
205
206 frappe.throw(_(msg))
207
208 def delink_serial_and_batch_bundle(self):
209 update_values = {
210 "serial_and_batch_bundle": "",
211 }
212
213 if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse):
214 update_values["rejected_serial_and_batch_bundle"] = ""
215
216 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values)
217
218 frappe.db.set_value(
219 "Serial and Batch Bundle",
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530220 {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
rohitwaghchauref09e2132024-01-04 14:58:02 +0530221 {"is_cancelled": 1},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530222 )
223
Rohit Waghchauref79f2a32023-04-04 11:50:38 +0530224 if self.sle.serial_and_batch_bundle:
225 frappe.get_cached_doc(
226 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle
227 ).validate_serial_and_batch_inventory()
228
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530229 def post_process(self):
rohitwaghchaure07432892023-12-17 12:42:07 +0530230 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 +0530231 return
232
rohitwaghchaure07432892023-12-17 12:42:07 +0530233 if self.sle.serial_and_batch_bundle:
234 docstatus = frappe.get_cached_value(
235 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
236 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530237
rohitwaghchaure07432892023-12-17 12:42:07 +0530238 if docstatus != 1:
239 self.submit_serial_and_batch_bundle()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530240
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530241 if self.item_details.has_serial_no == 1:
242 self.set_warehouse_and_status_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530243
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530244 if (
245 self.sle.actual_qty > 0
246 and self.item_details.has_serial_no == 1
247 and self.item_details.has_batch_no == 1
248 ):
249 self.set_batch_no_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530250
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530251 if self.item_details.has_batch_no == 1:
252 self.update_batch_qty()
253
rohitwaghchaure6e5484e2024-01-02 12:54:18 +0530254 if self.sle.is_cancelled and self.sle.serial_and_batch_bundle:
255 self.cancel_serial_and_batch_bundle()
256
257 def cancel_serial_and_batch_bundle(self):
258 frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel()
259
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530260 def submit_serial_and_batch_bundle(self):
261 doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530262 self.validate_actual_qty(doc)
263
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530264 doc.flags.ignore_voucher_validation = True
265 doc.submit()
266
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530267 def set_warehouse_and_status_in_serial_nos(self):
rohitwaghchaure07432892023-12-17 12:42:07 +0530268 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
269
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530270 serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
rohitwaghchaure07432892023-12-17 12:42:07 +0530271 if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
272 serial_nos = get_parsed_serial_nos(self.sle.serial_no)
273
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530274 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
275
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530276 if not serial_nos:
277 return
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530278
rohitwaghchaure592fc812023-11-28 18:28:48 +0530279 status = "Inactive"
280 if self.sle.actual_qty < 0:
281 status = "Delivered"
282
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530283 sn_table = frappe.qb.DocType("Serial No")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530284 (
285 frappe.qb.update(sn_table)
286 .set(sn_table.warehouse, warehouse)
rohitwaghchaure07432892023-12-17 12:42:07 +0530287 .set(
288 sn_table.status,
289 "Active"
290 if warehouse
291 else status
292 if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
293 else "Inactive",
294 )
s-aga-r7a04f0f2024-02-05 12:35:26 +0530295 .set(sn_table.company, self.sle.company)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530296 .where(sn_table.name.isin(serial_nos))
297 ).run()
298
299 def set_batch_no_in_serial_nos(self):
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530300 entries = frappe.get_all(
301 "Serial and Batch Entry",
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530302 fields=["serial_no", "batch_no"],
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530303 filters={"parent": self.sle.serial_and_batch_bundle},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530304 )
305
306 batch_serial_nos = {}
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530307 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530308 batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no)
309
310 for batch_no, serial_nos in batch_serial_nos.items():
311 sn_table = frappe.qb.DocType("Serial No")
312 (
313 frappe.qb.update(sn_table)
314 .set(sn_table.batch_no, batch_no)
315 .where(sn_table.name.isin(serial_nos))
316 ).run()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530317
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530318 def update_batch_qty(self):
319 from erpnext.stock.doctype.batch.batch import get_available_batches
320
321 batches = get_batch_nos(self.sle.serial_and_batch_bundle)
rohitwaghchaure07432892023-12-17 12:42:07 +0530322 if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
323 batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530324
325 batches_qty = get_available_batches(
rohitwaghchauree178ffc2024-03-06 12:50:44 +0530326 frappe._dict({"item_code": self.item_code, "batch_no": list(batches.keys())})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530327 )
328
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530329 for batch_no in batches:
330 frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0))
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530331
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530332
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530333def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530334 if not serial_and_batch_bundle:
335 return []
336
337 filters = {"parent": serial_and_batch_bundle, "serial_no": ("is", "set")}
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530338 if isinstance(serial_and_batch_bundle, list):
339 filters = {"parent": ("in", serial_and_batch_bundle)}
340
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530341 if serial_nos:
342 filters["serial_no"] = ("in", serial_nos)
343
Akhil Narang3effaf22024-03-27 11:37:26 +0530344 entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx")
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530345 if not entries:
346 return []
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530347
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530348 return [d.serial_no for d in entries if d.serial_no]
349
350
rohitwaghchaure07432892023-12-17 12:42:07 +0530351def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
352 if not serial_and_batch_bundle:
353 return []
354
355 filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")}
356 if isinstance(serial_and_batch_bundle, list):
357 filters = {"parent": ("in", serial_and_batch_bundle)}
358
359 if batches:
360 filters["batch_no"] = ("in", batches)
361
362 entries = frappe.get_all(
363 "Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1
364 )
365 if not entries:
366 return frappe._dict({})
367
368 return frappe._dict(entries)
369
370
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530371def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
372 return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530373
374
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530375def get_serial_or_batch_nos(bundle):
Rohit Waghchaure9cf645e2023-06-26 16:00:53 +0530376 # For print format
377
378 bundle_data = frappe.get_cached_value(
379 "Serial and Batch Bundle", bundle, ["has_serial_no", "has_batch_no"], as_dict=True
380 )
381
382 fields = []
383 if bundle_data.has_serial_no:
384 fields.append("serial_no")
385
386 if bundle_data.has_batch_no:
387 fields.extend(["batch_no", "qty"])
388
389 data = frappe.get_all("Serial and Batch Entry", fields=fields, filters={"parent": bundle})
390
391 if bundle_data.has_serial_no and not bundle_data.has_batch_no:
392 return ", ".join([d.serial_no for d in data])
393
394 elif bundle_data.has_batch_no:
395 html = "<table class= 'table table-borderless' style='margin-top: 0px;margin-bottom: 0px;'>"
396 for d in data:
397 if d.serial_no:
398 html += f"<tr><td>{d.batch_no}</th><th>{d.serial_no}</th ><th>{abs(d.qty)}</th></tr>"
399 else:
400 html += f"<tr><td>{d.batch_no}</td><td>{abs(d.qty)}</td></tr>"
401
402 html += "</table>"
403
404 return html
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530405
406
Rohit Waghchaure46704642023-03-23 11:41:20 +0530407class SerialNoValuation(DeprecatedSerialNoValuation):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530408 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530409 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530410 setattr(self, key, value)
411
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530412 self.calculate_stock_value_change()
413 self.calculate_valuation_rate()
414
415 def calculate_stock_value_change(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530416 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530417 self.stock_value_change = frappe.get_cached_value(
418 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
419 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530420
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530421 else:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530422 self.serial_no_incoming_rate = defaultdict(float)
423 self.stock_value_change = 0.0
424
rohitwaghchaure63792382024-03-04 12:04:41 +0530425 serial_nos = self.get_serial_nos()
426 for serial_no in serial_nos:
427 incoming_rate = self.get_incoming_rate_from_bundle(serial_no)
428 if not incoming_rate:
429 continue
430
431 self.stock_value_change += incoming_rate
432 self.serial_no_incoming_rate[serial_no] += incoming_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530433
434 self.calculate_stock_value_from_deprecarated_ledgers()
435
rohitwaghchaure63792382024-03-04 12:04:41 +0530436 def get_incoming_rate_from_bundle(self, serial_no) -> float:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530437 bundle = frappe.qb.DocType("Serial and Batch Bundle")
438 bundle_child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530439
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530440 query = (
441 frappe.qb.from_(bundle)
442 .inner_join(bundle_child)
443 .on(bundle.name == bundle_child.parent)
rohitwaghchaure63792382024-03-04 12:04:41 +0530444 .select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"))
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530445 .where(
446 (bundle.is_cancelled == 0)
447 & (bundle.docstatus == 1)
rohitwaghchaure63792382024-03-04 12:04:41 +0530448 & (bundle_child.serial_no == serial_no)
449 & (bundle.type_of_transaction == "Inward")
450 & (bundle_child.qty > 0)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530451 & (bundle.item_code == self.sle.item_code)
452 & (bundle_child.warehouse == self.sle.warehouse)
453 )
rohitwaghchaure63792382024-03-04 12:04:41 +0530454 .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc)
455 .limit(1)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530456 )
457
rohitwaghchaure5e9016f2023-12-01 21:42:22 +0530458 # Important to exclude the current voucher to calculate correct the stock value difference
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530459 if self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530460 query = query.where(bundle.voucher_no != self.sle.voucher_no)
461
462 if self.sle.posting_date:
463 if self.sle.posting_time is None:
464 self.sle.posting_time = nowtime()
465
466 timestamp_condition = CombineDatetime(
467 bundle.posting_date, bundle.posting_time
468 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
469
470 query = query.where(timestamp_condition)
471
rohitwaghchaure63792382024-03-04 12:04:41 +0530472 incoming_rate = query.run()
473 return flt(incoming_rate[0][0]) if incoming_rate else 0.0
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530474
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530475 def get_serial_nos(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530476 if self.sle.get("serial_nos"):
477 return self.sle.serial_nos
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530478
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530479 return get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530480
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530481 def calculate_valuation_rate(self):
482 if not hasattr(self, "wh_data"):
483 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530484
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530485 new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530486
487 if new_stock_qty > 0:
488 new_stock_value = (
489 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530490 ) + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530491 if new_stock_value >= 0:
492 # calculate new valuation rate only if stock value is positive
493 # else it remains the same as that of previous entry
494 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
495
Akhil Narang3effaf22024-03-27 11:37:26 +0530496 if not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry():
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530497 allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate(
498 self.sle.voucher_type, self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530499 )
500 if not allow_zero_rate:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530501 self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530502
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530503 self.wh_data.qty_after_transaction += self.sle.actual_qty
Akhil Narang3effaf22024-03-27 11:37:26 +0530504 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530505
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530506 def is_rejected_entry(self):
507 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530508
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530509 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530510 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530511
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530512 def get_incoming_rate_of_serial_no(self, serial_no):
513 return self.serial_no_incoming_rate.get(serial_no, 0.0)
514
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530515
516def is_rejected(voucher_type, voucher_detail_no, warehouse):
517 if voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
518 return warehouse == frappe.get_cached_value(
519 voucher_type + " Item", voucher_detail_no, "rejected_warehouse"
520 )
521
522 return False
523
524
Rohit Waghchaure46704642023-03-23 11:41:20 +0530525class BatchNoValuation(DeprecatedBatchNoValuation):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530526 def __init__(self, **kwargs):
527 for key, value in kwargs.items():
528 setattr(self, key, value)
529
530 self.batch_nos = self.get_batch_nos()
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530531 self.prepare_batches()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530532 self.calculate_avg_rate()
533 self.calculate_valuation_rate()
534
535 def calculate_avg_rate(self):
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530536 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530537 self.stock_value_change = frappe.get_cached_value(
538 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530539 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530540 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530541 entries = self.get_batch_no_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530542 self.stock_value_change = 0.0
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530543 self.batch_avg_rate = defaultdict(float)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530544 self.available_qty = defaultdict(float)
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530545 self.stock_value_differece = defaultdict(float)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530546
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530547 for ledger in entries:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530548 self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530549 self.available_qty[ledger.batch_no] += flt(ledger.qty)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530550
551 self.calculate_avg_rate_from_deprecarated_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530552 self.calculate_avg_rate_for_non_batchwise_valuation()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530553 self.set_stock_value_difference()
554
Akhil Narang3effaf22024-03-27 11:37:26 +0530555 def get_batch_no_ledgers(self) -> list[dict]:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530556 if not self.batchwise_valuation_batches:
557 return []
558
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530559 parent = frappe.qb.DocType("Serial and Batch Bundle")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530560 child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530561
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530562 timestamp_condition = ""
563 if self.sle.posting_date and self.sle.posting_time:
564 timestamp_condition = CombineDatetime(
565 parent.posting_date, parent.posting_time
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530566 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530567
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530568 query = (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530569 frappe.qb.from_(parent)
570 .inner_join(child)
571 .on(parent.name == child.parent)
572 .select(
573 child.batch_no,
574 Sum(child.stock_value_difference).as_("incoming_rate"),
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530575 Sum(child.qty).as_("qty"),
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530576 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530577 .where(
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530578 (child.batch_no.isin(self.batchwise_valuation_batches))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530579 & (parent.warehouse == self.sle.warehouse)
580 & (parent.item_code == self.sle.item_code)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530581 & (parent.docstatus == 1)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530582 & (parent.is_cancelled == 0)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530583 & (parent.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530584 )
585 .groupby(child.batch_no)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530586 )
587
rohitwaghchaure5e9016f2023-12-01 21:42:22 +0530588 # Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
589 if self.sle.voucher_detail_no:
590 query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no)
591 elif self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530592 query = query.where(parent.voucher_no != self.sle.voucher_no)
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530593
rohitwaghchaurea4198122024-03-12 14:18:39 +0530594 query = query.where(parent.voucher_type != "Pick List")
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530595 if timestamp_condition:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530596 query = query.where(timestamp_condition)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530597
598 return query.run(as_dict=True)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530599
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530600 def prepare_batches(self):
601 self.batches = self.batch_nos
602 if isinstance(self.batch_nos, dict):
603 self.batches = list(self.batch_nos.keys())
604
605 self.batchwise_valuation_batches = []
606 self.non_batchwise_valuation_batches = []
607
608 batches = frappe.get_all(
609 "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
610 )
611
612 for batch in batches:
613 self.batchwise_valuation_batches.append(batch.name)
614
Akhil Narang3effaf22024-03-27 11:37:26 +0530615 self.non_batchwise_valuation_batches = list(set(self.batches) - set(self.batchwise_valuation_batches))
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530616
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530617 def get_batch_nos(self) -> list:
618 if self.sle.get("batch_nos"):
619 return self.sle.batch_nos
620
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530621 return get_batch_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530622
623 def set_stock_value_difference(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530624 for batch_no, ledger in self.batch_nos.items():
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530625 if batch_no in self.non_batchwise_valuation_batches:
626 continue
627
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530628 if not self.available_qty[batch_no]:
629 continue
630
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530631 self.batch_avg_rate[batch_no] = (
632 self.stock_value_differece[batch_no] / self.available_qty[batch_no]
633 )
634
635 # New Stock Value Difference
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530636 stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530637 self.stock_value_change += stock_value_change
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530638
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530639 frappe.db.set_value(
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530640 "Serial and Batch Entry",
641 ledger.name,
642 {
643 "stock_value_difference": stock_value_change,
644 "incoming_rate": self.batch_avg_rate[batch_no],
645 },
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530646 )
647
648 def calculate_valuation_rate(self):
649 if not hasattr(self, "wh_data"):
650 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530651
Akhil Narang3effaf22024-03-27 11:37:26 +0530652 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + self.stock_value_change)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530653
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530654 self.wh_data.qty_after_transaction += self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530655 if self.wh_data.qty_after_transaction:
656 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
657
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530658 def get_incoming_rate(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530659 if not self.sle.actual_qty:
660 self.sle.actual_qty = self.get_actual_qty()
661
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530662 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530663
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530664 def get_actual_qty(self):
665 total_qty = 0.0
666 for batch_no in self.available_qty:
667 total_qty += self.available_qty[batch_no]
668
669 return total_qty
670
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530671
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530672def get_batch_nos(serial_and_batch_bundle):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530673 if not serial_and_batch_bundle:
674 return frappe._dict({})
675
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530676 entries = frappe.get_all(
677 "Serial and Batch Entry",
678 fields=["batch_no", "qty", "name"],
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530679 filters={"parent": serial_and_batch_bundle, "batch_no": ("is", "set")},
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530680 order_by="idx",
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530681 )
682
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530683 if not entries:
684 return frappe._dict({})
685
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530686 return {d.batch_no: d for d in entries}
687
688
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530689def get_empty_batches_based_work_order(work_order, item_code):
Rohit Waghchaure46704642023-03-23 11:41:20 +0530690 batches = get_batches_from_work_order(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530691 if not batches:
692 return batches
693
Rohit Waghchaure46704642023-03-23 11:41:20 +0530694 entries = get_batches_from_stock_entries(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530695 if not entries:
696 return batches
697
698 ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
699 if ids:
700 set_batch_details_from_package(ids, batches)
701
702 # Will be deprecated in v16
703 for d in entries:
704 if not d.batch_no:
705 continue
706
707 batches[d.batch_no] -= d.qty
708
709 return batches
710
711
Rohit Waghchaure46704642023-03-23 11:41:20 +0530712def get_batches_from_work_order(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530713 return frappe._dict(
714 frappe.get_all(
Rohit Waghchaure46704642023-03-23 11:41:20 +0530715 "Batch",
716 fields=["name", "qty_to_produce"],
717 filters={"reference_name": work_order, "item": item_code},
718 as_list=1,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530719 )
720 )
721
722
Rohit Waghchaure46704642023-03-23 11:41:20 +0530723def get_batches_from_stock_entries(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530724 entries = frappe.get_all(
725 "Stock Entry",
726 filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
727 fields=["name"],
728 )
729
730 return frappe.get_all(
731 "Stock Entry Detail",
732 fields=["batch_no", "qty", "serial_and_batch_bundle"],
733 filters={
734 "parent": ("in", [d.name for d in entries]),
735 "is_finished_item": 1,
Rohit Waghchaure46704642023-03-23 11:41:20 +0530736 "item_code": item_code,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530737 },
738 )
739
740
741def set_batch_details_from_package(ids, batches):
742 entries = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530743 "Serial and Batch Entry",
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530744 filters={"parent": ("in", ids), "is_outward": 0},
745 fields=["batch_no", "qty"],
746 )
747
748 for d in entries:
749 batches[d.batch_no] -= d.qty
Rohit Waghchaure46704642023-03-23 11:41:20 +0530750
751
752class SerialBatchCreation:
753 def __init__(self, args):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530754 self.set(args)
755 self.set_item_details()
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530756 self.set_other_details()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530757
758 def set(self, args):
759 self.__dict__ = {}
Rohit Waghchaure46704642023-03-23 11:41:20 +0530760 for key, value in args.items():
761 setattr(self, key, value)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530762 self.__dict__[key] = value
763
764 def get(self, key):
765 return self.__dict__.get(key)
766
767 def set_item_details(self):
768 fields = [
769 "has_batch_no",
770 "has_serial_no",
771 "item_name",
772 "item_group",
773 "serial_no_series",
774 "create_new_batch",
775 "batch_number_series",
776 "description",
777 ]
778
779 item_details = frappe.get_cached_value("Item", self.item_code, fields, as_dict=1)
780 for key, value in item_details.items():
781 setattr(self, key, value)
782
783 self.__dict__.update(item_details)
Rohit Waghchaure46704642023-03-23 11:41:20 +0530784
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530785 def set_other_details(self):
786 if not self.get("posting_date"):
Akhil Narang3effaf22024-03-27 11:37:26 +0530787 self.posting_date = today()
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530788 self.__dict__["posting_date"] = self.posting_date
789
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530790 if not self.get("actual_qty"):
791 qty = self.get("qty") or self.get("total_qty")
792
Akhil Narang3effaf22024-03-27 11:37:26 +0530793 self.actual_qty = qty
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530794 self.__dict__["actual_qty"] = self.actual_qty
795
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530796 if not hasattr(self, "use_serial_batch_fields"):
Akhil Narang3effaf22024-03-27 11:37:26 +0530797 self.use_serial_batch_fields = 0
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530798
Rohit Waghchaure46704642023-03-23 11:41:20 +0530799 def duplicate_package(self):
800 if not self.serial_and_batch_bundle:
801 return
802
803 id = self.serial_and_batch_bundle
804 package = frappe.get_doc("Serial and Batch Bundle", id)
805 new_package = frappe.copy_doc(package)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530806
807 if self.get("returned_serial_nos"):
808 self.remove_returned_serial_nos(new_package)
809
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530810 new_package.docstatus = 0
rohitwaghchaure59222812024-03-15 17:55:41 +0530811 new_package.warehouse = self.warehouse
812 new_package.voucher_no = ""
813 new_package.posting_date = today()
814 new_package.posting_time = nowtime()
Rohit Waghchaure46704642023-03-23 11:41:20 +0530815 new_package.type_of_transaction = self.type_of_transaction
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530816 new_package.returned_against = self.get("returned_against")
Rohit Waghchaure46704642023-03-23 11:41:20 +0530817 new_package.save()
818
819 self.serial_and_batch_bundle = new_package.name
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530820
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530821 def remove_returned_serial_nos(self, package):
822 remove_list = []
823 for d in package.entries:
824 if d.serial_no in self.returned_serial_nos:
825 remove_list.append(d)
826
827 for d in remove_list:
828 package.remove(d)
829
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530830 def make_serial_and_batch_bundle(self):
831 doc = frappe.new_doc("Serial and Batch Bundle")
832 valid_columns = doc.meta.get_valid_columns()
833 for key, value in self.__dict__.items():
834 if key in valid_columns:
835 doc.set(key, value)
836
837 if self.type_of_transaction == "Outward":
838 self.set_auto_serial_batch_entries_for_outward()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530839 elif self.type_of_transaction == "Inward":
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530840 self.set_auto_serial_batch_entries_for_inward()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530841 self.add_serial_nos_for_batch_item()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530842
843 self.set_serial_batch_entries(doc)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530844 if not doc.get("entries"):
845 return frappe._dict({})
846
Rohit Waghchaured73b1f32024-03-30 10:46:56 +0530847 if doc.voucher_no and frappe.get_cached_value(doc.voucher_type, doc.voucher_no, "docstatus") == 2:
848 doc.voucher_no = ""
849
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530850 doc.save()
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530851 self.validate_qty(doc)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530852
853 if not hasattr(self, "do_not_submit") or not self.do_not_submit:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530854 doc.flags.ignore_voucher_validation = True
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530855 doc.submit()
856
857 return doc
858
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530859 def add_serial_nos_for_batch_item(self):
860 if not (self.has_serial_no and self.has_batch_no):
861 return
862
863 if not self.get("serial_nos") and self.get("batches"):
864 batches = list(self.get("batches").keys())
865 if len(batches) == 1:
866 self.batch_no = batches[0]
867 self.serial_nos = self.get_auto_created_serial_nos()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530868
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530869 def update_serial_and_batch_entries(self):
870 doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
871 doc.type_of_transaction = self.type_of_transaction
872 doc.set("entries", [])
873 self.set_auto_serial_batch_entries_for_outward()
874 self.set_serial_batch_entries(doc)
875 if not doc.get("entries"):
876 return frappe._dict({})
877
878 doc.save()
879 return doc
880
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530881 def validate_qty(self, doc):
Rohit Waghchaure016585c2024-03-26 18:19:27 +0530882 if doc.type_of_transaction == "Outward" and self.actual_qty and doc.total_qty:
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530883 precision = doc.precision("total_qty")
884
Rohit Waghchaure016585c2024-03-26 18:19:27 +0530885 total_qty = flt(abs(doc.total_qty), precision)
886 required_qty = flt(abs(self.actual_qty), precision)
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530887
888 if required_qty - total_qty > 0:
889 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."
890 frappe.throw(msg, title=_("Insufficient Stock"))
891
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530892 def set_auto_serial_batch_entries_for_outward(self):
893 from erpnext.stock.doctype.batch.batch import get_available_batches
894 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
895
896 kwargs = frappe._dict(
897 {
898 "item_code": self.item_code,
899 "warehouse": self.warehouse,
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530900 "qty": abs(self.actual_qty) if self.actual_qty else 0,
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530901 "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
902 }
903 )
904
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530905 if self.get("ignore_serial_nos"):
906 kwargs["ignore_serial_nos"] = self.ignore_serial_nos
907
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530908 if self.has_serial_no and not self.get("serial_nos"):
909 self.serial_nos = get_serial_nos_for_outward(kwargs)
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530910 elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530911 self.batches = get_available_batches(kwargs)
912
913 def set_auto_serial_batch_entries_for_inward(self):
Akhil Narang3effaf22024-03-27 11:37:26 +0530914 if (self.get("batches") and self.has_batch_no) or (self.get("serial_nos") and self.has_serial_no):
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530915 if self.use_serial_batch_fields and self.get("serial_nos"):
916 self.make_serial_no_if_not_exists()
917
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530918 return
919
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530920 self.batch_no = None
921 if self.has_batch_no:
922 self.batch_no = self.create_batch()
923
924 if self.has_serial_no:
925 self.serial_nos = self.get_auto_created_serial_nos()
926 else:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530927 self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530928
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530929 def make_serial_no_if_not_exists(self):
930 non_exists_serial_nos = []
931 for row in self.serial_nos:
932 if not frappe.db.exists("Serial No", row):
933 non_exists_serial_nos.append(row)
934
935 if non_exists_serial_nos:
936 self.make_serial_nos(non_exists_serial_nos)
937
938 def make_serial_nos(self, serial_nos):
939 serial_nos_details = []
940 batch_no = None
941 if self.batches:
Akhil Narang3effaf22024-03-27 11:37:26 +0530942 batch_no = next(iter(self.batches.keys()))
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530943
944 for serial_no in serial_nos:
945 serial_nos_details.append(
946 (
947 serial_no,
948 serial_no,
949 now(),
950 now(),
951 frappe.session.user,
952 frappe.session.user,
953 self.warehouse,
954 self.company,
955 self.item_code,
956 self.item_name,
957 self.description,
958 "Active",
959 batch_no,
960 )
961 )
962
963 if serial_nos_details:
964 fields = [
965 "name",
966 "serial_no",
967 "creation",
968 "modified",
969 "owner",
970 "modified_by",
971 "warehouse",
972 "company",
973 "item_code",
974 "item_name",
975 "description",
976 "status",
977 "batch_no",
978 ]
979
980 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
981
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530982 def set_serial_batch_entries(self, doc):
983 if self.get("serial_nos"):
984 serial_no_wise_batch = frappe._dict({})
985 if self.has_batch_no:
s-aga-r2d8363a2023-09-02 11:02:24 +0530986 serial_no_wise_batch = get_serial_nos_batch(self.serial_nos)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530987
988 qty = -1 if self.type_of_transaction == "Outward" else 1
989 for serial_no in self.serial_nos:
990 doc.append(
991 "entries",
992 {
993 "serial_no": serial_no,
994 "qty": qty,
995 "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
996 "incoming_rate": self.get("incoming_rate"),
997 },
998 )
999
Rohit Waghchauree88c5d62023-04-05 20:03:44 +05301000 elif self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301001 for batch_no, batch_qty in self.batches.items():
1002 doc.append(
1003 "entries",
1004 {
1005 "batch_no": batch_no,
1006 "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
1007 "incoming_rate": self.get("incoming_rate"),
1008 },
1009 )
1010
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301011 def create_batch(self):
1012 from erpnext.stock.doctype.batch.batch import make_batch
1013
1014 return make_batch(
1015 frappe._dict(
1016 {
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301017 "item": self.get("item_code"),
1018 "reference_doctype": self.get("voucher_type"),
1019 "reference_name": self.get("voucher_no"),
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301020 }
1021 )
1022 )
1023
1024 def get_auto_created_serial_nos(self):
1025 sr_nos = []
1026 serial_nos_details = []
1027
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301028 if not self.serial_no_series:
1029 msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually."
1030 frappe.throw(_(msg))
1031
Akhil Narang3effaf22024-03-27 11:37:26 +05301032 for _i in range(abs(cint(self.actual_qty))):
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301033 serial_no = make_autoname(self.serial_no_series, "Serial No")
1034 sr_nos.append(serial_no)
1035 serial_nos_details.append(
1036 (
1037 serial_no,
1038 serial_no,
1039 now(),
1040 now(),
1041 frappe.session.user,
1042 frappe.session.user,
1043 self.warehouse,
1044 self.company,
1045 self.item_code,
1046 self.item_name,
1047 self.description,
1048 "Active",
1049 self.batch_no,
1050 )
1051 )
1052
1053 if serial_nos_details:
1054 fields = [
1055 "name",
1056 "serial_no",
1057 "creation",
1058 "modified",
1059 "owner",
1060 "modified_by",
1061 "warehouse",
1062 "company",
1063 "item_code",
1064 "item_name",
1065 "description",
1066 "status",
1067 "batch_no",
1068 ]
1069
1070 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
1071
1072 return sr_nos
1073
1074
1075def get_serial_or_batch_items(items):
1076 serial_or_batch_items = frappe.get_all(
1077 "Item",
1078 filters={"name": ("in", [d.item_code for d in items])},
1079 or_filters={"has_serial_no": 1, "has_batch_no": 1},
1080 )
1081
1082 if not serial_or_batch_items:
1083 return
1084 else:
1085 serial_or_batch_items = [d.name for d in serial_or_batch_items]
1086
1087 return serial_or_batch_items
s-aga-r2d8363a2023-09-02 11:02:24 +05301088
1089
1090def get_serial_nos_batch(serial_nos):
1091 return frappe._dict(
1092 frappe.get_all(
1093 "Serial No",
1094 fields=["name", "batch_no"],
1095 filters={"name": ("in", serial_nos)},
1096 as_list=1,
1097 )
1098 )