blob: 2c18f99acd9443c498305fec67f784efb04b01e2 [file] [log] [blame]
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05301from collections import defaultdict
2from typing import List
Rohit Waghchauref1b59662023-03-06 12:08:28 +05303
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05304import frappe
5from frappe import _, bold
6from frappe.model.naming import make_autoname
Rohit Waghchaure86da3062023-03-20 14:15:34 +05307from frappe.query_builder.functions import CombineDatetime, Sum
Rohit Waghchauref968f0f2023-06-14 23:22:22 +05308from frappe.utils import cint, flt, get_link_to_form, now, nowtime, today
Rohit Waghchauree6143ab2023-03-13 14:51:43 +05309
10from erpnext.stock.deprecated_serial_batch import (
11 DeprecatedBatchNoValuation,
12 DeprecatedSerialNoValuation,
13)
Rohit Waghchauref1b59662023-03-06 12:08:28 +053014from erpnext.stock.valuation import round_off_if_near_zero
15
16
17class SerialBatchBundle:
18 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053019 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +053020 setattr(self, key, value)
21
22 self.set_item_details()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053023 self.process_serial_and_batch_bundle()
24 if self.sle.is_cancelled:
25 self.delink_serial_and_batch_bundle()
26
27 self.post_process()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053028
29 def process_serial_and_batch_bundle(self):
30 if self.item_details.has_serial_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053031 self.process_serial_no()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053032 elif self.item_details.has_batch_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053033 self.process_batch_no()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053034
35 def set_item_details(self):
36 fields = [
37 "has_batch_no",
38 "has_serial_no",
39 "item_name",
40 "item_group",
41 "serial_no_series",
42 "create_new_batch",
43 "batch_number_series",
44 ]
45
46 self.item_details = frappe.get_cached_value("Item", self.sle.item_code, fields, as_dict=1)
47
48 def process_serial_no(self):
49 if (
50 not self.sle.is_cancelled
51 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +053052 and self.item_details.has_serial_no == 1
Rohit Waghchauref1b59662023-03-06 12:08:28 +053053 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053054 self.make_serial_batch_no_bundle()
55 elif not self.sle.is_cancelled:
56 self.validate_item_and_warehouse()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053057
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053058 def make_serial_batch_no_bundle(self):
Rohit Waghchaure648efca2023-03-28 12:16:27 +053059 self.validate_item()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053060
Rohit Waghchaure648efca2023-03-28 12:16:27 +053061 sn_doc = SerialBatchCreation(
62 {
63 "item_code": self.item_code,
64 "warehouse": self.warehouse,
65 "posting_date": self.sle.posting_date,
66 "posting_time": self.sle.posting_time,
67 "voucher_type": self.sle.voucher_type,
68 "voucher_no": self.sle.voucher_no,
69 "voucher_detail_no": self.sle.voucher_detail_no,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +053070 "qty": self.sle.actual_qty,
Rohit Waghchaure648efca2023-03-28 12:16:27 +053071 "avg_rate": self.sle.incoming_rate,
72 "total_amount": flt(self.sle.actual_qty) * flt(self.sle.incoming_rate),
73 "type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward",
74 "company": self.company,
75 "is_rejected": self.is_rejected_entry(),
76 }
77 ).make_serial_and_batch_bundle()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053078
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053079 self.set_serial_and_batch_bundle(sn_doc)
Rohit Waghchauref1b59662023-03-06 12:08:28 +053080
Rohit Waghchaure42b22942023-05-27 19:18:03 +053081 def validate_actual_qty(self, sn_doc):
Rohit Waghchauref968f0f2023-06-14 23:22:22 +053082 link = get_link_to_form("Serial and Batch Bundle", sn_doc.name)
83
84 condition = {
85 "Inward": self.sle.actual_qty > 0,
86 "Outward": self.sle.actual_qty < 0,
87 }.get(sn_doc.type_of_transaction)
88
89 if not condition:
90 correct_type = "Inward"
91 if sn_doc.type_of_transaction == "Inward":
92 correct_type = "Outward"
93
94 msg = f"The type of transaction of Serial and Batch Bundle {link} is {bold(sn_doc.type_of_transaction)} but as per the Actual Qty {self.sle.actual_qty} for the item {bold(self.sle.item_code)} in the {self.sle.voucher_type} {self.sle.voucher_no} the type of transaction should be {bold(correct_type)}"
95 frappe.throw(_(msg), title=_("Incorrect Type of Transaction"))
96
Rohit Waghchaure42b22942023-05-27 19:18:03 +053097 precision = sn_doc.precision("total_qty")
98 if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision):
Rohit Waghchauref968f0f2023-06-14 23:22:22 +053099 msg = f"Total qty {flt(sn_doc.total_qty, precision)} of Serial and Batch Bundle {link} is not equal to Actual Qty {flt(self.sle.actual_qty, precision)} in the {self.sle.voucher_type} {self.sle.voucher_no}"
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530100 frappe.throw(_(msg))
101
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530102 def validate_item(self):
103 msg = ""
104 if self.sle.actual_qty > 0:
105 if not self.item_details.has_batch_no and not self.item_details.has_serial_no:
106 msg = f"Item {self.item_code} is not a batch or serial no item"
107
108 if self.item_details.has_serial_no and not self.item_details.serial_no_series:
109 msg += f". If you want auto pick serial bundle, then kindly set Serial No Series in Item {self.item_code}"
110
111 if (
112 self.item_details.has_batch_no
113 and not self.item_details.batch_number_series
114 and not frappe.db.get_single_value("Stock Settings", "naming_series_prefix")
115 ):
116 msg += f". If you want auto pick batch bundle, then kindly set Batch Number Series in Item {self.item_code}"
117
118 elif self.sle.actual_qty < 0:
119 if not frappe.db.get_single_value(
120 "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
121 ):
122 msg += ". If you want auto pick serial/batch bundle, then kindly enable 'Auto Create Serial and Batch Bundle' in Stock Settings."
123
124 if msg:
125 error_msg = (
126 f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}."
127 + msg
128 )
129 frappe.throw(_(error_msg))
130
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530131 def set_serial_and_batch_bundle(self, sn_doc):
132 self.sle.db_set("serial_and_batch_bundle", sn_doc.name)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530133
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530134 if sn_doc.is_rejected:
135 frappe.db.set_value(
136 self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name
137 )
138 else:
139 frappe.db.set_value(
140 self.child_doctype, self.sle.voucher_detail_no, "serial_and_batch_bundle", sn_doc.name
141 )
142
143 @property
144 def child_doctype(self):
145 child_doctype = self.sle.voucher_type + " Item"
146 if self.sle.voucher_type == "Stock Entry":
147 child_doctype = "Stock Entry Detail"
148
Rohit Waghchaure26b39ac2023-04-06 01:36:18 +0530149 if self.sle.voucher_type == "Asset Capitalization":
150 child_doctype = "Asset Capitalization Stock Item"
151
152 if self.sle.voucher_type == "Asset Repair":
153 child_doctype = "Asset Repair Consumed Item"
154
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530155 return child_doctype
156
157 def is_rejected_entry(self):
158 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
159
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530160 def process_batch_no(self):
161 if (
162 not self.sle.is_cancelled
163 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530164 and self.item_details.has_batch_no == 1
165 and self.item_details.create_new_batch
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530166 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530167 self.make_serial_batch_no_bundle()
168 elif not self.sle.is_cancelled:
169 self.validate_item_and_warehouse()
170
171 def validate_item_and_warehouse(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530172 if self.sle.serial_and_batch_bundle and not frappe.db.exists(
173 "Serial and Batch Bundle",
174 {
175 "name": self.sle.serial_and_batch_bundle,
176 "item_code": self.item_code,
177 "warehouse": self.warehouse,
178 "voucher_no": self.sle.voucher_no,
179 },
180 ):
181 msg = f"""
182 The Serial and Batch Bundle
183 {bold(self.sle.serial_and_batch_bundle)}
184 does not belong to Item {bold(self.item_code)}
185 or Warehouse {bold(self.warehouse)}
186 or {self.sle.voucher_type} no {bold(self.sle.voucher_no)}
187 """
188
189 frappe.throw(_(msg))
190
191 def delink_serial_and_batch_bundle(self):
192 update_values = {
193 "serial_and_batch_bundle": "",
194 }
195
196 if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse):
197 update_values["rejected_serial_and_batch_bundle"] = ""
198
199 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values)
200
201 frappe.db.set_value(
202 "Serial and Batch Bundle",
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530203 {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530204 {"is_cancelled": 1, "voucher_no": ""},
205 )
206
Rohit Waghchauref79f2a32023-04-04 11:50:38 +0530207 if self.sle.serial_and_batch_bundle:
208 frappe.get_cached_doc(
209 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle
210 ).validate_serial_and_batch_inventory()
211
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530212 def post_process(self):
Rohit Waghchaure674bd3e2023-03-17 16:42:59 +0530213 if not self.sle.serial_and_batch_bundle:
214 return
215
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530216 docstatus = frappe.get_cached_value(
217 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
218 )
219
220 if docstatus != 1:
221 self.submit_serial_and_batch_bundle()
222
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530223 if self.item_details.has_serial_no == 1:
224 self.set_warehouse_and_status_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530225
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530226 if (
227 self.sle.actual_qty > 0
228 and self.item_details.has_serial_no == 1
229 and self.item_details.has_batch_no == 1
230 ):
231 self.set_batch_no_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530232
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530233 if self.item_details.has_batch_no == 1:
234 self.update_batch_qty()
235
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530236 def submit_serial_and_batch_bundle(self):
237 doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530238 self.validate_actual_qty(doc)
239
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530240 doc.flags.ignore_voucher_validation = True
241 doc.submit()
242
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530243 def set_warehouse_and_status_in_serial_nos(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530244 serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530245 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
246
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530247 if not serial_nos:
248 return
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530249
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530250 sn_table = frappe.qb.DocType("Serial No")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530251 (
252 frappe.qb.update(sn_table)
253 .set(sn_table.warehouse, warehouse)
254 .set(sn_table.status, "Active" if warehouse else "Inactive")
255 .where(sn_table.name.isin(serial_nos))
256 ).run()
257
258 def set_batch_no_in_serial_nos(self):
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530259 entries = frappe.get_all(
260 "Serial and Batch Entry",
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530261 fields=["serial_no", "batch_no"],
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530262 filters={"parent": self.sle.serial_and_batch_bundle},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530263 )
264
265 batch_serial_nos = {}
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530266 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530267 batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no)
268
269 for batch_no, serial_nos in batch_serial_nos.items():
270 sn_table = frappe.qb.DocType("Serial No")
271 (
272 frappe.qb.update(sn_table)
273 .set(sn_table.batch_no, batch_no)
274 .where(sn_table.name.isin(serial_nos))
275 ).run()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530276
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530277 def update_batch_qty(self):
278 from erpnext.stock.doctype.batch.batch import get_available_batches
279
280 batches = get_batch_nos(self.sle.serial_and_batch_bundle)
281
282 batches_qty = get_available_batches(
283 frappe._dict(
284 {"item_code": self.item_code, "warehouse": self.warehouse, "batch_no": list(batches.keys())}
285 )
286 )
287
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530288 for batch_no in batches:
289 frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0))
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530290
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530291
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530292def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530293 if not serial_and_batch_bundle:
294 return []
295
296 filters = {"parent": serial_and_batch_bundle, "serial_no": ("is", "set")}
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530297 if isinstance(serial_and_batch_bundle, list):
298 filters = {"parent": ("in", serial_and_batch_bundle)}
299
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530300 if serial_nos:
301 filters["serial_no"] = ("in", serial_nos)
302
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530303 entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters)
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530304 if not entries:
305 return []
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530306
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530307 return [d.serial_no for d in entries if d.serial_no]
308
309
310def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
311 return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530312
313
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530314def get_serial_or_batch_nos(bundle):
315 return frappe.get_all("Serial and Batch Entry", fields=["*"], filters={"parent": bundle})
316
317
Rohit Waghchaure46704642023-03-23 11:41:20 +0530318class SerialNoValuation(DeprecatedSerialNoValuation):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530319 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530320 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530321 setattr(self, key, value)
322
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530323 self.calculate_stock_value_change()
324 self.calculate_valuation_rate()
325
326 def calculate_stock_value_change(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530327 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530328 self.stock_value_change = frappe.get_cached_value(
329 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
330 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530331
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530332 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530333 entries = self.get_serial_no_ledgers()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530334
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530335 self.serial_no_incoming_rate = defaultdict(float)
336 self.stock_value_change = 0.0
337
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530338 for ledger in entries:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530339 self.stock_value_change += ledger.incoming_rate
340 self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530341
342 self.calculate_stock_value_from_deprecarated_ledgers()
343
344 def get_serial_no_ledgers(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530345 serial_nos = self.get_serial_nos()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530346 bundle = frappe.qb.DocType("Serial and Batch Bundle")
347 bundle_child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530348
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530349 query = (
350 frappe.qb.from_(bundle)
351 .inner_join(bundle_child)
352 .on(bundle.name == bundle_child.parent)
353 .select(
354 bundle.name,
355 bundle_child.serial_no,
356 (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"),
357 )
358 .where(
359 (bundle.is_cancelled == 0)
360 & (bundle.docstatus == 1)
361 & (bundle_child.serial_no.isin(serial_nos))
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530362 & (bundle.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530363 & (bundle.item_code == self.sle.item_code)
364 & (bundle_child.warehouse == self.sle.warehouse)
365 )
366 .orderby(bundle.posting_date, bundle.posting_time, bundle.creation)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530367 )
368
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530369 # Important to exclude the current voucher
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530370 if self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530371 query = query.where(bundle.voucher_no != self.sle.voucher_no)
372
373 if self.sle.posting_date:
374 if self.sle.posting_time is None:
375 self.sle.posting_time = nowtime()
376
377 timestamp_condition = CombineDatetime(
378 bundle.posting_date, bundle.posting_time
379 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
380
381 query = query.where(timestamp_condition)
382
383 return query.run(as_dict=True)
384
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530385 def get_serial_nos(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530386 if self.sle.get("serial_nos"):
387 return self.sle.serial_nos
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530388
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530389 return get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530390
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530391 def calculate_valuation_rate(self):
392 if not hasattr(self, "wh_data"):
393 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530394
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530395 new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530396
397 if new_stock_qty > 0:
398 new_stock_value = (
399 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530400 ) + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530401 if new_stock_value >= 0:
402 # calculate new valuation rate only if stock value is positive
403 # else it remains the same as that of previous entry
404 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
405
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530406 if (
407 not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry()
408 ):
409 allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate(
410 self.sle.voucher_type, self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530411 )
412 if not allow_zero_rate:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530413 self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530414
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530415 self.wh_data.qty_after_transaction += self.sle.actual_qty
416 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
417 self.wh_data.valuation_rate
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530418 )
419
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530420 def is_rejected_entry(self):
421 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530422
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530423 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530424 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530425
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530426 def get_incoming_rate_of_serial_no(self, serial_no):
427 return self.serial_no_incoming_rate.get(serial_no, 0.0)
428
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530429
430def is_rejected(voucher_type, voucher_detail_no, warehouse):
431 if voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
432 return warehouse == frappe.get_cached_value(
433 voucher_type + " Item", voucher_detail_no, "rejected_warehouse"
434 )
435
436 return False
437
438
Rohit Waghchaure46704642023-03-23 11:41:20 +0530439class BatchNoValuation(DeprecatedBatchNoValuation):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530440 def __init__(self, **kwargs):
441 for key, value in kwargs.items():
442 setattr(self, key, value)
443
444 self.batch_nos = self.get_batch_nos()
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530445 self.prepare_batches()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530446 self.calculate_avg_rate()
447 self.calculate_valuation_rate()
448
449 def calculate_avg_rate(self):
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530450 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530451 self.stock_value_change = frappe.get_cached_value(
452 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530453 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530454 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530455 entries = self.get_batch_no_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530456 self.stock_value_change = 0.0
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530457 self.batch_avg_rate = defaultdict(float)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530458 self.available_qty = defaultdict(float)
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530459 self.stock_value_differece = defaultdict(float)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530460
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530461 for ledger in entries:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530462 self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530463 self.available_qty[ledger.batch_no] += flt(ledger.qty)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530464
465 self.calculate_avg_rate_from_deprecarated_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530466 self.calculate_avg_rate_for_non_batchwise_valuation()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530467 self.set_stock_value_difference()
468
469 def get_batch_no_ledgers(self) -> List[dict]:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530470 if not self.batchwise_valuation_batches:
471 return []
472
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530473 parent = frappe.qb.DocType("Serial and Batch Bundle")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530474 child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530475
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530476 timestamp_condition = ""
477 if self.sle.posting_date and self.sle.posting_time:
478 timestamp_condition = CombineDatetime(
479 parent.posting_date, parent.posting_time
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530480 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530481
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530482 query = (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530483 frappe.qb.from_(parent)
484 .inner_join(child)
485 .on(parent.name == child.parent)
486 .select(
487 child.batch_no,
488 Sum(child.stock_value_difference).as_("incoming_rate"),
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530489 Sum(child.qty).as_("qty"),
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530490 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530491 .where(
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530492 (child.batch_no.isin(self.batchwise_valuation_batches))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530493 & (parent.warehouse == self.sle.warehouse)
494 & (parent.item_code == self.sle.item_code)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530495 & (parent.docstatus == 1)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530496 & (parent.is_cancelled == 0)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530497 & (parent.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530498 )
499 .groupby(child.batch_no)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530500 )
501
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530502 # Important to exclude the current voucher
503 if self.sle.voucher_no:
504 query = query.where(parent.voucher_no != self.sle.voucher_no)
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530505
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530506 if timestamp_condition:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530507 query = query.where(timestamp_condition)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530508
509 return query.run(as_dict=True)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530510
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530511 def prepare_batches(self):
512 self.batches = self.batch_nos
513 if isinstance(self.batch_nos, dict):
514 self.batches = list(self.batch_nos.keys())
515
516 self.batchwise_valuation_batches = []
517 self.non_batchwise_valuation_batches = []
518
519 batches = frappe.get_all(
520 "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
521 )
522
523 for batch in batches:
524 self.batchwise_valuation_batches.append(batch.name)
525
526 self.non_batchwise_valuation_batches = list(
527 set(self.batches) - set(self.batchwise_valuation_batches)
528 )
529
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530530 def get_batch_nos(self) -> list:
531 if self.sle.get("batch_nos"):
532 return self.sle.batch_nos
533
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530534 return get_batch_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530535
536 def set_stock_value_difference(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530537 for batch_no, ledger in self.batch_nos.items():
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530538 if batch_no in self.non_batchwise_valuation_batches:
539 continue
540
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530541 if not self.available_qty[batch_no]:
542 continue
543
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530544 self.batch_avg_rate[batch_no] = (
545 self.stock_value_differece[batch_no] / self.available_qty[batch_no]
546 )
547
548 # New Stock Value Difference
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530549 stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530550 self.stock_value_change += stock_value_change
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530551
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530552 frappe.db.set_value(
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530553 "Serial and Batch Entry",
554 ledger.name,
555 {
556 "stock_value_difference": stock_value_change,
557 "incoming_rate": self.batch_avg_rate[batch_no],
558 },
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530559 )
560
561 def calculate_valuation_rate(self):
562 if not hasattr(self, "wh_data"):
563 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530564
565 self.wh_data.stock_value = round_off_if_near_zero(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530566 self.wh_data.stock_value + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530567 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530568
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530569 self.wh_data.qty_after_transaction += self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530570 if self.wh_data.qty_after_transaction:
571 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
572
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530573 def get_incoming_rate(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530574 if not self.sle.actual_qty:
575 self.sle.actual_qty = self.get_actual_qty()
576
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530577 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530578
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530579 def get_actual_qty(self):
580 total_qty = 0.0
581 for batch_no in self.available_qty:
582 total_qty += self.available_qty[batch_no]
583
584 return total_qty
585
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530586
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530587def get_batch_nos(serial_and_batch_bundle):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530588 if not serial_and_batch_bundle:
589 return frappe._dict({})
590
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530591 entries = frappe.get_all(
592 "Serial and Batch Entry",
593 fields=["batch_no", "qty", "name"],
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530594 filters={"parent": serial_and_batch_bundle, "batch_no": ("is", "set")},
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530595 order_by="idx",
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530596 )
597
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530598 if not entries:
599 return frappe._dict({})
600
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530601 return {d.batch_no: d for d in entries}
602
603
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530604def get_empty_batches_based_work_order(work_order, item_code):
Rohit Waghchaure46704642023-03-23 11:41:20 +0530605 batches = get_batches_from_work_order(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530606 if not batches:
607 return batches
608
Rohit Waghchaure46704642023-03-23 11:41:20 +0530609 entries = get_batches_from_stock_entries(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530610 if not entries:
611 return batches
612
613 ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
614 if ids:
615 set_batch_details_from_package(ids, batches)
616
617 # Will be deprecated in v16
618 for d in entries:
619 if not d.batch_no:
620 continue
621
622 batches[d.batch_no] -= d.qty
623
624 return batches
625
626
Rohit Waghchaure46704642023-03-23 11:41:20 +0530627def get_batches_from_work_order(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530628 return frappe._dict(
629 frappe.get_all(
Rohit Waghchaure46704642023-03-23 11:41:20 +0530630 "Batch",
631 fields=["name", "qty_to_produce"],
632 filters={"reference_name": work_order, "item": item_code},
633 as_list=1,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530634 )
635 )
636
637
Rohit Waghchaure46704642023-03-23 11:41:20 +0530638def get_batches_from_stock_entries(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530639 entries = frappe.get_all(
640 "Stock Entry",
641 filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
642 fields=["name"],
643 )
644
645 return frappe.get_all(
646 "Stock Entry Detail",
647 fields=["batch_no", "qty", "serial_and_batch_bundle"],
648 filters={
649 "parent": ("in", [d.name for d in entries]),
650 "is_finished_item": 1,
Rohit Waghchaure46704642023-03-23 11:41:20 +0530651 "item_code": item_code,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530652 },
653 )
654
655
656def set_batch_details_from_package(ids, batches):
657 entries = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530658 "Serial and Batch Entry",
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530659 filters={"parent": ("in", ids), "is_outward": 0},
660 fields=["batch_no", "qty"],
661 )
662
663 for d in entries:
664 batches[d.batch_no] -= d.qty
Rohit Waghchaure46704642023-03-23 11:41:20 +0530665
666
667class SerialBatchCreation:
668 def __init__(self, args):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530669 self.set(args)
670 self.set_item_details()
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530671 self.set_other_details()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530672
673 def set(self, args):
674 self.__dict__ = {}
Rohit Waghchaure46704642023-03-23 11:41:20 +0530675 for key, value in args.items():
676 setattr(self, key, value)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530677 self.__dict__[key] = value
678
679 def get(self, key):
680 return self.__dict__.get(key)
681
682 def set_item_details(self):
683 fields = [
684 "has_batch_no",
685 "has_serial_no",
686 "item_name",
687 "item_group",
688 "serial_no_series",
689 "create_new_batch",
690 "batch_number_series",
691 "description",
692 ]
693
694 item_details = frappe.get_cached_value("Item", self.item_code, fields, as_dict=1)
695 for key, value in item_details.items():
696 setattr(self, key, value)
697
698 self.__dict__.update(item_details)
Rohit Waghchaure46704642023-03-23 11:41:20 +0530699
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530700 def set_other_details(self):
701 if not self.get("posting_date"):
702 setattr(self, "posting_date", today())
703 self.__dict__["posting_date"] = self.posting_date
704
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530705 if not self.get("actual_qty"):
706 qty = self.get("qty") or self.get("total_qty")
707
708 setattr(self, "actual_qty", qty)
709 self.__dict__["actual_qty"] = self.actual_qty
710
Rohit Waghchaure46704642023-03-23 11:41:20 +0530711 def duplicate_package(self):
712 if not self.serial_and_batch_bundle:
713 return
714
715 id = self.serial_and_batch_bundle
716 package = frappe.get_doc("Serial and Batch Bundle", id)
717 new_package = frappe.copy_doc(package)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530718
719 if self.get("returned_serial_nos"):
720 self.remove_returned_serial_nos(new_package)
721
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530722 new_package.docstatus = 0
Rohit Waghchaure46704642023-03-23 11:41:20 +0530723 new_package.type_of_transaction = self.type_of_transaction
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530724 new_package.returned_against = self.get("returned_against")
Rohit Waghchaure46704642023-03-23 11:41:20 +0530725 new_package.save()
726
727 self.serial_and_batch_bundle = new_package.name
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530728
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530729 def remove_returned_serial_nos(self, package):
730 remove_list = []
731 for d in package.entries:
732 if d.serial_no in self.returned_serial_nos:
733 remove_list.append(d)
734
735 for d in remove_list:
736 package.remove(d)
737
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530738 def make_serial_and_batch_bundle(self):
739 doc = frappe.new_doc("Serial and Batch Bundle")
740 valid_columns = doc.meta.get_valid_columns()
741 for key, value in self.__dict__.items():
742 if key in valid_columns:
743 doc.set(key, value)
744
745 if self.type_of_transaction == "Outward":
746 self.set_auto_serial_batch_entries_for_outward()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530747 elif self.type_of_transaction == "Inward":
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530748 self.set_auto_serial_batch_entries_for_inward()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530749 self.add_serial_nos_for_batch_item()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530750
751 self.set_serial_batch_entries(doc)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530752 if not doc.get("entries"):
753 return frappe._dict({})
754
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530755 doc.save()
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530756 self.validate_qty(doc)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530757
758 if not hasattr(self, "do_not_submit") or not self.do_not_submit:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530759 doc.flags.ignore_voucher_validation = True
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530760 doc.submit()
761
762 return doc
763
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530764 def add_serial_nos_for_batch_item(self):
765 if not (self.has_serial_no and self.has_batch_no):
766 return
767
768 if not self.get("serial_nos") and self.get("batches"):
769 batches = list(self.get("batches").keys())
770 if len(batches) == 1:
771 self.batch_no = batches[0]
772 self.serial_nos = self.get_auto_created_serial_nos()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530773
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530774 def update_serial_and_batch_entries(self):
775 doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
776 doc.type_of_transaction = self.type_of_transaction
777 doc.set("entries", [])
778 self.set_auto_serial_batch_entries_for_outward()
779 self.set_serial_batch_entries(doc)
780 if not doc.get("entries"):
781 return frappe._dict({})
782
783 doc.save()
784 return doc
785
Rohit Waghchauredcb04622023-06-05 16:56:29 +0530786 def validate_qty(self, doc):
787 if doc.type_of_transaction == "Outward":
788 precision = doc.precision("total_qty")
789
790 total_qty = abs(flt(doc.total_qty, precision))
791 required_qty = abs(flt(self.actual_qty, precision))
792
793 if required_qty - total_qty > 0:
794 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."
795 frappe.throw(msg, title=_("Insufficient Stock"))
796
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530797 def set_auto_serial_batch_entries_for_outward(self):
798 from erpnext.stock.doctype.batch.batch import get_available_batches
799 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
800
801 kwargs = frappe._dict(
802 {
803 "item_code": self.item_code,
804 "warehouse": self.warehouse,
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530805 "qty": abs(self.actual_qty) if self.actual_qty else 0,
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530806 "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
807 }
808 )
809
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530810 if self.get("ignore_serial_nos"):
811 kwargs["ignore_serial_nos"] = self.ignore_serial_nos
812
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530813 if self.has_serial_no and not self.get("serial_nos"):
814 self.serial_nos = get_serial_nos_for_outward(kwargs)
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530815 elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530816 self.batches = get_available_batches(kwargs)
817
818 def set_auto_serial_batch_entries_for_inward(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530819 if (self.get("batches") and self.has_batch_no) or (
820 self.get("serial_nos") and self.has_serial_no
821 ):
822 return
823
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530824 self.batch_no = None
825 if self.has_batch_no:
826 self.batch_no = self.create_batch()
827
828 if self.has_serial_no:
829 self.serial_nos = self.get_auto_created_serial_nos()
830 else:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530831 self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530832
833 def set_serial_batch_entries(self, doc):
834 if self.get("serial_nos"):
835 serial_no_wise_batch = frappe._dict({})
836 if self.has_batch_no:
837 serial_no_wise_batch = self.get_serial_nos_batch(self.serial_nos)
838
839 qty = -1 if self.type_of_transaction == "Outward" else 1
840 for serial_no in self.serial_nos:
841 doc.append(
842 "entries",
843 {
844 "serial_no": serial_no,
845 "qty": qty,
846 "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
847 "incoming_rate": self.get("incoming_rate"),
848 },
849 )
850
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530851 elif self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530852 for batch_no, batch_qty in self.batches.items():
853 doc.append(
854 "entries",
855 {
856 "batch_no": batch_no,
857 "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
858 "incoming_rate": self.get("incoming_rate"),
859 },
860 )
861
862 def get_serial_nos_batch(self, serial_nos):
863 return frappe._dict(
864 frappe.get_all(
865 "Serial No",
866 fields=["name", "batch_no"],
867 filters={"name": ("in", serial_nos)},
868 as_list=1,
869 )
870 )
871
872 def create_batch(self):
873 from erpnext.stock.doctype.batch.batch import make_batch
874
875 return make_batch(
876 frappe._dict(
877 {
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530878 "item": self.get("item_code"),
879 "reference_doctype": self.get("voucher_type"),
880 "reference_name": self.get("voucher_no"),
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530881 }
882 )
883 )
884
885 def get_auto_created_serial_nos(self):
886 sr_nos = []
887 serial_nos_details = []
888
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530889 if not self.serial_no_series:
890 msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually."
891 frappe.throw(_(msg))
892
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530893 for i in range(abs(cint(self.actual_qty))):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530894 serial_no = make_autoname(self.serial_no_series, "Serial No")
895 sr_nos.append(serial_no)
896 serial_nos_details.append(
897 (
898 serial_no,
899 serial_no,
900 now(),
901 now(),
902 frappe.session.user,
903 frappe.session.user,
904 self.warehouse,
905 self.company,
906 self.item_code,
907 self.item_name,
908 self.description,
909 "Active",
910 self.batch_no,
911 )
912 )
913
914 if serial_nos_details:
915 fields = [
916 "name",
917 "serial_no",
918 "creation",
919 "modified",
920 "owner",
921 "modified_by",
922 "warehouse",
923 "company",
924 "item_code",
925 "item_name",
926 "description",
927 "status",
928 "batch_no",
929 ]
930
931 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
932
933 return sr_nos
934
935
936def get_serial_or_batch_items(items):
937 serial_or_batch_items = frappe.get_all(
938 "Item",
939 filters={"name": ("in", [d.item_code for d in items])},
940 or_filters={"has_serial_no": 1, "has_batch_no": 1},
941 )
942
943 if not serial_or_batch_items:
944 return
945 else:
946 serial_or_batch_items = [d.name for d in serial_or_batch_items]
947
948 return serial_or_batch_items