blob: 9c55358da25deff69c7298d409e7dd14ba680384 [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 Waghchaured3ceb072023-03-31 09:03:54 +05308from frappe.utils import cint, flt, 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):
82 precision = sn_doc.precision("total_qty")
83 if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision):
84 msg = f"Total qty {flt(sn_doc.total_qty, precision)} of Serial and Batch Bundle {sn_doc.name} is not equal to Actual Qty {flt(self.sle.actual_qty, precision)} in the {self.sle.voucher_type} {self.sle.voucher_no}"
85 frappe.throw(_(msg))
86
Rohit Waghchaure648efca2023-03-28 12:16:27 +053087 def validate_item(self):
88 msg = ""
89 if self.sle.actual_qty > 0:
90 if not self.item_details.has_batch_no and not self.item_details.has_serial_no:
91 msg = f"Item {self.item_code} is not a batch or serial no item"
92
93 if self.item_details.has_serial_no and not self.item_details.serial_no_series:
94 msg += f". If you want auto pick serial bundle, then kindly set Serial No Series in Item {self.item_code}"
95
96 if (
97 self.item_details.has_batch_no
98 and not self.item_details.batch_number_series
99 and not frappe.db.get_single_value("Stock Settings", "naming_series_prefix")
100 ):
101 msg += f". If you want auto pick batch bundle, then kindly set Batch Number Series in Item {self.item_code}"
102
103 elif self.sle.actual_qty < 0:
104 if not frappe.db.get_single_value(
105 "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
106 ):
107 msg += ". If you want auto pick serial/batch bundle, then kindly enable 'Auto Create Serial and Batch Bundle' in Stock Settings."
108
109 if msg:
110 error_msg = (
111 f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}."
112 + msg
113 )
114 frappe.throw(_(error_msg))
115
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530116 def set_serial_and_batch_bundle(self, sn_doc):
117 self.sle.db_set("serial_and_batch_bundle", sn_doc.name)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530118
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530119 if sn_doc.is_rejected:
120 frappe.db.set_value(
121 self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name
122 )
123 else:
124 frappe.db.set_value(
125 self.child_doctype, self.sle.voucher_detail_no, "serial_and_batch_bundle", sn_doc.name
126 )
127
128 @property
129 def child_doctype(self):
130 child_doctype = self.sle.voucher_type + " Item"
131 if self.sle.voucher_type == "Stock Entry":
132 child_doctype = "Stock Entry Detail"
133
Rohit Waghchaure26b39ac2023-04-06 01:36:18 +0530134 if self.sle.voucher_type == "Asset Capitalization":
135 child_doctype = "Asset Capitalization Stock Item"
136
137 if self.sle.voucher_type == "Asset Repair":
138 child_doctype = "Asset Repair Consumed Item"
139
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530140 return child_doctype
141
142 def is_rejected_entry(self):
143 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
144
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530145 def process_batch_no(self):
146 if (
147 not self.sle.is_cancelled
148 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530149 and self.item_details.has_batch_no == 1
150 and self.item_details.create_new_batch
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530151 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530152 self.make_serial_batch_no_bundle()
153 elif not self.sle.is_cancelled:
154 self.validate_item_and_warehouse()
155
156 def validate_item_and_warehouse(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530157 if self.sle.serial_and_batch_bundle and not frappe.db.exists(
158 "Serial and Batch Bundle",
159 {
160 "name": self.sle.serial_and_batch_bundle,
161 "item_code": self.item_code,
162 "warehouse": self.warehouse,
163 "voucher_no": self.sle.voucher_no,
164 },
165 ):
166 msg = f"""
167 The Serial and Batch Bundle
168 {bold(self.sle.serial_and_batch_bundle)}
169 does not belong to Item {bold(self.item_code)}
170 or Warehouse {bold(self.warehouse)}
171 or {self.sle.voucher_type} no {bold(self.sle.voucher_no)}
172 """
173
174 frappe.throw(_(msg))
175
176 def delink_serial_and_batch_bundle(self):
177 update_values = {
178 "serial_and_batch_bundle": "",
179 }
180
181 if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse):
182 update_values["rejected_serial_and_batch_bundle"] = ""
183
184 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values)
185
186 frappe.db.set_value(
187 "Serial and Batch Bundle",
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530188 {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530189 {"is_cancelled": 1, "voucher_no": ""},
190 )
191
Rohit Waghchauref79f2a32023-04-04 11:50:38 +0530192 if self.sle.serial_and_batch_bundle:
193 frappe.get_cached_doc(
194 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle
195 ).validate_serial_and_batch_inventory()
196
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530197 def post_process(self):
Rohit Waghchaure674bd3e2023-03-17 16:42:59 +0530198 if not self.sle.serial_and_batch_bundle:
199 return
200
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530201 docstatus = frappe.get_cached_value(
202 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
203 )
204
205 if docstatus != 1:
206 self.submit_serial_and_batch_bundle()
207
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530208 if self.item_details.has_serial_no == 1:
209 self.set_warehouse_and_status_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530210
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530211 if (
212 self.sle.actual_qty > 0
213 and self.item_details.has_serial_no == 1
214 and self.item_details.has_batch_no == 1
215 ):
216 self.set_batch_no_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530217
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530218 if self.item_details.has_batch_no == 1:
219 self.update_batch_qty()
220
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530221 def submit_serial_and_batch_bundle(self):
222 doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530223 self.validate_actual_qty(doc)
224
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530225 doc.flags.ignore_voucher_validation = True
226 doc.submit()
227
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530228 def set_warehouse_and_status_in_serial_nos(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530229 serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530230 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
231
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530232 if not serial_nos:
233 return
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530234
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530235 sn_table = frappe.qb.DocType("Serial No")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530236 (
237 frappe.qb.update(sn_table)
238 .set(sn_table.warehouse, warehouse)
239 .set(sn_table.status, "Active" if warehouse else "Inactive")
240 .where(sn_table.name.isin(serial_nos))
241 ).run()
242
243 def set_batch_no_in_serial_nos(self):
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530244 entries = frappe.get_all(
245 "Serial and Batch Entry",
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530246 fields=["serial_no", "batch_no"],
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530247 filters={"parent": self.sle.serial_and_batch_bundle},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530248 )
249
250 batch_serial_nos = {}
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530251 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530252 batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no)
253
254 for batch_no, serial_nos in batch_serial_nos.items():
255 sn_table = frappe.qb.DocType("Serial No")
256 (
257 frappe.qb.update(sn_table)
258 .set(sn_table.batch_no, batch_no)
259 .where(sn_table.name.isin(serial_nos))
260 ).run()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530261
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530262 def update_batch_qty(self):
263 from erpnext.stock.doctype.batch.batch import get_available_batches
264
265 batches = get_batch_nos(self.sle.serial_and_batch_bundle)
266
267 batches_qty = get_available_batches(
268 frappe._dict(
269 {"item_code": self.item_code, "warehouse": self.warehouse, "batch_no": list(batches.keys())}
270 )
271 )
272
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530273 for batch_no in batches:
274 frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0))
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530275
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530276
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530277def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530278 if not serial_and_batch_bundle:
279 return []
280
281 filters = {"parent": serial_and_batch_bundle, "serial_no": ("is", "set")}
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530282 if isinstance(serial_and_batch_bundle, list):
283 filters = {"parent": ("in", serial_and_batch_bundle)}
284
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530285 if serial_nos:
286 filters["serial_no"] = ("in", serial_nos)
287
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530288 entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters)
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530289 if not entries:
290 return []
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530291
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530292 return [d.serial_no for d in entries if d.serial_no]
293
294
295def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
296 return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530297
298
Rohit Waghchaurebb954512023-06-02 00:11:43 +0530299def get_serial_or_batch_nos(bundle):
300 return frappe.get_all("Serial and Batch Entry", fields=["*"], filters={"parent": bundle})
301
302
Rohit Waghchaure46704642023-03-23 11:41:20 +0530303class SerialNoValuation(DeprecatedSerialNoValuation):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530304 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530305 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530306 setattr(self, key, value)
307
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530308 self.calculate_stock_value_change()
309 self.calculate_valuation_rate()
310
311 def calculate_stock_value_change(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530312 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530313 self.stock_value_change = frappe.get_cached_value(
314 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
315 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530316
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530317 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530318 entries = self.get_serial_no_ledgers()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530319
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530320 self.serial_no_incoming_rate = defaultdict(float)
321 self.stock_value_change = 0.0
322
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530323 for ledger in entries:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530324 self.stock_value_change += ledger.incoming_rate
325 self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530326
327 self.calculate_stock_value_from_deprecarated_ledgers()
328
329 def get_serial_no_ledgers(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530330 serial_nos = self.get_serial_nos()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530331 bundle = frappe.qb.DocType("Serial and Batch Bundle")
332 bundle_child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530333
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530334 query = (
335 frappe.qb.from_(bundle)
336 .inner_join(bundle_child)
337 .on(bundle.name == bundle_child.parent)
338 .select(
339 bundle.name,
340 bundle_child.serial_no,
341 (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"),
342 )
343 .where(
344 (bundle.is_cancelled == 0)
345 & (bundle.docstatus == 1)
346 & (bundle_child.serial_no.isin(serial_nos))
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530347 & (bundle.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530348 & (bundle.item_code == self.sle.item_code)
349 & (bundle_child.warehouse == self.sle.warehouse)
350 )
351 .orderby(bundle.posting_date, bundle.posting_time, bundle.creation)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530352 )
353
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530354 # Important to exclude the current voucher
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530355 if self.sle.voucher_no:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530356 query = query.where(bundle.voucher_no != self.sle.voucher_no)
357
358 if self.sle.posting_date:
359 if self.sle.posting_time is None:
360 self.sle.posting_time = nowtime()
361
362 timestamp_condition = CombineDatetime(
363 bundle.posting_date, bundle.posting_time
364 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
365
366 query = query.where(timestamp_condition)
367
368 return query.run(as_dict=True)
369
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530370 def get_serial_nos(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530371 if self.sle.get("serial_nos"):
372 return self.sle.serial_nos
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530373
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530374 return get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530375
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530376 def calculate_valuation_rate(self):
377 if not hasattr(self, "wh_data"):
378 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530379
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530380 new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530381
382 if new_stock_qty > 0:
383 new_stock_value = (
384 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530385 ) + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530386 if new_stock_value >= 0:
387 # calculate new valuation rate only if stock value is positive
388 # else it remains the same as that of previous entry
389 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
390
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530391 if (
392 not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry()
393 ):
394 allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate(
395 self.sle.voucher_type, self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530396 )
397 if not allow_zero_rate:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530398 self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530399
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530400 self.wh_data.qty_after_transaction += self.sle.actual_qty
401 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
402 self.wh_data.valuation_rate
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530403 )
404
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530405 def is_rejected_entry(self):
406 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530407
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530408 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530409 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530410
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530411 def get_incoming_rate_of_serial_no(self, serial_no):
412 return self.serial_no_incoming_rate.get(serial_no, 0.0)
413
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530414
415def is_rejected(voucher_type, voucher_detail_no, warehouse):
416 if voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
417 return warehouse == frappe.get_cached_value(
418 voucher_type + " Item", voucher_detail_no, "rejected_warehouse"
419 )
420
421 return False
422
423
Rohit Waghchaure46704642023-03-23 11:41:20 +0530424class BatchNoValuation(DeprecatedBatchNoValuation):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530425 def __init__(self, **kwargs):
426 for key, value in kwargs.items():
427 setattr(self, key, value)
428
429 self.batch_nos = self.get_batch_nos()
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530430 self.prepare_batches()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530431 self.calculate_avg_rate()
432 self.calculate_valuation_rate()
433
434 def calculate_avg_rate(self):
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530435 if flt(self.sle.actual_qty) > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530436 self.stock_value_change = frappe.get_cached_value(
437 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530438 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530439 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530440 entries = self.get_batch_no_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530441 self.stock_value_change = 0.0
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530442 self.batch_avg_rate = defaultdict(float)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530443 self.available_qty = defaultdict(float)
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530444 self.stock_value_differece = defaultdict(float)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530445
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530446 for ledger in entries:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530447 self.stock_value_differece[ledger.batch_no] += flt(ledger.incoming_rate)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530448 self.available_qty[ledger.batch_no] += flt(ledger.qty)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530449
450 self.calculate_avg_rate_from_deprecarated_ledgers()
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530451 self.calculate_avg_rate_for_non_batchwise_valuation()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530452 self.set_stock_value_difference()
453
454 def get_batch_no_ledgers(self) -> List[dict]:
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530455 if not self.batchwise_valuation_batches:
456 return []
457
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530458 parent = frappe.qb.DocType("Serial and Batch Bundle")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530459 child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530460
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530461 timestamp_condition = ""
462 if self.sle.posting_date and self.sle.posting_time:
463 timestamp_condition = CombineDatetime(
464 parent.posting_date, parent.posting_time
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530465 ) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530466
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530467 query = (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530468 frappe.qb.from_(parent)
469 .inner_join(child)
470 .on(parent.name == child.parent)
471 .select(
472 child.batch_no,
473 Sum(child.stock_value_difference).as_("incoming_rate"),
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530474 Sum(child.qty).as_("qty"),
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530475 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530476 .where(
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530477 (child.batch_no.isin(self.batchwise_valuation_batches))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530478 & (parent.warehouse == self.sle.warehouse)
479 & (parent.item_code == self.sle.item_code)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530480 & (parent.docstatus == 1)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530481 & (parent.is_cancelled == 0)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530482 & (parent.type_of_transaction.isin(["Inward", "Outward"]))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530483 )
484 .groupby(child.batch_no)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530485 )
486
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530487 # Important to exclude the current voucher
488 if self.sle.voucher_no:
489 query = query.where(parent.voucher_no != self.sle.voucher_no)
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530490
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530491 if timestamp_condition:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530492 query = query.where(timestamp_condition)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530493
494 return query.run(as_dict=True)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530495
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530496 def prepare_batches(self):
497 self.batches = self.batch_nos
498 if isinstance(self.batch_nos, dict):
499 self.batches = list(self.batch_nos.keys())
500
501 self.batchwise_valuation_batches = []
502 self.non_batchwise_valuation_batches = []
503
504 batches = frappe.get_all(
505 "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"]
506 )
507
508 for batch in batches:
509 self.batchwise_valuation_batches.append(batch.name)
510
511 self.non_batchwise_valuation_batches = list(
512 set(self.batches) - set(self.batchwise_valuation_batches)
513 )
514
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530515 def get_batch_nos(self) -> list:
516 if self.sle.get("batch_nos"):
517 return self.sle.batch_nos
518
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530519 return get_batch_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530520
521 def set_stock_value_difference(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530522 for batch_no, ledger in self.batch_nos.items():
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530523 if batch_no in self.non_batchwise_valuation_batches:
524 continue
525
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530526 if not self.available_qty[batch_no]:
527 continue
528
Rohit Waghchauref704eb72023-03-30 11:32:39 +0530529 self.batch_avg_rate[batch_no] = (
530 self.stock_value_differece[batch_no] / self.available_qty[batch_no]
531 )
532
533 # New Stock Value Difference
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530534 stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530535 self.stock_value_change += stock_value_change
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530536
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530537 frappe.db.set_value(
Rohit Waghchaure40ab3bd2023-06-01 16:08:49 +0530538 "Serial and Batch Entry",
539 ledger.name,
540 {
541 "stock_value_difference": stock_value_change,
542 "incoming_rate": self.batch_avg_rate[batch_no],
543 },
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530544 )
545
546 def calculate_valuation_rate(self):
547 if not hasattr(self, "wh_data"):
548 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530549
550 self.wh_data.stock_value = round_off_if_near_zero(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530551 self.wh_data.stock_value + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530552 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530553
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530554 self.wh_data.qty_after_transaction += self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530555 if self.wh_data.qty_after_transaction:
556 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
557
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530558 def get_incoming_rate(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530559 if not self.sle.actual_qty:
560 self.sle.actual_qty = self.get_actual_qty()
561
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530562 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530563
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530564 def get_actual_qty(self):
565 total_qty = 0.0
566 for batch_no in self.available_qty:
567 total_qty += self.available_qty[batch_no]
568
569 return total_qty
570
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530571
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530572def get_batch_nos(serial_and_batch_bundle):
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530573 if not serial_and_batch_bundle:
574 return frappe._dict({})
575
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530576 entries = frappe.get_all(
577 "Serial and Batch Entry",
578 fields=["batch_no", "qty", "name"],
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530579 filters={"parent": serial_and_batch_bundle, "batch_no": ("is", "set")},
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530580 order_by="idx",
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530581 )
582
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530583 if not entries:
584 return frappe._dict({})
585
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530586 return {d.batch_no: d for d in entries}
587
588
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530589def get_empty_batches_based_work_order(work_order, item_code):
Rohit Waghchaure46704642023-03-23 11:41:20 +0530590 batches = get_batches_from_work_order(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530591 if not batches:
592 return batches
593
Rohit Waghchaure46704642023-03-23 11:41:20 +0530594 entries = get_batches_from_stock_entries(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530595 if not entries:
596 return batches
597
598 ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
599 if ids:
600 set_batch_details_from_package(ids, batches)
601
602 # Will be deprecated in v16
603 for d in entries:
604 if not d.batch_no:
605 continue
606
607 batches[d.batch_no] -= d.qty
608
609 return batches
610
611
Rohit Waghchaure46704642023-03-23 11:41:20 +0530612def get_batches_from_work_order(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530613 return frappe._dict(
614 frappe.get_all(
Rohit Waghchaure46704642023-03-23 11:41:20 +0530615 "Batch",
616 fields=["name", "qty_to_produce"],
617 filters={"reference_name": work_order, "item": item_code},
618 as_list=1,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530619 )
620 )
621
622
Rohit Waghchaure46704642023-03-23 11:41:20 +0530623def get_batches_from_stock_entries(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530624 entries = frappe.get_all(
625 "Stock Entry",
626 filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
627 fields=["name"],
628 )
629
630 return frappe.get_all(
631 "Stock Entry Detail",
632 fields=["batch_no", "qty", "serial_and_batch_bundle"],
633 filters={
634 "parent": ("in", [d.name for d in entries]),
635 "is_finished_item": 1,
Rohit Waghchaure46704642023-03-23 11:41:20 +0530636 "item_code": item_code,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530637 },
638 )
639
640
641def set_batch_details_from_package(ids, batches):
642 entries = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530643 "Serial and Batch Entry",
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530644 filters={"parent": ("in", ids), "is_outward": 0},
645 fields=["batch_no", "qty"],
646 )
647
648 for d in entries:
649 batches[d.batch_no] -= d.qty
Rohit Waghchaure46704642023-03-23 11:41:20 +0530650
651
652class SerialBatchCreation:
653 def __init__(self, args):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530654 self.set(args)
655 self.set_item_details()
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530656 self.set_other_details()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530657
658 def set(self, args):
659 self.__dict__ = {}
Rohit Waghchaure46704642023-03-23 11:41:20 +0530660 for key, value in args.items():
661 setattr(self, key, value)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530662 self.__dict__[key] = value
663
664 def get(self, key):
665 return self.__dict__.get(key)
666
667 def set_item_details(self):
668 fields = [
669 "has_batch_no",
670 "has_serial_no",
671 "item_name",
672 "item_group",
673 "serial_no_series",
674 "create_new_batch",
675 "batch_number_series",
676 "description",
677 ]
678
679 item_details = frappe.get_cached_value("Item", self.item_code, fields, as_dict=1)
680 for key, value in item_details.items():
681 setattr(self, key, value)
682
683 self.__dict__.update(item_details)
Rohit Waghchaure46704642023-03-23 11:41:20 +0530684
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530685 def set_other_details(self):
686 if not self.get("posting_date"):
687 setattr(self, "posting_date", today())
688 self.__dict__["posting_date"] = self.posting_date
689
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530690 if not self.get("actual_qty"):
691 qty = self.get("qty") or self.get("total_qty")
692
693 setattr(self, "actual_qty", qty)
694 self.__dict__["actual_qty"] = self.actual_qty
695
Rohit Waghchaure46704642023-03-23 11:41:20 +0530696 def duplicate_package(self):
697 if not self.serial_and_batch_bundle:
698 return
699
700 id = self.serial_and_batch_bundle
701 package = frappe.get_doc("Serial and Batch Bundle", id)
702 new_package = frappe.copy_doc(package)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530703
704 if self.get("returned_serial_nos"):
705 self.remove_returned_serial_nos(new_package)
706
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530707 new_package.docstatus = 0
Rohit Waghchaure46704642023-03-23 11:41:20 +0530708 new_package.type_of_transaction = self.type_of_transaction
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530709 new_package.returned_against = self.get("returned_against")
Rohit Waghchaure46704642023-03-23 11:41:20 +0530710 new_package.save()
711
712 self.serial_and_batch_bundle = new_package.name
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530713
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530714 def remove_returned_serial_nos(self, package):
715 remove_list = []
716 for d in package.entries:
717 if d.serial_no in self.returned_serial_nos:
718 remove_list.append(d)
719
720 for d in remove_list:
721 package.remove(d)
722
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530723 def make_serial_and_batch_bundle(self):
724 doc = frappe.new_doc("Serial and Batch Bundle")
725 valid_columns = doc.meta.get_valid_columns()
726 for key, value in self.__dict__.items():
727 if key in valid_columns:
728 doc.set(key, value)
729
730 if self.type_of_transaction == "Outward":
731 self.set_auto_serial_batch_entries_for_outward()
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530732 elif self.type_of_transaction == "Inward":
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530733 self.set_auto_serial_batch_entries_for_inward()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530734 self.add_serial_nos_for_batch_item()
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530735
736 self.set_serial_batch_entries(doc)
Rohit Waghchauref8bf4aa2023-04-02 13:13:42 +0530737 if not doc.get("entries"):
738 return frappe._dict({})
739
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530740 doc.save()
741
742 if not hasattr(self, "do_not_submit") or not self.do_not_submit:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530743 doc.flags.ignore_voucher_validation = True
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530744 doc.submit()
745
746 return doc
747
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530748 def add_serial_nos_for_batch_item(self):
749 if not (self.has_serial_no and self.has_batch_no):
750 return
751
752 if not self.get("serial_nos") and self.get("batches"):
753 batches = list(self.get("batches").keys())
754 if len(batches) == 1:
755 self.batch_no = batches[0]
756 self.serial_nos = self.get_auto_created_serial_nos()
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530757
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530758 def update_serial_and_batch_entries(self):
759 doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
760 doc.type_of_transaction = self.type_of_transaction
761 doc.set("entries", [])
762 self.set_auto_serial_batch_entries_for_outward()
763 self.set_serial_batch_entries(doc)
764 if not doc.get("entries"):
765 return frappe._dict({})
766
767 doc.save()
768 return doc
769
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530770 def set_auto_serial_batch_entries_for_outward(self):
771 from erpnext.stock.doctype.batch.batch import get_available_batches
772 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
773
774 kwargs = frappe._dict(
775 {
776 "item_code": self.item_code,
777 "warehouse": self.warehouse,
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530778 "qty": abs(self.actual_qty) if self.actual_qty else 0,
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530779 "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
780 }
781 )
782
Rohit Waghchaure74ab20f2023-04-03 12:26:12 +0530783 if self.get("ignore_serial_nos"):
784 kwargs["ignore_serial_nos"] = self.ignore_serial_nos
785
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530786 if self.has_serial_no and not self.get("serial_nos"):
787 self.serial_nos = get_serial_nos_for_outward(kwargs)
Rohit Waghchaure9b728452023-03-28 14:03:59 +0530788 elif not self.has_serial_no and self.has_batch_no and not self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530789 self.batches = get_available_batches(kwargs)
790
791 def set_auto_serial_batch_entries_for_inward(self):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530792 if (self.get("batches") and self.has_batch_no) or (
793 self.get("serial_nos") and self.has_serial_no
794 ):
795 return
796
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530797 self.batch_no = None
798 if self.has_batch_no:
799 self.batch_no = self.create_batch()
800
801 if self.has_serial_no:
802 self.serial_nos = self.get_auto_created_serial_nos()
803 else:
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530804 self.batches = frappe._dict({self.batch_no: abs(self.actual_qty)})
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530805
806 def set_serial_batch_entries(self, doc):
807 if self.get("serial_nos"):
808 serial_no_wise_batch = frappe._dict({})
809 if self.has_batch_no:
810 serial_no_wise_batch = self.get_serial_nos_batch(self.serial_nos)
811
812 qty = -1 if self.type_of_transaction == "Outward" else 1
813 for serial_no in self.serial_nos:
814 doc.append(
815 "entries",
816 {
817 "serial_no": serial_no,
818 "qty": qty,
819 "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
820 "incoming_rate": self.get("incoming_rate"),
821 },
822 )
823
Rohit Waghchauree88c5d62023-04-05 20:03:44 +0530824 elif self.get("batches"):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530825 for batch_no, batch_qty in self.batches.items():
826 doc.append(
827 "entries",
828 {
829 "batch_no": batch_no,
830 "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
831 "incoming_rate": self.get("incoming_rate"),
832 },
833 )
834
835 def get_serial_nos_batch(self, serial_nos):
836 return frappe._dict(
837 frappe.get_all(
838 "Serial No",
839 fields=["name", "batch_no"],
840 filters={"name": ("in", serial_nos)},
841 as_list=1,
842 )
843 )
844
845 def create_batch(self):
846 from erpnext.stock.doctype.batch.batch import make_batch
847
848 return make_batch(
849 frappe._dict(
850 {
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530851 "item": self.get("item_code"),
852 "reference_doctype": self.get("voucher_type"),
853 "reference_name": self.get("voucher_no"),
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530854 }
855 )
856 )
857
858 def get_auto_created_serial_nos(self):
859 sr_nos = []
860 serial_nos_details = []
861
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530862 if not self.serial_no_series:
863 msg = f"Please set Serial No Series in the item {self.item_code} or create Serial and Batch Bundle manually."
864 frappe.throw(_(msg))
865
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530866 for i in range(abs(cint(self.actual_qty))):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530867 serial_no = make_autoname(self.serial_no_series, "Serial No")
868 sr_nos.append(serial_no)
869 serial_nos_details.append(
870 (
871 serial_no,
872 serial_no,
873 now(),
874 now(),
875 frappe.session.user,
876 frappe.session.user,
877 self.warehouse,
878 self.company,
879 self.item_code,
880 self.item_name,
881 self.description,
882 "Active",
883 self.batch_no,
884 )
885 )
886
887 if serial_nos_details:
888 fields = [
889 "name",
890 "serial_no",
891 "creation",
892 "modified",
893 "owner",
894 "modified_by",
895 "warehouse",
896 "company",
897 "item_code",
898 "item_name",
899 "description",
900 "status",
901 "batch_no",
902 ]
903
904 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
905
906 return sr_nos
907
908
909def get_serial_or_batch_items(items):
910 serial_or_batch_items = frappe.get_all(
911 "Item",
912 filters={"name": ("in", [d.item_code for d in items])},
913 or_filters={"has_serial_no": 1, "has_batch_no": 1},
914 )
915
916 if not serial_or_batch_items:
917 return
918 else:
919 serial_or_batch_items = [d.name for d in serial_or_batch_items]
920
921 return serial_or_batch_items