blob: fc1cca46f6644590adcf7b95a96620bda3756e77 [file] [log] [blame]
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05301from collections import defaultdict
2from typing import List
Rohit Waghchauref1b59662023-03-06 12:08:28 +05303
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05304import frappe
5from frappe import _, bold
6from frappe.model.naming import make_autoname
Rohit Waghchaure86da3062023-03-20 14:15:34 +05307from frappe.query_builder.functions import CombineDatetime, Sum
rohitwaghchaure4b24fcd2024-02-20 23:45:07 +05308from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05309
10from erpnext.stock.deprecated_serial_batch import (
11 DeprecatedBatchNoValuation,
12 DeprecatedSerialNoValuation,
13)
Rohit Waghchauref1b59662023-03-06 12:08:28 +053014from erpnext.stock.valuation import round_off_if_near_zero
15
16
17class SerialBatchBundle:
18 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053019 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +053020 setattr(self, key, value)
21
22 self.set_item_details()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053023 self.process_serial_and_batch_bundle()
24 if self.sle.is_cancelled:
25 self.delink_serial_and_batch_bundle()
26
27 self.post_process()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053028
29 def process_serial_and_batch_bundle(self):
30 if self.item_details.has_serial_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053031 self.process_serial_no()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053032 elif self.item_details.has_batch_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053033 self.process_batch_no()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053034
35 def set_item_details(self):
36 fields = [
37 "has_batch_no",
38 "has_serial_no",
39 "item_name",
40 "item_group",
41 "serial_no_series",
42 "create_new_batch",
43 "batch_number_series",
44 ]
45
46 self.item_details = frappe.get_cached_value("Item", self.sle.item_code, fields, as_dict=1)
47
48 def process_serial_no(self):
49 if (
50 not self.sle.is_cancelled
51 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +053052 and self.item_details.has_serial_no == 1
Rohit Waghchauref1b59662023-03-06 12:08:28 +053053 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053054 self.make_serial_batch_no_bundle()
55 elif not self.sle.is_cancelled:
56 self.validate_item_and_warehouse()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053057
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053058 def make_serial_batch_no_bundle(self):
Rohit Waghchaure648efca2023-03-28 12:16:27 +053059 self.validate_item()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053060
Rohit Waghchaure648efca2023-03-28 12:16:27 +053061 sn_doc = SerialBatchCreation(
62 {
63 "item_code": self.item_code,
64 "warehouse": self.warehouse,
65 "posting_date": self.sle.posting_date,
66 "posting_time": self.sle.posting_time,
67 "voucher_type": self.sle.voucher_type,
68 "voucher_no": self.sle.voucher_no,
69 "voucher_detail_no": self.sle.voucher_detail_no,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +053070 "qty": self.sle.actual_qty,
Rohit Waghchaure648efca2023-03-28 12:16:27 +053071 "avg_rate": self.sle.incoming_rate,
72 "total_amount": flt(self.sle.actual_qty) * flt(self.sle.incoming_rate),
73 "type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward",
74 "company": self.company,
75 "is_rejected": self.is_rejected_entry(),
76 }
77 ).make_serial_and_batch_bundle()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053078
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053079 self.set_serial_and_batch_bundle(sn_doc)
Rohit Waghchauref1b59662023-03-06 12:08:28 +053080
Rohit Waghchaure42b22942023-05-27 19:18:03 +053081 def validate_actual_qty(self, sn_doc):
Rohit Waghchauref968f0f2023-06-14 23:22:22 +053082 link = get_link_to_form("Serial and Batch Bundle", sn_doc.name)
83
84 condition = {
85 "Inward": self.sle.actual_qty > 0,
86 "Outward": self.sle.actual_qty < 0,
87 }.get(sn_doc.type_of_transaction)
88
89 if not condition:
90 correct_type = "Inward"
91 if sn_doc.type_of_transaction == "Inward":
92 correct_type = "Outward"
93
94 msg = f"The type of transaction of Serial and Batch Bundle {link} is {bold(sn_doc.type_of_transaction)} but as per the Actual Qty {self.sle.actual_qty} for the item {bold(self.sle.item_code)} in the {self.sle.voucher_type} {self.sle.voucher_no} the type of transaction should be {bold(correct_type)}"
95 frappe.throw(_(msg), title=_("Incorrect Type of Transaction"))
96
Rohit Waghchaure42b22942023-05-27 19:18:03 +053097 precision = sn_doc.precision("total_qty")
98 if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision):
Rohit Waghchauref968f0f2023-06-14 23:22:22 +053099 msg = f"Total qty {flt(sn_doc.total_qty, precision)} of Serial and Batch Bundle {link} is not equal to Actual Qty {flt(self.sle.actual_qty, precision)} in the {self.sle.voucher_type} {self.sle.voucher_no}"
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530100 frappe.throw(_(msg))
101
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530102 def validate_item(self):
103 msg = ""
104 if self.sle.actual_qty > 0:
105 if not self.item_details.has_batch_no and not self.item_details.has_serial_no:
106 msg = f"Item {self.item_code} is not a batch or serial no item"
107
108 if self.item_details.has_serial_no and not self.item_details.serial_no_series:
109 msg += f". If you want auto pick serial bundle, then kindly set Serial No Series in Item {self.item_code}"
110
111 if (
112 self.item_details.has_batch_no
113 and not self.item_details.batch_number_series
114 and not frappe.db.get_single_value("Stock Settings", "naming_series_prefix")
115 ):
116 msg += f". If you want auto pick batch bundle, then kindly set Batch Number Series in Item {self.item_code}"
117
118 elif self.sle.actual_qty < 0:
119 if not frappe.db.get_single_value(
120 "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
121 ):
122 msg += ". If you want auto pick serial/batch bundle, then kindly enable 'Auto Create Serial and Batch Bundle' in Stock Settings."
123
124 if msg:
125 error_msg = (
126 f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}."
127 + msg
128 )
129 frappe.throw(_(error_msg))
130
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530131 def set_serial_and_batch_bundle(self, sn_doc):
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530132 self.sle.db_set(
133 {"serial_and_batch_bundle": sn_doc.name, "auto_created_serial_and_batch_bundle": 1}
134 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530135
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530136 if sn_doc.is_rejected:
137 frappe.db.set_value(
138 self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name
139 )
140 else:
rohitwaghchaure4b24fcd2024-02-20 23:45:07 +0530141 values_to_update = {
142 "serial_and_batch_bundle": sn_doc.name,
143 }
144
145 if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
146 if sn_doc.has_serial_no:
147 values_to_update["serial_no"] = "\n".join(cstr(d.serial_no) for d in sn_doc.entries)
148 elif sn_doc.has_batch_no and len(sn_doc.entries) == 1:
149 values_to_update["batch_no"] = sn_doc.entries[0].batch_no
150
151 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530152
153 @property
154 def child_doctype(self):
155 child_doctype = self.sle.voucher_type + " Item"
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530156
157 if (
158 self.sle.voucher_type == "Subcontracting Receipt" and self.sle.dependant_sle_voucher_detail_no
159 ):
160 child_doctype = "Subcontracting Receipt Supplied Item"
161
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530162 if self.sle.voucher_type == "Stock Entry":
163 child_doctype = "Stock Entry Detail"
164
Rohit Waghchaure26b39ac2023-04-06 01:36:18 +0530165 if self.sle.voucher_type == "Asset Capitalization":
166 child_doctype = "Asset Capitalization Stock Item"
167
168 if self.sle.voucher_type == "Asset Repair":
169 child_doctype = "Asset Repair Consumed Item"
170
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530171 return child_doctype
172
173 def is_rejected_entry(self):
174 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
175
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530176 def process_batch_no(self):
177 if (
178 not self.sle.is_cancelled
179 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530180 and self.item_details.has_batch_no == 1
181 and self.item_details.create_new_batch
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530182 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530183 self.make_serial_batch_no_bundle()
184 elif not self.sle.is_cancelled:
185 self.validate_item_and_warehouse()
186
187 def validate_item_and_warehouse(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530188 if self.sle.serial_and_batch_bundle and not frappe.db.exists(
189 "Serial and Batch Bundle",
190 {
191 "name": self.sle.serial_and_batch_bundle,
192 "item_code": self.item_code,
193 "warehouse": self.warehouse,
194 "voucher_no": self.sle.voucher_no,
195 },
196 ):
197 msg = f"""
198 The Serial and Batch Bundle
199 {bold(self.sle.serial_and_batch_bundle)}
200 does not belong to Item {bold(self.item_code)}
201 or Warehouse {bold(self.warehouse)}
202 or {self.sle.voucher_type} no {bold(self.sle.voucher_no)}
203 """
204
205 frappe.throw(_(msg))
206
207 def delink_serial_and_batch_bundle(self):
208 update_values = {
209 "serial_and_batch_bundle": "",
210 }
211
212 if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse):
213 update_values["rejected_serial_and_batch_bundle"] = ""
214
215 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values)
216
217 frappe.db.set_value(
218 "Serial and Batch Bundle",
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530219 {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
rohitwaghchauref09e2132024-01-04 14:58:02 +0530220 {"is_cancelled": 1},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530221 )
222
Rohit Waghchauref79f2a32023-04-04 11:50:38 +0530223 if self.sle.serial_and_batch_bundle:
224 frappe.get_cached_doc(
225 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle
226 ).validate_serial_and_batch_inventory()
227
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530228 def post_process(self):
rohitwaghchaure07432892023-12-17 12:42:07 +0530229 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 +0530230 return
231
rohitwaghchaure07432892023-12-17 12:42:07 +0530232 if self.sle.serial_and_batch_bundle:
233 docstatus = frappe.get_cached_value(
234 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
235 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530236
rohitwaghchaure07432892023-12-17 12:42:07 +0530237 if docstatus != 1:
238 self.submit_serial_and_batch_bundle()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530239
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530240 if self.item_details.has_serial_no == 1:
241 self.set_warehouse_and_status_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530242
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530243 if (
244 self.sle.actual_qty > 0
245 and self.item_details.has_serial_no == 1
246 and self.item_details.has_batch_no == 1
247 ):
248 self.set_batch_no_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530249
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530250 if self.item_details.has_batch_no == 1:
251 self.update_batch_qty()
252
rohitwaghchaure6e5484e2024-01-02 12:54:18 +0530253 if self.sle.is_cancelled and self.sle.serial_and_batch_bundle:
254 self.cancel_serial_and_batch_bundle()
255
256 def cancel_serial_and_batch_bundle(self):
257 frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel()
258
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530259 def submit_serial_and_batch_bundle(self):
260 doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530261 self.validate_actual_qty(doc)
262
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530263 doc.flags.ignore_voucher_validation = True
264 doc.submit()
265
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530266 def set_warehouse_and_status_in_serial_nos(self):
rohitwaghchaure07432892023-12-17 12:42:07 +0530267 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
268
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530269 serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
rohitwaghchaure07432892023-12-17 12:42:07 +0530270 if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
271 serial_nos = get_parsed_serial_nos(self.sle.serial_no)
272
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530273 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
274
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530275 if not serial_nos:
276 return
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530277
rohitwaghchaure592fc812023-11-28 18:28:48 +0530278 status = "Inactive"
279 if self.sle.actual_qty < 0:
280 status = "Delivered"
281
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530282 sn_table = frappe.qb.DocType("Serial No")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530283 (
284 frappe.qb.update(sn_table)
285 .set(sn_table.warehouse, warehouse)
rohitwaghchaure07432892023-12-17 12:42:07 +0530286 .set(
287 sn_table.status,
288 "Active"
289 if warehouse
290 else status
291 if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
292 else "Inactive",
293 )
s-aga-r7a04f0f2024-02-05 12:35:26 +0530294 .set(sn_table.company, self.sle.company)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530295 .where(sn_table.name.isin(serial_nos))
296 ).run()
297
298 def set_batch_no_in_serial_nos(self):
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530299 entries = frappe.get_all(
300 "Serial and Batch Entry",
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530301 fields=["serial_no", "batch_no"],
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530302 filters={"parent": self.sle.serial_and_batch_bundle},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530303 )
304
305 batch_serial_nos = {}
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530306 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530307 batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no)
308
309 for batch_no, serial_nos in batch_serial_nos.items():
310 sn_table = frappe.qb.DocType("Serial No")
311 (
312 frappe.qb.update(sn_table)
313 .set(sn_table.batch_no, batch_no)
314 .where(sn_table.name.isin(serial_nos))
315 ).run()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530316
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530317 def update_batch_qty(self):
318 from erpnext.stock.doctype.batch.batch import get_available_batches
319
320 batches = get_batch_nos(self.sle.serial_and_batch_bundle)
rohitwaghchaure07432892023-12-17 12:42:07 +0530321 if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
322 batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530323
324 batches_qty = get_available_batches(
325 frappe._dict(
326 {"item_code": self.item_code, "warehouse": self.warehouse, "batch_no": list(batches.keys())}
327 )
328 )
329
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530330 for batch_no in batches:
331 frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0))
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530332
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530333
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530334def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530335 if not serial_and_batch_bundle:
336 return []
337
338 filters = {"parent": serial_and_batch_bundle, "serial_no": ("is", "set")}
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530339 if isinstance(serial_and_batch_bundle, list):
340 filters = {"parent": ("in", serial_and_batch_bundle)}
341
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530342 if serial_nos:
343 filters["serial_no"] = ("in", serial_nos)
344
rohitwaghchaure07432892023-12-17 12:42:07 +0530345 entries = frappe.get_all(
346 "Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx"
347 )
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530348 if not entries:
349 return []
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530350
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530351 return [d.serial_no for d in entries if d.serial_no]
352
353
rohitwaghchaure07432892023-12-17 12:42:07 +0530354def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
355 if not serial_and_batch_bundle:
356 return []
357
358 filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")}
359 if isinstance(serial_and_batch_bundle, list):
360 filters = {"parent": ("in", serial_and_batch_bundle)}
361
362 if batches:
363 filters["batch_no"] = ("in", batches)
364
365 entries = frappe.get_all(
366 "Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1
367 )
368 if not entries:
369 return frappe._dict({})
370
371 return frappe._dict(entries)
372
373
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530374def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
375 return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530376
377
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530378def get_serial_or_batch_nos(bundle):
Rohit Waghchaure9cf645e2023-06-26 16:00:53 +0530379 # For print format
380
381 bundle_data = frappe.get_cached_value(
382 "Serial and Batch Bundle", bundle, ["has_serial_no", "has_batch_no"], as_dict=True
383 )
384
385 fields = []
386 if bundle_data.has_serial_no:
387 fields.append("serial_no")
388
389 if bundle_data.has_batch_no:
390 fields.extend(["batch_no", "qty"])
391
392 data = frappe.get_all("Serial and Batch Entry", fields=fields, filters={"parent": bundle})
393
394 if bundle_data.has_serial_no and not bundle_data.has_batch_no:
395 return ", ".join([d.serial_no for d in data])
396
397 elif bundle_data.has_batch_no:
398 html = "<table class= 'table table-borderless' style='margin-top: 0px;margin-bottom: 0px;'>"
399 for d in data:
400 if d.serial_no:
401 html += f"<tr><td>{d.batch_no}</th><th>{d.serial_no}</th ><th>{abs(d.qty)}</th></tr>"
402 else:
403 html += f"<tr><td>{d.batch_no}</td><td>{abs(d.qty)}</td></tr>"
404
405 html += "</table>"
406
407 return html
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530408
409
Rohit Waghchaure46704642023-03-23 11:41:20 +0530410class SerialNoValuation(DeprecatedSerialNoValuation):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530411 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530412 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530413 setattr(self, key, value)
414
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530415 self.calculate_stock_value_change()
416 self.calculate_valuation_rate()
417
418 def calculate_stock_value_change(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530419 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530420 self.stock_value_change = frappe.get_cached_value(
421 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
422 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530423
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530424 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530425 entries = self.get_serial_no_ledgers()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530426
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530427 self.serial_no_incoming_rate = defaultdict(float)
428 self.stock_value_change = 0.0
429
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530430 for ledger in entries:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530431 self.stock_value_change += ledger.incoming_rate
432 self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530433
434 self.calculate_stock_value_from_deprecarated_ledgers()
435
436 def get_serial_no_ledgers(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530437 serial_nos = self.get_serial_nos()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530438 bundle = frappe.qb.DocType("Serial and Batch Bundle")
439 bundle_child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530440
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530441 query = (
442 frappe.qb.from_(bundle)
443 .inner_join(bundle_child)
444 .on(bundle.name == bundle_child.parent)
445 .select(
446 bundle.name,
447 bundle_child.serial_no,
448 (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"),
449 )
450 .where(
451 (bundle.is_cancelled == 0)
452 & (bundle.docstatus == 1)
453 & (bundle_child.serial_no.isin(serial_nos))
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530454 & (bundle.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530455 & (bundle.item_code == self.sle.item_code)
456 & (bundle_child.warehouse == self.sle.warehouse)
457 )
458 .orderby(bundle.posting_date, bundle.posting_time, bundle.creation)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530459 )
460
rohitwaghchaure5e9016f2023-12-01 21:42:22 +0530461 # Important to exclude the current voucher to calculate correct the stock value difference
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530462 if self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530463 query = query.where(bundle.voucher_no != self.sle.voucher_no)
464
465 if self.sle.posting_date:
466 if self.sle.posting_time is None:
467 self.sle.posting_time = nowtime()
468
469 timestamp_condition = CombineDatetime(
470 bundle.posting_date, bundle.posting_time
471 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
472
473 query = query.where(timestamp_condition)
474
475 return query.run(as_dict=True)
476
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530477 def get_serial_nos(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530478 if self.sle.get("serial_nos"):
479 return self.sle.serial_nos
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530480
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530481 return get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530482
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530483 def calculate_valuation_rate(self):
484 if not hasattr(self, "wh_data"):
485 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530486
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530487 new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530488
489 if new_stock_qty > 0:
490 new_stock_value = (
491 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530492 ) + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530493 if new_stock_value >= 0:
494 # calculate new valuation rate only if stock value is positive
495 # else it remains the same as that of previous entry
496 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
497
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530498 if (
499 not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry()
500 ):
501 allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate(
502 self.sle.voucher_type, self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530503 )
504 if not allow_zero_rate:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530505 self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530506
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530507 self.wh_data.qty_after_transaction += self.sle.actual_qty
508 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
509 self.wh_data.valuation_rate
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530510 )
511
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530512 def is_rejected_entry(self):
513 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530514
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530515 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530516 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530517
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530518 def get_incoming_rate_of_serial_no(self, serial_no):
519 return self.serial_no_incoming_rate.get(serial_no, 0.0)
520
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530521
522def is_rejected(voucher_type, voucher_detail_no, warehouse):
523 if voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
524 return warehouse == frappe.get_cached_value(
525 voucher_type + " Item", voucher_detail_no, "rejected_warehouse"
526 )
527
528 return False
529
530
Rohit Waghchaure46704642023-03-23 11:41:20 +0530531class BatchNoValuation(DeprecatedBatchNoValuation):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530532 def __init__(self, **kwargs):
533 for key, value in kwargs.items():
534 setattr(self, key, value)
535
536 self.batch_nos = self.get_batch_nos()
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530537 self.prepare_batches()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530538 self.calculate_avg_rate()
539 self.calculate_valuation_rate()
540
541 def calculate_avg_rate(self):
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530542 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530543 self.stock_value_change = frappe.get_cached_value(
544 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530545 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530546 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530547 entries = self.get_batch_no_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530548 self.stock_value_change = 0.0
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530549 self.batch_avg_rate = defaultdict(float)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530550 self.available_qty = defaultdict(float)
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530551 self.stock_value_differece = defaultdict(float)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530552
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530553 for ledger in entries:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530554 self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530555 self.available_qty[ledger.batch_no] += flt(ledger.qty)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530556
557 self.calculate_avg_rate_from_deprecarated_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530558 self.calculate_avg_rate_for_non_batchwise_valuation()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530559 self.set_stock_value_difference()
560
561 def get_batch_no_ledgers(self) -> List[dict]:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530562 if not self.batchwise_valuation_batches:
563 return []
564
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530565 parent = frappe.qb.DocType("Serial and Batch Bundle")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530566 child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530567
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530568 timestamp_condition = ""
569 if self.sle.posting_date and self.sle.posting_time:
570 timestamp_condition = CombineDatetime(
571 parent.posting_date, parent.posting_time
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530572 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530573
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530574 query = (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530575 frappe.qb.from_(parent)
576 .inner_join(child)
577 .on(parent.name == child.parent)
578 .select(
579 child.batch_no,
580 Sum(child.stock_value_difference).as_("incoming_rate"),
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530581 Sum(child.qty).as_("qty"),
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530582 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530583 .where(
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530584 (child.batch_no.isin(self.batchwise_valuation_batches))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530585 & (parent.warehouse == self.sle.warehouse)
586 & (parent.item_code == self.sle.item_code)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530587 & (parent.docstatus == 1)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530588 & (parent.is_cancelled == 0)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530589 & (parent.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530590 )
591 .groupby(child.batch_no)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530592 )
593
rohitwaghchaure5e9016f2023-12-01 21:42:22 +0530594 # Important to exclude the current voucher detail no / voucher no to calculate the correct stock value difference
595 if self.sle.voucher_detail_no:
596 query = query.where(parent.voucher_detail_no != self.sle.voucher_detail_no)
597 elif self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530598 query = query.where(parent.voucher_no != self.sle.voucher_no)
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530599
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530600 if timestamp_condition:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530601 query = query.where(timestamp_condition)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530602
603 return query.run(as_dict=True)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530604
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530605 def prepare_batches(self):
606 self.batches = self.batch_nos
607 if isinstance(self.batch_nos, dict):
608 self.batches = list(self.batch_nos.keys())
609
610 self.batchwise_valuation_batches = []
611 self.non_batchwise_valuation_batches = []
612
613 batches = frappe.get_all(
614 "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
615 )
616
617 for batch in batches:
618 self.batchwise_valuation_batches.append(batch.name)
619
620 self.non_batchwise_valuation_batches = list(
621 set(self.batches) - set(self.batchwise_valuation_batches)
622 )
623
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530624 def get_batch_nos(self) -> list:
625 if self.sle.get("batch_nos"):
626 return self.sle.batch_nos
627
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530628 return get_batch_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530629
630 def set_stock_value_difference(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530631 for batch_no, ledger in self.batch_nos.items():
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530632 if batch_no in self.non_batchwise_valuation_batches:
633 continue
634
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530635 if not self.available_qty[batch_no]:
636 continue
637
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530638 self.batch_avg_rate[batch_no] = (
639 self.stock_value_differece[batch_no] / self.available_qty[batch_no]
640 )
641
642 # New Stock Value Difference
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530643 stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530644 self.stock_value_change += stock_value_change
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530645
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530646 frappe.db.set_value(
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530647 "Serial and Batch Entry",
648 ledger.name,
649 {
650 "stock_value_difference": stock_value_change,
651 "incoming_rate": self.batch_avg_rate[batch_no],
652 },
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530653 )
654
655 def calculate_valuation_rate(self):
656 if not hasattr(self, "wh_data"):
657 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530658
659 self.wh_data.stock_value = round_off_if_near_zero(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530660 self.wh_data.stock_value + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530661 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530662
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530663 self.wh_data.qty_after_transaction += self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530664 if self.wh_data.qty_after_transaction:
665 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
666
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530667 def get_incoming_rate(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530668 if not self.sle.actual_qty:
669 self.sle.actual_qty = self.get_actual_qty()
670
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530671 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530672
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530673 def get_actual_qty(self):
674 total_qty = 0.0
675 for batch_no in self.available_qty:
676 total_qty += self.available_qty[batch_no]
677
678 return total_qty
679
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530680
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530681def get_batch_nos(serial_and_batch_bundle):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530682 if not serial_and_batch_bundle:
683 return frappe._dict({})
684
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530685 entries = frappe.get_all(
686 "Serial and Batch Entry",
687 fields=["batch_no", "qty", "name"],
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530688 filters={"parent": serial_and_batch_bundle, "batch_no": ("is", "set")},
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530689 order_by="idx",
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530690 )
691
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530692 if not entries:
693 return frappe._dict({})
694
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530695 return {d.batch_no: d for d in entries}
696
697
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530698def get_empty_batches_based_work_order(work_order, item_code):
Rohit Waghchaure46704642023-03-23 11:41:20 +0530699 batches = get_batches_from_work_order(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530700 if not batches:
701 return batches
702
Rohit Waghchaure46704642023-03-23 11:41:20 +0530703 entries = get_batches_from_stock_entries(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530704 if not entries:
705 return batches
706
707 ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
708 if ids:
709 set_batch_details_from_package(ids, batches)
710
711 # Will be deprecated in v16
712 for d in entries:
713 if not d.batch_no:
714 continue
715
716 batches[d.batch_no] -= d.qty
717
718 return batches
719
720
Rohit Waghchaure46704642023-03-23 11:41:20 +0530721def get_batches_from_work_order(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530722 return frappe._dict(
723 frappe.get_all(
Rohit Waghchaure46704642023-03-23 11:41:20 +0530724 "Batch",
725 fields=["name", "qty_to_produce"],
726 filters={"reference_name": work_order, "item": item_code},
727 as_list=1,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530728 )
729 )
730
731
Rohit Waghchaure46704642023-03-23 11:41:20 +0530732def get_batches_from_stock_entries(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530733 entries = frappe.get_all(
734 "Stock Entry",
735 filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
736 fields=["name"],
737 )
738
739 return frappe.get_all(
740 "Stock Entry Detail",
741 fields=["batch_no", "qty", "serial_and_batch_bundle"],
742 filters={
743 "parent": ("in", [d.name for d in entries]),
744 "is_finished_item": 1,
Rohit Waghchaure46704642023-03-23 11:41:20 +0530745 "item_code": item_code,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530746 },
747 )
748
749
750def set_batch_details_from_package(ids, batches):
751 entries = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530752 "Serial and Batch Entry",
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530753 filters={"parent": ("in", ids), "is_outward": 0},
754 fields=["batch_no", "qty"],
755 )
756
757 for d in entries:
758 batches[d.batch_no] -= d.qty
Rohit Waghchaure46704642023-03-23 11:41:20 +0530759
760
761class SerialBatchCreation:
762 def __init__(self, args):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530763 self.set(args)
764 self.set_item_details()
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530765 self.set_other_details()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530766
767 def set(self, args):
768 self.__dict__ = {}
Rohit Waghchaure46704642023-03-23 11:41:20 +0530769 for key, value in args.items():
770 setattr(self, key, value)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530771 self.__dict__[key] = value
772
773 def get(self, key):
774 return self.__dict__.get(key)
775
776 def set_item_details(self):
777 fields = [
778 "has_batch_no",
779 "has_serial_no",
780 "item_name",
781 "item_group",
782 "serial_no_series",
783 "create_new_batch",
784 "batch_number_series",
785 "description",
786 ]
787
788 item_details = frappe.get_cached_value("Item", self.item_code, fields, as_dict=1)
789 for key, value in item_details.items():
790 setattr(self, key, value)
791
792 self.__dict__.update(item_details)
Rohit Waghchaure46704642023-03-23 11:41:20 +0530793
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530794 def set_other_details(self):
795 if not self.get("posting_date"):
796 setattr(self, "posting_date", today())
797 self.__dict__["posting_date"] = self.posting_date
798
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530799 if not self.get("actual_qty"):
800 qty = self.get("qty") or self.get("total_qty")
801
802 setattr(self, "actual_qty", qty)
803 self.__dict__["actual_qty"] = self.actual_qty
804
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530805 if not hasattr(self, "use_serial_batch_fields"):
806 setattr(self, "use_serial_batch_fields", 0)
807
Rohit Waghchaure46704642023-03-23 11:41:20 +0530808 def duplicate_package(self):
809 if not self.serial_and_batch_bundle:
810 return
811
812 id = self.serial_and_batch_bundle
813 package = frappe.get_doc("Serial and Batch Bundle", id)
814 new_package = frappe.copy_doc(package)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530815
816 if self.get("returned_serial_nos"):
817 self.remove_returned_serial_nos(new_package)
818
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530819 new_package.docstatus = 0
Rohit Waghchaure46704642023-03-23 11:41:20 +0530820 new_package.type_of_transaction = self.type_of_transaction
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530821 new_package.returned_against = self.get("returned_against")
Rohit Waghchaure46704642023-03-23 11:41:20 +0530822 new_package.save()
823
824 self.serial_and_batch_bundle = new_package.name
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530825
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530826 def remove_returned_serial_nos(self, package):
827 remove_list = []
828 for d in package.entries:
829 if d.serial_no in self.returned_serial_nos:
830 remove_list.append(d)
831
832 for d in remove_list:
833 package.remove(d)
834
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530835 def make_serial_and_batch_bundle(self):
836 doc = frappe.new_doc("Serial and Batch Bundle")
837 valid_columns = doc.meta.get_valid_columns()
838 for key, value in self.__dict__.items():
839 if key in valid_columns:
840 doc.set(key, value)
841
842 if self.type_of_transaction == "Outward":
843 self.set_auto_serial_batch_entries_for_outward()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530844 elif self.type_of_transaction == "Inward":
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530845 self.set_auto_serial_batch_entries_for_inward()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530846 self.add_serial_nos_for_batch_item()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530847
848 self.set_serial_batch_entries(doc)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530849 if not doc.get("entries"):
850 return frappe._dict({})
851
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530852 doc.save()
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530853 self.validate_qty(doc)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530854
855 if not hasattr(self, "do_not_submit") or not self.do_not_submit:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530856 doc.flags.ignore_voucher_validation = True
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530857 doc.submit()
858
859 return doc
860
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530861 def add_serial_nos_for_batch_item(self):
862 if not (self.has_serial_no and self.has_batch_no):
863 return
864
865 if not self.get("serial_nos") and self.get("batches"):
866 batches = list(self.get("batches").keys())
867 if len(batches) == 1:
868 self.batch_no = batches[0]
869 self.serial_nos = self.get_auto_created_serial_nos()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530870
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530871 def update_serial_and_batch_entries(self):
872 doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
873 doc.type_of_transaction = self.type_of_transaction
874 doc.set("entries", [])
875 self.set_auto_serial_batch_entries_for_outward()
876 self.set_serial_batch_entries(doc)
877 if not doc.get("entries"):
878 return frappe._dict({})
879
880 doc.save()
881 return doc
882
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530883 def validate_qty(self, doc):
884 if doc.type_of_transaction == "Outward":
885 precision = doc.precision("total_qty")
886
887 total_qty = abs(flt(doc.total_qty, precision))
888 required_qty = abs(flt(self.actual_qty, precision))
889
890 if required_qty - total_qty > 0:
891 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."
892 frappe.throw(msg, title=_("Insufficient Stock"))
893
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530894 def set_auto_serial_batch_entries_for_outward(self):
895 from erpnext.stock.doctype.batch.batch import get_available_batches
896 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
897
898 kwargs = frappe._dict(
899 {
900 "item_code": self.item_code,
901 "warehouse": self.warehouse,
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530902 "qty": abs(self.actual_qty) if self.actual_qty else 0,
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530903 "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
904 }
905 )
906
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530907 if self.get("ignore_serial_nos"):
908 kwargs["ignore_serial_nos"] = self.ignore_serial_nos
909
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530910 if self.has_serial_no and not self.get("serial_nos"):
911 self.serial_nos = get_serial_nos_for_outward(kwargs)
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530912 elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530913 self.batches = get_available_batches(kwargs)
914
915 def set_auto_serial_batch_entries_for_inward(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530916 if (self.get("batches") and self.has_batch_no) or (
917 self.get("serial_nos") and self.has_serial_no
918 ):
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530919 if self.use_serial_batch_fields and self.get("serial_nos"):
920 self.make_serial_no_if_not_exists()
921
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530922 return
923
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530924 self.batch_no = None
925 if self.has_batch_no:
926 self.batch_no = self.create_batch()
927
928 if self.has_serial_no:
929 self.serial_nos = self.get_auto_created_serial_nos()
930 else:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530931 self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530932
Rohit Waghchaure9fafc832024-02-04 10:42:31 +0530933 def make_serial_no_if_not_exists(self):
934 non_exists_serial_nos = []
935 for row in self.serial_nos:
936 if not frappe.db.exists("Serial No", row):
937 non_exists_serial_nos.append(row)
938
939 if non_exists_serial_nos:
940 self.make_serial_nos(non_exists_serial_nos)
941
942 def make_serial_nos(self, serial_nos):
943 serial_nos_details = []
944 batch_no = None
945 if self.batches:
946 batch_no = list(self.batches.keys())[0]
947
948 for serial_no in serial_nos:
949 serial_nos_details.append(
950 (
951 serial_no,
952 serial_no,
953 now(),
954 now(),
955 frappe.session.user,
956 frappe.session.user,
957 self.warehouse,
958 self.company,
959 self.item_code,
960 self.item_name,
961 self.description,
962 "Active",
963 batch_no,
964 )
965 )
966
967 if serial_nos_details:
968 fields = [
969 "name",
970 "serial_no",
971 "creation",
972 "modified",
973 "owner",
974 "modified_by",
975 "warehouse",
976 "company",
977 "item_code",
978 "item_name",
979 "description",
980 "status",
981 "batch_no",
982 ]
983
984 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
985
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530986 def set_serial_batch_entries(self, doc):
987 if self.get("serial_nos"):
988 serial_no_wise_batch = frappe._dict({})
989 if self.has_batch_no:
s-aga-r2d8363a2023-09-02 11:02:24 +0530990 serial_no_wise_batch = get_serial_nos_batch(self.serial_nos)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530991
992 qty = -1 if self.type_of_transaction == "Outward" else 1
993 for serial_no in self.serial_nos:
994 doc.append(
995 "entries",
996 {
997 "serial_no": serial_no,
998 "qty": qty,
999 "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
1000 "incoming_rate": self.get("incoming_rate"),
1001 },
1002 )
1003
Rohit Waghchauree88c5d62023-04-05 20:03:44 +05301004 elif self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301005 for batch_no, batch_qty in self.batches.items():
1006 doc.append(
1007 "entries",
1008 {
1009 "batch_no": batch_no,
1010 "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
1011 "incoming_rate": self.get("incoming_rate"),
1012 },
1013 )
1014
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301015 def create_batch(self):
1016 from erpnext.stock.doctype.batch.batch import make_batch
1017
1018 return make_batch(
1019 frappe._dict(
1020 {
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301021 "item": self.get("item_code"),
1022 "reference_doctype": self.get("voucher_type"),
1023 "reference_name": self.get("voucher_no"),
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301024 }
1025 )
1026 )
1027
1028 def get_auto_created_serial_nos(self):
1029 sr_nos = []
1030 serial_nos_details = []
1031
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301032 if not self.serial_no_series:
1033 msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually."
1034 frappe.throw(_(msg))
1035
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301036 for i in range(abs(cint(self.actual_qty))):
Rohit Waghchaure648efca2023-03-28 12:16:27 +05301037 serial_no = make_autoname(self.serial_no_series, "Serial No")
1038 sr_nos.append(serial_no)
1039 serial_nos_details.append(
1040 (
1041 serial_no,
1042 serial_no,
1043 now(),
1044 now(),
1045 frappe.session.user,
1046 frappe.session.user,
1047 self.warehouse,
1048 self.company,
1049 self.item_code,
1050 self.item_name,
1051 self.description,
1052 "Active",
1053 self.batch_no,
1054 )
1055 )
1056
1057 if serial_nos_details:
1058 fields = [
1059 "name",
1060 "serial_no",
1061 "creation",
1062 "modified",
1063 "owner",
1064 "modified_by",
1065 "warehouse",
1066 "company",
1067 "item_code",
1068 "item_name",
1069 "description",
1070 "status",
1071 "batch_no",
1072 ]
1073
1074 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
1075
1076 return sr_nos
1077
1078
1079def get_serial_or_batch_items(items):
1080 serial_or_batch_items = frappe.get_all(
1081 "Item",
1082 filters={"name": ("in", [d.item_code for d in items])},
1083 or_filters={"has_serial_no": 1, "has_batch_no": 1},
1084 )
1085
1086 if not serial_or_batch_items:
1087 return
1088 else:
1089 serial_or_batch_items = [d.name for d in serial_or_batch_items]
1090
1091 return serial_or_batch_items
s-aga-r2d8363a2023-09-02 11:02:24 +05301092
1093
1094def get_serial_nos_batch(serial_nos):
1095 return frappe._dict(
1096 frappe.get_all(
1097 "Serial No",
1098 fields=["name", "batch_no"],
1099 filters={"name": ("in", serial_nos)},
1100 as_list=1,
1101 )
1102 )