blob: 4e87fa022d86e3979785fcc65bc6d185b58da822 [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
rohitwaghchaure63792382024-03-04 12:04:41 +05307from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp
rohitwaghchaure4b24fcd2024-02-20 23:45:07 +05308from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today
rohitwaghchaure63792382024-03-04 12:04:41 +05309from pypika import Order
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053010
11from erpnext.stock.deprecated_serial_batch import (
12 DeprecatedBatchNoValuation,
13 DeprecatedSerialNoValuation,
14)
Rohit Waghchauref1b59662023-03-06 12:08:28 +053015from erpnext.stock.valuation import round_off_if_near_zero
16
17
18class SerialBatchBundle:
19 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053020 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +053021 setattr(self, key, value)
22
23 self.set_item_details()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053024 self.process_serial_and_batch_bundle()
25 if self.sle.is_cancelled:
26 self.delink_serial_and_batch_bundle()
27
28 self.post_process()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053029
30 def process_serial_and_batch_bundle(self):
31 if self.item_details.has_serial_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053032 self.process_serial_no()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053033 elif self.item_details.has_batch_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053034 self.process_batch_no()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053035
36 def set_item_details(self):
37 fields = [
38 "has_batch_no",
39 "has_serial_no",
40 "item_name",
41 "item_group",
42 "serial_no_series",
43 "create_new_batch",
44 "batch_number_series",
45 ]
46
47 self.item_details = frappe.get_cached_value("Item", self.sle.item_code, fields, as_dict=1)
48
49 def process_serial_no(self):
50 if (
51 not self.sle.is_cancelled
52 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +053053 and self.item_details.has_serial_no == 1
Rohit Waghchauref1b59662023-03-06 12:08:28 +053054 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053055 self.make_serial_batch_no_bundle()
56 elif not self.sle.is_cancelled:
57 self.validate_item_and_warehouse()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053058
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053059 def make_serial_batch_no_bundle(self):
Rohit Waghchaure648efca2023-03-28 12:16:27 +053060 self.validate_item()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053061
Rohit Waghchaure648efca2023-03-28 12:16:27 +053062 sn_doc = SerialBatchCreation(
63 {
64 "item_code": self.item_code,
65 "warehouse": self.warehouse,
66 "posting_date": self.sle.posting_date,
67 "posting_time": self.sle.posting_time,
68 "voucher_type": self.sle.voucher_type,
69 "voucher_no": self.sle.voucher_no,
70 "voucher_detail_no": self.sle.voucher_detail_no,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +053071 "qty": self.sle.actual_qty,
Rohit Waghchaure648efca2023-03-28 12:16:27 +053072 "avg_rate": self.sle.incoming_rate,
73 "total_amount": flt(self.sle.actual_qty) * flt(self.sle.incoming_rate),
74 "type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward",
75 "company": self.company,
76 "is_rejected": self.is_rejected_entry(),
77 }
78 ).make_serial_and_batch_bundle()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053079
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053080 self.set_serial_and_batch_bundle(sn_doc)
Rohit Waghchauref1b59662023-03-06 12:08:28 +053081
Rohit Waghchaure42b22942023-05-27 19:18:03 +053082 def validate_actual_qty(self, sn_doc):
Rohit Waghchauref968f0f2023-06-14 23:22:22 +053083 link = get_link_to_form("Serial and Batch Bundle", sn_doc.name)
84
85 condition = {
86 "Inward": self.sle.actual_qty > 0,
87 "Outward": self.sle.actual_qty < 0,
88 }.get(sn_doc.type_of_transaction)
89
90 if not condition:
91 correct_type = "Inward"
92 if sn_doc.type_of_transaction == "Inward":
93 correct_type = "Outward"
94
95 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)}"
96 frappe.throw(_(msg), title=_("Incorrect Type of Transaction"))
97
Rohit Waghchaure42b22942023-05-27 19:18:03 +053098 precision = sn_doc.precision("total_qty")
99 if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision):
Rohit Waghchauref968f0f2023-06-14 23:22:22 +0530100 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 +0530101 frappe.throw(_(msg))
102
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530103 def validate_item(self):
104 msg = ""
105 if self.sle.actual_qty > 0:
106 if not self.item_details.has_batch_no and not self.item_details.has_serial_no:
107 msg = f"Item {self.item_code} is not a batch or serial no item"
108
109 if self.item_details.has_serial_no and not self.item_details.serial_no_series:
110 msg += f". If you want auto pick serial bundle, then kindly set Serial No Series in Item {self.item_code}"
111
112 if (
113 self.item_details.has_batch_no
114 and not self.item_details.batch_number_series
115 and not frappe.db.get_single_value("Stock Settings", "naming_series_prefix")
116 ):
117 msg += f". If you want auto pick batch bundle, then kindly set Batch Number Series in Item {self.item_code}"
118
119 elif self.sle.actual_qty < 0:
120 if not frappe.db.get_single_value(
121 "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
122 ):
123 msg += ". If you want auto pick serial/batch bundle, then kindly enable 'Auto Create Serial and Batch Bundle' in Stock Settings."
124
125 if msg:
126 error_msg = (
127 f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}."
128 + msg
129 )
130 frappe.throw(_(error_msg))
131
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530132 def set_serial_and_batch_bundle(self, sn_doc):
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530133 self.sle.db_set(
134 {"serial_and_batch_bundle": sn_doc.name, "auto_created_serial_and_batch_bundle": 1}
135 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530136
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530137 if sn_doc.is_rejected:
138 frappe.db.set_value(
139 self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name
140 )
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
160 if (
161 self.sle.voucher_type == "Subcontracting Receipt" and self.sle.dependant_sle_voucher_detail_no
162 ):
163 child_doctype = "Subcontracting Receipt Supplied Item"
164
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530165 if self.sle.voucher_type == "Stock Entry":
166 child_doctype = "Stock Entry Detail"
167
Rohit Waghchaure26b39ac2023-04-06 01:36:18 +0530168 if self.sle.voucher_type == "Asset Capitalization":
169 child_doctype = "Asset Capitalization Stock Item"
170
171 if self.sle.voucher_type == "Asset Repair":
172 child_doctype = "Asset Repair Consumed Item"
173
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530174 return child_doctype
175
176 def is_rejected_entry(self):
177 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
178
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530179 def process_batch_no(self):
180 if (
181 not self.sle.is_cancelled
182 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530183 and self.item_details.has_batch_no == 1
184 and self.item_details.create_new_batch
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530185 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530186 self.make_serial_batch_no_bundle()
187 elif not self.sle.is_cancelled:
188 self.validate_item_and_warehouse()
189
190 def validate_item_and_warehouse(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530191 if self.sle.serial_and_batch_bundle and not frappe.db.exists(
192 "Serial and Batch Bundle",
193 {
194 "name": self.sle.serial_and_batch_bundle,
195 "item_code": self.item_code,
196 "warehouse": self.warehouse,
197 "voucher_no": self.sle.voucher_no,
198 },
199 ):
200 msg = f"""
201 The Serial and Batch Bundle
202 {bold(self.sle.serial_and_batch_bundle)}
203 does not belong to Item {bold(self.item_code)}
204 or Warehouse {bold(self.warehouse)}
205 or {self.sle.voucher_type} no {bold(self.sle.voucher_no)}
206 """
207
208 frappe.throw(_(msg))
209
210 def delink_serial_and_batch_bundle(self):
211 update_values = {
212 "serial_and_batch_bundle": "",
213 }
214
215 if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse):
216 update_values["rejected_serial_and_batch_bundle"] = ""
217
218 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values)
219
220 frappe.db.set_value(
221 "Serial and Batch Bundle",
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530222 {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
rohitwaghchauref09e2132024-01-04 14:58:02 +0530223 {"is_cancelled": 1},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530224 )
225
Rohit Waghchauref79f2a32023-04-04 11:50:38 +0530226 if self.sle.serial_and_batch_bundle:
227 frappe.get_cached_doc(
228 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle
229 ).validate_serial_and_batch_inventory()
230
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530231 def post_process(self):
rohitwaghchaure07432892023-12-17 12:42:07 +0530232 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 +0530233 return
234
rohitwaghchaure07432892023-12-17 12:42:07 +0530235 if self.sle.serial_and_batch_bundle:
236 docstatus = frappe.get_cached_value(
237 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
238 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530239
rohitwaghchaure07432892023-12-17 12:42:07 +0530240 if docstatus != 1:
241 self.submit_serial_and_batch_bundle()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530242
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530243 if self.item_details.has_serial_no == 1:
244 self.set_warehouse_and_status_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530245
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530246 if (
247 self.sle.actual_qty > 0
248 and self.item_details.has_serial_no == 1
249 and self.item_details.has_batch_no == 1
250 ):
251 self.set_batch_no_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530252
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530253 if self.item_details.has_batch_no == 1:
254 self.update_batch_qty()
255
rohitwaghchaure6e5484e2024-01-02 12:54:18 +0530256 if self.sle.is_cancelled and self.sle.serial_and_batch_bundle:
257 self.cancel_serial_and_batch_bundle()
258
259 def cancel_serial_and_batch_bundle(self):
260 frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel()
261
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530262 def submit_serial_and_batch_bundle(self):
263 doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530264 self.validate_actual_qty(doc)
265
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530266 doc.flags.ignore_voucher_validation = True
267 doc.submit()
268
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530269 def set_warehouse_and_status_in_serial_nos(self):
rohitwaghchaure07432892023-12-17 12:42:07 +0530270 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
271
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530272 serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
rohitwaghchaure07432892023-12-17 12:42:07 +0530273 if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
274 serial_nos = get_parsed_serial_nos(self.sle.serial_no)
275
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530276 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
277
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530278 if not serial_nos:
279 return
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530280
rohitwaghchaure592fc812023-11-28 18:28:48 +0530281 status = "Inactive"
282 if self.sle.actual_qty < 0:
283 status = "Delivered"
284
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530285 sn_table = frappe.qb.DocType("Serial No")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530286 (
287 frappe.qb.update(sn_table)
288 .set(sn_table.warehouse, warehouse)
rohitwaghchaure07432892023-12-17 12:42:07 +0530289 .set(
290 sn_table.status,
291 "Active"
292 if warehouse
293 else status
294 if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
295 else "Inactive",
296 )
s-aga-r7a04f0f2024-02-05 12:35:26 +0530297 .set(sn_table.company, self.sle.company)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530298 .where(sn_table.name.isin(serial_nos))
299 ).run()
300
301 def set_batch_no_in_serial_nos(self):
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530302 entries = frappe.get_all(
303 "Serial and Batch Entry",
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530304 fields=["serial_no", "batch_no"],
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530305 filters={"parent": self.sle.serial_and_batch_bundle},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530306 )
307
308 batch_serial_nos = {}
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530309 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530310 batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no)
311
312 for batch_no, serial_nos in batch_serial_nos.items():
313 sn_table = frappe.qb.DocType("Serial No")
314 (
315 frappe.qb.update(sn_table)
316 .set(sn_table.batch_no, batch_no)
317 .where(sn_table.name.isin(serial_nos))
318 ).run()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530319
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530320 def update_batch_qty(self):
321 from erpnext.stock.doctype.batch.batch import get_available_batches
322
323 batches = get_batch_nos(self.sle.serial_and_batch_bundle)
rohitwaghchaure07432892023-12-17 12:42:07 +0530324 if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
325 batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530326
327 batches_qty = get_available_batches(
rohitwaghchauree178ffc2024-03-06 12:50:44 +0530328 frappe._dict({"item_code": self.item_code, "batch_no": list(batches.keys())})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530329 )
330
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530331 for batch_no in batches:
332 frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0))
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530333
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530334
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530335def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530336 if not serial_and_batch_bundle:
337 return []
338
339 filters = {"parent": serial_and_batch_bundle, "serial_no": ("is", "set")}
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530340 if isinstance(serial_and_batch_bundle, list):
341 filters = {"parent": ("in", serial_and_batch_bundle)}
342
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530343 if serial_nos:
344 filters["serial_no"] = ("in", serial_nos)
345
rohitwaghchaure07432892023-12-17 12:42:07 +0530346 entries = frappe.get_all(
347 "Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx"
348 )
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530349 if not entries:
350 return []
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530351
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530352 return [d.serial_no for d in entries if d.serial_no]
353
354
rohitwaghchaure07432892023-12-17 12:42:07 +0530355def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
356 if not serial_and_batch_bundle:
357 return []
358
359 filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")}
360 if isinstance(serial_and_batch_bundle, list):
361 filters = {"parent": ("in", serial_and_batch_bundle)}
362
363 if batches:
364 filters["batch_no"] = ("in", batches)
365
366 entries = frappe.get_all(
367 "Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1
368 )
369 if not entries:
370 return frappe._dict({})
371
372 return frappe._dict(entries)
373
374
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530375def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
376 return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530377
378
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530379def get_serial_or_batch_nos(bundle):
Rohit Waghchaure9cf645e2023-06-26 16:00:53 +0530380 # For print format
381
382 bundle_data = frappe.get_cached_value(
383 "Serial and Batch Bundle", bundle, ["has_serial_no", "has_batch_no"], as_dict=True
384 )
385
386 fields = []
387 if bundle_data.has_serial_no:
388 fields.append("serial_no")
389
390 if bundle_data.has_batch_no:
391 fields.extend(["batch_no", "qty"])
392
393 data = frappe.get_all("Serial and Batch Entry", fields=fields, filters={"parent": bundle})
394
395 if bundle_data.has_serial_no and not bundle_data.has_batch_no:
396 return ", ".join([d.serial_no for d in data])
397
398 elif bundle_data.has_batch_no:
399 html = "<table class= 'table table-borderless' style='margin-top: 0px;margin-bottom: 0px;'>"
400 for d in data:
401 if d.serial_no:
402 html += f"<tr><td>{d.batch_no}</th><th>{d.serial_no}</th ><th>{abs(d.qty)}</th></tr>"
403 else:
404 html += f"<tr><td>{d.batch_no}</td><td>{abs(d.qty)}</td></tr>"
405
406 html += "</table>"
407
408 return html
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530409
410
Rohit Waghchaure46704642023-03-23 11:41:20 +0530411class SerialNoValuation(DeprecatedSerialNoValuation):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530412 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530413 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530414 setattr(self, key, value)
415
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530416 self.calculate_stock_value_change()
417 self.calculate_valuation_rate()
418
419 def calculate_stock_value_change(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530420 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530421 self.stock_value_change = frappe.get_cached_value(
422 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
423 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530424
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530425 else:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530426 self.serial_no_incoming_rate = defaultdict(float)
427 self.stock_value_change = 0.0
428
rohitwaghchaure63792382024-03-04 12:04:41 +0530429 serial_nos = self.get_serial_nos()
430 for serial_no in serial_nos:
431 incoming_rate = self.get_incoming_rate_from_bundle(serial_no)
432 if not incoming_rate:
433 continue
434
435 self.stock_value_change += incoming_rate
436 self.serial_no_incoming_rate[serial_no] += incoming_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530437
438 self.calculate_stock_value_from_deprecarated_ledgers()
439
rohitwaghchaure63792382024-03-04 12:04:41 +0530440 def get_incoming_rate_from_bundle(self, serial_no) -> float:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530441 bundle = frappe.qb.DocType("Serial and Batch Bundle")
442 bundle_child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530443
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530444 query = (
445 frappe.qb.from_(bundle)
446 .inner_join(bundle_child)
447 .on(bundle.name == bundle_child.parent)
rohitwaghchaure63792382024-03-04 12:04:41 +0530448 .select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"))
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530449 .where(
450 (bundle.is_cancelled == 0)
451 & (bundle.docstatus == 1)
rohitwaghchaure63792382024-03-04 12:04:41 +0530452 & (bundle_child.serial_no == serial_no)
453 & (bundle.type_of_transaction == "Inward")
454 & (bundle_child.qty > 0)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530455 & (bundle.item_code == self.sle.item_code)
456 & (bundle_child.warehouse == self.sle.warehouse)
457 )
rohitwaghchaure63792382024-03-04 12:04:41 +0530458 .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc)
459 .limit(1)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530460 )
461
rohitwaghchaure5e9016f2023-12-01 21:42:22 +0530462 # Important to exclude the current voucher to calculate correct the stock value difference
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530463 if self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530464 query = query.where(bundle.voucher_no != self.sle.voucher_no)
465
466 if self.sle.posting_date:
467 if self.sle.posting_time is None:
468 self.sle.posting_time = nowtime()
469
470 timestamp_condition = CombineDatetime(
471 bundle.posting_date, bundle.posting_time
472 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
473
474 query = query.where(timestamp_condition)
475
rohitwaghchaure63792382024-03-04 12:04:41 +0530476 incoming_rate = query.run()
477 return flt(incoming_rate[0][0]) if incoming_rate else 0.0
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530478
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530479 def get_serial_nos(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530480 if self.sle.get("serial_nos"):
481 return self.sle.serial_nos
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530482
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530483 return get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530484
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530485 def calculate_valuation_rate(self):
486 if not hasattr(self, "wh_data"):
487 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530488
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530489 new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530490
491 if new_stock_qty > 0:
492 new_stock_value = (
493 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530494 ) + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530495 if new_stock_value >= 0:
496 # calculate new valuation rate only if stock value is positive
497 # else it remains the same as that of previous entry
498 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
499
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530500 if (
501 not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry()
502 ):
503 allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate(
504 self.sle.voucher_type, self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530505 )
506 if not allow_zero_rate:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530507 self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530508
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530509 self.wh_data.qty_after_transaction += self.sle.actual_qty
510 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
511 self.wh_data.valuation_rate
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530512 )
513
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530514 def is_rejected_entry(self):
515 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530516
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530517 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530518 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530519
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530520 def get_incoming_rate_of_serial_no(self, serial_no):
521 return self.serial_no_incoming_rate.get(serial_no, 0.0)
522
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530523
524def is_rejected(voucher_type, voucher_detail_no, warehouse):
525 if voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
526 return warehouse == frappe.get_cached_value(
527 voucher_type + " Item", voucher_detail_no, "rejected_warehouse"
528 )
529
530 return False
531
532
Rohit Waghchaure46704642023-03-23 11:41:20 +0530533class BatchNoValuation(DeprecatedBatchNoValuation):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530534 def __init__(self, **kwargs):
535 for key, value in kwargs.items():
536 setattr(self, key, value)
537
538 self.batch_nos = self.get_batch_nos()
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530539 self.prepare_batches()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530540 self.calculate_avg_rate()
541 self.calculate_valuation_rate()
542
543 def calculate_avg_rate(self):
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530544 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530545 self.stock_value_change = frappe.get_cached_value(
546 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530547 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530548 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530549 entries = self.get_batch_no_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530550 self.stock_value_change = 0.0
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530551 self.batch_avg_rate = defaultdict(float)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530552 self.available_qty = defaultdict(float)
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530553 self.stock_value_differece = defaultdict(float)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530554
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530555 for ledger in entries:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530556 self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530557 self.available_qty[ledger.batch_no] += flt(ledger.qty)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530558
559 self.calculate_avg_rate_from_deprecarated_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530560 self.calculate_avg_rate_for_non_batchwise_valuation()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530561 self.set_stock_value_difference()
562
563 def get_batch_no_ledgers(self) -> List[dict]:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530564 if not self.batchwise_valuation_batches:
565 return []
566
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530567 parent = frappe.qb.DocType("Serial and Batch Bundle")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530568 child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530569
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530570 timestamp_condition = ""
571 if self.sle.posting_date and self.sle.posting_time:
572 timestamp_condition = CombineDatetime(
573 parent.posting_date, parent.posting_time
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530574 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530575
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530576 query = (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530577 frappe.qb.from_(parent)
578 .inner_join(child)
579 .on(parent.name == child.parent)
580 .select(
581 child.batch_no,
582 Sum(child.stock_value_difference).as_("incoming_rate"),
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530583 Sum(child.qty).as_("qty"),
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530584 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530585 .where(
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530586 (child.batch_no.isin(self.batchwise_valuation_batches))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530587 & (parent.warehouse == self.sle.warehouse)
588 & (parent.item_code == self.sle.item_code)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530589 & (parent.docstatus == 1)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530590 & (parent.is_cancelled == 0)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530591 & (parent.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530592 )
593 .groupby(child.batch_no)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530594 )
595
rohitwaghchaure5e9016f2023-12-01 21:42:22 +0530596 # Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
597 if self.sle.voucher_detail_no:
598 query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no)
599 elif self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530600 query = query.where(parent.voucher_no != self.sle.voucher_no)
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530601
rohitwaghchaurea4198122024-03-12 14:18:39 +0530602 query = query.where(parent.voucher_type != "Pick List")
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530603 if timestamp_condition:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530604 query = query.where(timestamp_condition)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530605
606 return query.run(as_dict=True)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530607
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530608 def prepare_batches(self):
609 self.batches = self.batch_nos
610 if isinstance(self.batch_nos, dict):
611 self.batches = list(self.batch_nos.keys())
612
613 self.batchwise_valuation_batches = []
614 self.non_batchwise_valuation_batches = []
615
616 batches = frappe.get_all(
617 "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
618 )
619
620 for batch in batches:
621 self.batchwise_valuation_batches.append(batch.name)
622
623 self.non_batchwise_valuation_batches = list(
624 set(self.batches) - set(self.batchwise_valuation_batches)
625 )
626
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530627 def get_batch_nos(self) -> list:
628 if self.sle.get("batch_nos"):
629 return self.sle.batch_nos
630
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530631 return get_batch_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530632
633 def set_stock_value_difference(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530634 for batch_no, ledger in self.batch_nos.items():
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530635 if batch_no in self.non_batchwise_valuation_batches:
636 continue
637
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530638 if not self.available_qty[batch_no]:
639 continue
640
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530641 self.batch_avg_rate[batch_no] = (
642 self.stock_value_differece[batch_no] / self.available_qty[batch_no]
643 )
644
645 # New Stock Value Difference
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530646 stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530647 self.stock_value_change += stock_value_change
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530648
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530649 frappe.db.set_value(
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530650 "Serial and Batch Entry",
651 ledger.name,
652 {
653 "stock_value_difference": stock_value_change,
654 "incoming_rate": self.batch_avg_rate[batch_no],
655 },
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530656 )
657
658 def calculate_valuation_rate(self):
659 if not hasattr(self, "wh_data"):
660 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530661
662 self.wh_data.stock_value = round_off_if_near_zero(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530663 self.wh_data.stock_value + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530664 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530665
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530666 self.wh_data.qty_after_transaction += self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530667 if self.wh_data.qty_after_transaction:
668 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
669
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530670 def get_incoming_rate(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530671 if not self.sle.actual_qty:
672 self.sle.actual_qty = self.get_actual_qty()
673
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530674 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530675
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530676 def get_actual_qty(self):
677 total_qty = 0.0
678 for batch_no in self.available_qty:
679 total_qty += self.available_qty[batch_no]
680
681 return total_qty
682
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530683
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530684def get_batch_nos(serial_and_batch_bundle):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530685 if not serial_and_batch_bundle:
686 return frappe._dict({})
687
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530688 entries = frappe.get_all(
689 "Serial and Batch Entry",
690 fields=["batch_no", "qty", "name"],
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530691 filters={"parent": serial_and_batch_bundle, "batch_no": ("is", "set")},
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530692 order_by="idx",
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530693 )
694
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530695 if not entries:
696 return frappe._dict({})
697
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530698 return {d.batch_no: d for d in entries}
699
700
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530701def get_empty_batches_based_work_order(work_order, item_code):
Rohit Waghchaure46704642023-03-23 11:41:20 +0530702 batches = get_batches_from_work_order(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530703 if not batches:
704 return batches
705
Rohit Waghchaure46704642023-03-23 11:41:20 +0530706 entries = get_batches_from_stock_entries(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530707 if not entries:
708 return batches
709
710 ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
711 if ids:
712 set_batch_details_from_package(ids, batches)
713
714 # Will be deprecated in v16
715 for d in entries:
716 if not d.batch_no:
717 continue
718
719 batches[d.batch_no] -= d.qty
720
721 return batches
722
723
Rohit Waghchaure46704642023-03-23 11:41:20 +0530724def get_batches_from_work_order(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530725 return frappe._dict(
726 frappe.get_all(
Rohit Waghchaure46704642023-03-23 11:41:20 +0530727 "Batch",
728 fields=["name", "qty_to_produce"],
729 filters={"reference_name": work_order, "item": item_code},
730 as_list=1,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530731 )
732 )
733
734
Rohit Waghchaure46704642023-03-23 11:41:20 +0530735def get_batches_from_stock_entries(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530736 entries = frappe.get_all(
737 "Stock Entry",
738 filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
739 fields=["name"],
740 )
741
742 return frappe.get_all(
743 "Stock Entry Detail",
744 fields=["batch_no", "qty", "serial_and_batch_bundle"],
745 filters={
746 "parent": ("in", [d.name for d in entries]),
747 "is_finished_item": 1,
Rohit Waghchaure46704642023-03-23 11:41:20 +0530748 "item_code": item_code,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530749 },
750 )
751
752
753def set_batch_details_from_package(ids, batches):
754 entries = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530755 "Serial and Batch Entry",
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530756 filters={"parent": ("in", ids), "is_outward": 0},
757 fields=["batch_no", "qty"],
758 )
759
760 for d in entries:
761 batches[d.batch_no] -= d.qty
Rohit Waghchaure46704642023-03-23 11:41:20 +0530762
763
764class SerialBatchCreation:
765 def __init__(self, args):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530766 self.set(args)
767 self.set_item_details()
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530768 self.set_other_details()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530769
770 def set(self, args):
771 self.__dict__ = {}
Rohit Waghchaure46704642023-03-23 11:41:20 +0530772 for key, value in args.items():
773 setattr(self, key, value)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530774 self.__dict__[key] = value
775
776 def get(self, key):
777 return self.__dict__.get(key)
778
779 def set_item_details(self):
780 fields = [
781 "has_batch_no",
782 "has_serial_no",
783 "item_name",
784 "item_group",
785 "serial_no_series",
786 "create_new_batch",
787 "batch_number_series",
788 "description",
789 ]
790
791 item_details = frappe.get_cached_value("Item", self.item_code, fields, as_dict=1)
792 for key, value in item_details.items():
793 setattr(self, key, value)
794
795 self.__dict__.update(item_details)
Rohit Waghchaure46704642023-03-23 11:41:20 +0530796
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530797 def set_other_details(self):
798 if not self.get("posting_date"):
799 setattr(self, "posting_date", today())
800 self.__dict__["posting_date"] = self.posting_date
801
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530802 if not self.get("actual_qty"):
803 qty = self.get("qty") or self.get("total_qty")
804
805 setattr(self, "actual_qty", qty)
806 self.__dict__["actual_qty"] = self.actual_qty
807
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530808 if not hasattr(self, "use_serial_batch_fields"):
809 setattr(self, "use_serial_batch_fields", 0)
810
Rohit Waghchaure46704642023-03-23 11:41:20 +0530811 def duplicate_package(self):
812 if not self.serial_and_batch_bundle:
813 return
814
815 id = self.serial_and_batch_bundle
816 package = frappe.get_doc("Serial and Batch Bundle", id)
817 new_package = frappe.copy_doc(package)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530818
819 if self.get("returned_serial_nos"):
820 self.remove_returned_serial_nos(new_package)
821
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530822 new_package.docstatus = 0
Rohit Waghchaure46704642023-03-23 11:41:20 +0530823 new_package.type_of_transaction = self.type_of_transaction
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530824 new_package.returned_against = self.get("returned_against")
Rohit Waghchaure46704642023-03-23 11:41:20 +0530825 new_package.save()
826
827 self.serial_and_batch_bundle = new_package.name
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530828
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530829 def remove_returned_serial_nos(self, package):
830 remove_list = []
831 for d in package.entries:
832 if d.serial_no in self.returned_serial_nos:
833 remove_list.append(d)
834
835 for d in remove_list:
836 package.remove(d)
837
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530838 def make_serial_and_batch_bundle(self):
839 doc = frappe.new_doc("Serial and Batch Bundle")
840 valid_columns = doc.meta.get_valid_columns()
841 for key, value in self.__dict__.items():
842 if key in valid_columns:
843 doc.set(key, value)
844
845 if self.type_of_transaction == "Outward":
846 self.set_auto_serial_batch_entries_for_outward()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530847 elif self.type_of_transaction == "Inward":
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530848 self.set_auto_serial_batch_entries_for_inward()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530849 self.add_serial_nos_for_batch_item()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530850
851 self.set_serial_batch_entries(doc)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530852 if not doc.get("entries"):
853 return frappe._dict({})
854
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530855 doc.save()
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530856 self.validate_qty(doc)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530857
858 if not hasattr(self, "do_not_submit") or not self.do_not_submit:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530859 doc.flags.ignore_voucher_validation = True
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530860 doc.submit()
861
862 return doc
863
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530864 def add_serial_nos_for_batch_item(self):
865 if not (self.has_serial_no and self.has_batch_no):
866 return
867
868 if not self.get("serial_nos") and self.get("batches"):
869 batches = list(self.get("batches").keys())
870 if len(batches) == 1:
871 self.batch_no = batches[0]
872 self.serial_nos = self.get_auto_created_serial_nos()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530873
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530874 def update_serial_and_batch_entries(self):
875 doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
876 doc.type_of_transaction = self.type_of_transaction
877 doc.set("entries", [])
878 self.set_auto_serial_batch_entries_for_outward()
879 self.set_serial_batch_entries(doc)
880 if not doc.get("entries"):
881 return frappe._dict({})
882
883 doc.save()
884 return doc
885
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530886 def validate_qty(self, doc):
887 if doc.type_of_transaction == "Outward":
888 precision = doc.precision("total_qty")
889
890 total_qty = abs(flt(doc.total_qty, precision))
891 required_qty = abs(flt(self.actual_qty, precision))
892
893 if required_qty - total_qty > 0:
894 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."
895 frappe.throw(msg, title=_("Insufficient Stock"))
896
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530897 def set_auto_serial_batch_entries_for_outward(self):
898 from erpnext.stock.doctype.batch.batch import get_available_batches
899 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
900
901 kwargs = frappe._dict(
902 {
903 "item_code": self.item_code,
904 "warehouse": self.warehouse,
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530905 "qty": abs(self.actual_qty) if self.actual_qty else 0,
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530906 "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
907 }
908 )
909
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530910 if self.get("ignore_serial_nos"):
911 kwargs["ignore_serial_nos"] = self.ignore_serial_nos
912
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530913 if self.has_serial_no and not self.get("serial_nos"):
914 self.serial_nos = get_serial_nos_for_outward(kwargs)
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530915 elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530916 self.batches = get_available_batches(kwargs)
917
918 def set_auto_serial_batch_entries_for_inward(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530919 if (self.get("batches") and self.has_batch_no) or (
920 self.get("serial_nos") and self.has_serial_no
921 ):
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530922 if self.use_serial_batch_fields and self.get("serial_nos"):
923 self.make_serial_no_if_not_exists()
924
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530925 return
926
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530927 self.batch_no = None
928 if self.has_batch_no:
929 self.batch_no = self.create_batch()
930
931 if self.has_serial_no:
932 self.serial_nos = self.get_auto_created_serial_nos()
933 else:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530934 self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530935
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530936 def make_serial_no_if_not_exists(self):
937 non_exists_serial_nos = []
938 for row in self.serial_nos:
939 if not frappe.db.exists("Serial No", row):
940 non_exists_serial_nos.append(row)
941
942 if non_exists_serial_nos:
943 self.make_serial_nos(non_exists_serial_nos)
944
945 def make_serial_nos(self, serial_nos):
946 serial_nos_details = []
947 batch_no = None
948 if self.batches:
949 batch_no = list(self.batches.keys())[0]
950
951 for serial_no in serial_nos:
952 serial_nos_details.append(
953 (
954 serial_no,
955 serial_no,
956 now(),
957 now(),
958 frappe.session.user,
959 frappe.session.user,
960 self.warehouse,
961 self.company,
962 self.item_code,
963 self.item_name,
964 self.description,
965 "Active",
966 batch_no,
967 )
968 )
969
970 if serial_nos_details:
971 fields = [
972 "name",
973 "serial_no",
974 "creation",
975 "modified",
976 "owner",
977 "modified_by",
978 "warehouse",
979 "company",
980 "item_code",
981 "item_name",
982 "description",
983 "status",
984 "batch_no",
985 ]
986
987 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
988
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530989 def set_serial_batch_entries(self, doc):
990 if self.get("serial_nos"):
991 serial_no_wise_batch = frappe._dict({})
992 if self.has_batch_no:
s-aga-r2d8363a2023-09-02 11:02:24 +0530993 serial_no_wise_batch = get_serial_nos_batch(self.serial_nos)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530994
995 qty = -1 if self.type_of_transaction == "Outward" else 1
996 for serial_no in self.serial_nos:
997 doc.append(
998 "entries",
999 {
1000 "serial_no": serial_no,
1001 "qty": qty,
1002 "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
1003 "incoming_rate": self.get("incoming_rate"),
1004 },
1005 )
1006
Rohit Waghchauree88c5d62023-04-05 20:03:44 +05301007 elif self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301008 for batch_no, batch_qty in self.batches.items():
1009 doc.append(
1010 "entries",
1011 {
1012 "batch_no": batch_no,
1013 "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
1014 "incoming_rate": self.get("incoming_rate"),
1015 },
1016 )
1017
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301018 def create_batch(self):
1019 from erpnext.stock.doctype.batch.batch import make_batch
1020
1021 return make_batch(
1022 frappe._dict(
1023 {
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301024 "item": self.get("item_code"),
1025 "reference_doctype": self.get("voucher_type"),
1026 "reference_name": self.get("voucher_no"),
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301027 }
1028 )
1029 )
1030
1031 def get_auto_created_serial_nos(self):
1032 sr_nos = []
1033 serial_nos_details = []
1034
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301035 if not self.serial_no_series:
1036 msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually."
1037 frappe.throw(_(msg))
1038
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301039 for i in range(abs(cint(self.actual_qty))):
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301040 serial_no = make_autoname(self.serial_no_series, "Serial No")
1041 sr_nos.append(serial_no)
1042 serial_nos_details.append(
1043 (
1044 serial_no,
1045 serial_no,
1046 now(),
1047 now(),
1048 frappe.session.user,
1049 frappe.session.user,
1050 self.warehouse,
1051 self.company,
1052 self.item_code,
1053 self.item_name,
1054 self.description,
1055 "Active",
1056 self.batch_no,
1057 )
1058 )
1059
1060 if serial_nos_details:
1061 fields = [
1062 "name",
1063 "serial_no",
1064 "creation",
1065 "modified",
1066 "owner",
1067 "modified_by",
1068 "warehouse",
1069 "company",
1070 "item_code",
1071 "item_name",
1072 "description",
1073 "status",
1074 "batch_no",
1075 ]
1076
1077 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
1078
1079 return sr_nos
1080
1081
1082def get_serial_or_batch_items(items):
1083 serial_or_batch_items = frappe.get_all(
1084 "Item",
1085 filters={"name": ("in", [d.item_code for d in items])},
1086 or_filters={"has_serial_no": 1, "has_batch_no": 1},
1087 )
1088
1089 if not serial_or_batch_items:
1090 return
1091 else:
1092 serial_or_batch_items = [d.name for d in serial_or_batch_items]
1093
1094 return serial_or_batch_items
s-aga-r2d8363a2023-09-02 11:02:24 +05301095
1096
1097def get_serial_nos_batch(serial_nos):
1098 return frappe._dict(
1099 frappe.get_all(
1100 "Serial No",
1101 fields=["name", "batch_no"],
1102 filters={"name": ("in", serial_nos)},
1103 as_list=1,
1104 )
1105 )