blob: 926863eb3c0c2a2647371782afff6fded1accb7a [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 Waghchauree6143ab2023-03-13 14:51:43 +05308from frappe.utils import cint, flt, now
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,
70 "total_qty": self.sle.actual_qty,
71 "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 Waghchaure648efca2023-03-28 12:16:27 +053081 def validate_item(self):
82 msg = ""
83 if self.sle.actual_qty > 0:
84 if not self.item_details.has_batch_no and not self.item_details.has_serial_no:
85 msg = f"Item {self.item_code} is not a batch or serial no item"
86
87 if self.item_details.has_serial_no and not self.item_details.serial_no_series:
88 msg += f". If you want auto pick serial bundle, then kindly set Serial No Series in Item {self.item_code}"
89
90 if (
91 self.item_details.has_batch_no
92 and not self.item_details.batch_number_series
93 and not frappe.db.get_single_value("Stock Settings", "naming_series_prefix")
94 ):
95 msg += f". If you want auto pick batch bundle, then kindly set Batch Number Series in Item {self.item_code}"
96
97 elif self.sle.actual_qty < 0:
98 if not frappe.db.get_single_value(
99 "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"
100 ):
101 msg += ". If you want auto pick serial/batch bundle, then kindly enable 'Auto Create Serial and Batch Bundle' in Stock Settings."
102
103 if msg:
104 error_msg = (
105 f"Serial and Batch Bundle not set for item {self.item_code} in warehouse {self.warehouse}."
106 + msg
107 )
108 frappe.throw(_(error_msg))
109
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530110 def set_serial_and_batch_bundle(self, sn_doc):
111 self.sle.db_set("serial_and_batch_bundle", sn_doc.name)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530112
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530113 if sn_doc.is_rejected:
114 frappe.db.set_value(
115 self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name
116 )
117 else:
118 frappe.db.set_value(
119 self.child_doctype, self.sle.voucher_detail_no, "serial_and_batch_bundle", sn_doc.name
120 )
121
122 @property
123 def child_doctype(self):
124 child_doctype = self.sle.voucher_type + " Item"
125 if self.sle.voucher_type == "Stock Entry":
126 child_doctype = "Stock Entry Detail"
127
128 return child_doctype
129
130 def is_rejected_entry(self):
131 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
132
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530133 def process_batch_no(self):
134 if (
135 not self.sle.is_cancelled
136 and not self.sle.serial_and_batch_bundle
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530137 and self.item_details.has_batch_no == 1
138 and self.item_details.create_new_batch
139 and self.item_details.batch_number_series
140 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530141 self.make_serial_batch_no_bundle()
142 elif not self.sle.is_cancelled:
143 self.validate_item_and_warehouse()
144
145 def validate_item_and_warehouse(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530146 if self.sle.serial_and_batch_bundle and not frappe.db.exists(
147 "Serial and Batch Bundle",
148 {
149 "name": self.sle.serial_and_batch_bundle,
150 "item_code": self.item_code,
151 "warehouse": self.warehouse,
152 "voucher_no": self.sle.voucher_no,
153 },
154 ):
155 msg = f"""
156 The Serial and Batch Bundle
157 {bold(self.sle.serial_and_batch_bundle)}
158 does not belong to Item {bold(self.item_code)}
159 or Warehouse {bold(self.warehouse)}
160 or {self.sle.voucher_type} no {bold(self.sle.voucher_no)}
161 """
162
163 frappe.throw(_(msg))
164
165 def delink_serial_and_batch_bundle(self):
166 update_values = {
167 "serial_and_batch_bundle": "",
168 }
169
170 if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse):
171 update_values["rejected_serial_and_batch_bundle"] = ""
172
173 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values)
174
175 frappe.db.set_value(
176 "Serial and Batch Bundle",
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530177 {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530178 {"is_cancelled": 1, "voucher_no": ""},
179 )
180
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530181 def post_process(self):
Rohit Waghchaure674bd3e2023-03-17 16:42:59 +0530182 if not self.sle.serial_and_batch_bundle:
183 return
184
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530185 if self.item_details.has_serial_no == 1:
186 self.set_warehouse_and_status_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530187
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530188 if (
189 self.sle.actual_qty > 0
190 and self.item_details.has_serial_no == 1
191 and self.item_details.has_batch_no == 1
192 ):
193 self.set_batch_no_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530194
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530195 if self.item_details.has_batch_no == 1:
196 self.update_batch_qty()
197
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530198 def set_warehouse_and_status_in_serial_nos(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530199 serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle, check_outward=False)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530200 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
201
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530202 if not serial_nos:
203 return
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530204
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530205 sn_table = frappe.qb.DocType("Serial No")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530206 (
207 frappe.qb.update(sn_table)
208 .set(sn_table.warehouse, warehouse)
209 .set(sn_table.status, "Active" if warehouse else "Inactive")
210 .where(sn_table.name.isin(serial_nos))
211 ).run()
212
213 def set_batch_no_in_serial_nos(self):
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530214 entries = frappe.get_all(
215 "Serial and Batch Entry",
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530216 fields=["serial_no", "batch_no"],
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530217 filters={"parent": self.sle.serial_and_batch_bundle},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530218 )
219
220 batch_serial_nos = {}
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530221 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530222 batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no)
223
224 for batch_no, serial_nos in batch_serial_nos.items():
225 sn_table = frappe.qb.DocType("Serial No")
226 (
227 frappe.qb.update(sn_table)
228 .set(sn_table.batch_no, batch_no)
229 .where(sn_table.name.isin(serial_nos))
230 ).run()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530231
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530232 def update_batch_qty(self):
233 from erpnext.stock.doctype.batch.batch import get_available_batches
234
235 batches = get_batch_nos(self.sle.serial_and_batch_bundle)
236
237 batches_qty = get_available_batches(
238 frappe._dict(
239 {"item_code": self.item_code, "warehouse": self.warehouse, "batch_no": list(batches.keys())}
240 )
241 )
242
243 for batch_no, qty in batches_qty.items():
244 frappe.db.set_value("Batch", batch_no, "batch_qty", qty)
245
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530246
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530247def get_serial_nos(serial_and_batch_bundle, check_outward=True):
248 filters = {"parent": serial_and_batch_bundle}
249 if check_outward:
250 filters["is_outward"] = 1
251
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530252 entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530253
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530254 return [d.serial_no for d in entries]
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530255
256
Rohit Waghchaure46704642023-03-23 11:41:20 +0530257class SerialNoValuation(DeprecatedSerialNoValuation):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530258 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530259 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530260 setattr(self, key, value)
261
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530262 self.calculate_stock_value_change()
263 self.calculate_valuation_rate()
264
265 def calculate_stock_value_change(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530266 if self.sle.actual_qty > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530267 self.stock_value_change = frappe.get_cached_value(
268 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
269 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530270
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530271 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530272 entries = self.get_serial_no_ledgers()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530273
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530274 self.serial_no_incoming_rate = defaultdict(float)
275 self.stock_value_change = 0.0
276
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530277 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530278 self.stock_value_change += ledger.incoming_rate * -1
279 self.serial_no_incoming_rate[ledger.serial_no] = ledger.incoming_rate
280
281 self.calculate_stock_value_from_deprecarated_ledgers()
282
283 def get_serial_no_ledgers(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530284 serial_nos = self.get_serial_nos()
285
286 subquery = f"""
287 SELECT
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530288 MAX(
289 TIMESTAMP(
290 parent.posting_date, parent.posting_time
291 )
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530292 ), child.name, child.serial_no, child.warehouse
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530293 FROM
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530294 `tabSerial and Batch Bundle` as parent,
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530295 `tabSerial and Batch Entry` as child
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530296 WHERE
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530297 parent.name = child.parent
298 AND child.serial_no IN ({', '.join([frappe.db.escape(s) for s in serial_nos])})
299 AND child.is_outward = 0
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530300 AND parent.docstatus = 1
Rohit Waghchaureba6e1442023-03-22 23:21:47 +0530301 AND parent.type_of_transaction != 'Maintenance'
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530302 AND parent.is_cancelled = 0
303 AND child.warehouse = {frappe.db.escape(self.sle.warehouse)}
304 AND parent.item_code = {frappe.db.escape(self.sle.item_code)}
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530305 AND (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530306 parent.posting_date < '{self.sle.posting_date}'
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530307 OR (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530308 parent.posting_date = '{self.sle.posting_date}'
309 AND parent.posting_time <= '{self.sle.posting_time}'
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530310 )
311 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530312 GROUP BY
313 child.serial_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530314 """
315
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530316 return frappe.db.sql(
317 f"""
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530318 SELECT
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530319 ledger.serial_no, ledger.incoming_rate, ledger.warehouse
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530320 FROM
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530321 `tabSerial and Batch Entry` AS ledger,
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530322 ({subquery}) AS SubQuery
323 WHERE
324 ledger.name = SubQuery.name
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530325 AND ledger.serial_no = SubQuery.serial_no
326 AND ledger.warehouse = SubQuery.warehouse
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530327 GROUP BY
328 ledger.serial_no
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530329 Order By
330 ledger.creation
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530331 """,
332 as_dict=1,
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530333 )
334
335 def get_serial_nos(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530336 if self.sle.get("serial_nos"):
337 return self.sle.serial_nos
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530338
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530339 return get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530340
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530341 def calculate_valuation_rate(self):
342 if not hasattr(self, "wh_data"):
343 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530344
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530345 new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530346
347 if new_stock_qty > 0:
348 new_stock_value = (
349 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530350 ) + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530351 if new_stock_value >= 0:
352 # calculate new valuation rate only if stock value is positive
353 # else it remains the same as that of previous entry
354 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
355
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530356 if (
357 not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry()
358 ):
359 allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate(
360 self.sle.voucher_type, self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530361 )
362 if not allow_zero_rate:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530363 self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530364
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530365 self.wh_data.qty_after_transaction += self.sle.actual_qty
366 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
367 self.wh_data.valuation_rate
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530368 )
369
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530370 def is_rejected_entry(self):
371 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530372
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530373 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530374 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530375
376
377def is_rejected(voucher_type, voucher_detail_no, warehouse):
378 if voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
379 return warehouse == frappe.get_cached_value(
380 voucher_type + " Item", voucher_detail_no, "rejected_warehouse"
381 )
382
383 return False
384
385
Rohit Waghchaure46704642023-03-23 11:41:20 +0530386class BatchNoValuation(DeprecatedBatchNoValuation):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530387 def __init__(self, **kwargs):
388 for key, value in kwargs.items():
389 setattr(self, key, value)
390
391 self.batch_nos = self.get_batch_nos()
392 self.calculate_avg_rate()
393 self.calculate_valuation_rate()
394
395 def calculate_avg_rate(self):
396 if self.sle.actual_qty > 0:
397 self.stock_value_change = frappe.get_cached_value(
398 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530399 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530400 else:
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530401 entries = self.get_batch_no_ledgers()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530402
403 self.batch_avg_rate = defaultdict(float)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530404 self.available_qty = defaultdict(float)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530405
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530406 for ledger in entries:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530407 self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530408 self.available_qty[ledger.batch_no] += flt(ledger.qty)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530409
410 self.calculate_avg_rate_from_deprecarated_ledgers()
411 self.set_stock_value_difference()
412
413 def get_batch_no_ledgers(self) -> List[dict]:
414 parent = frappe.qb.DocType("Serial and Batch Bundle")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530415 child = frappe.qb.DocType("Serial and Batch Entry")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530416
417 batch_nos = list(self.batch_nos.keys())
418
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530419 timestamp_condition = ""
420 if self.sle.posting_date and self.sle.posting_time:
421 timestamp_condition = CombineDatetime(
422 parent.posting_date, parent.posting_time
423 ) < CombineDatetime(self.sle.posting_date, self.sle.posting_time)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530424
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530425 query = (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530426 frappe.qb.from_(parent)
427 .inner_join(child)
428 .on(parent.name == child.parent)
429 .select(
430 child.batch_no,
431 Sum(child.stock_value_difference).as_("incoming_rate"),
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530432 Sum(child.qty).as_("qty"),
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530433 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530434 .where(
435 (child.batch_no.isin(batch_nos))
436 & (child.parent != self.sle.serial_and_batch_bundle)
437 & (parent.warehouse == self.sle.warehouse)
438 & (parent.item_code == self.sle.item_code)
Rohit Waghchaure86da3062023-03-20 14:15:34 +0530439 & (parent.docstatus == 1)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530440 & (parent.is_cancelled == 0)
Rohit Waghchaureba6e1442023-03-22 23:21:47 +0530441 & (parent.type_of_transaction != "Maintenance")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530442 )
443 .groupby(child.batch_no)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530444 )
445
446 if timestamp_condition:
447 query.where(timestamp_condition)
448
449 return query.run(as_dict=True)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530450
451 def get_batch_nos(self) -> list:
452 if self.sle.get("batch_nos"):
453 return self.sle.batch_nos
454
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530455 return get_batch_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530456
457 def set_stock_value_difference(self):
458 self.stock_value_change = 0
459 for batch_no, ledger in self.batch_nos.items():
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530460 stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530461 self.stock_value_change += stock_value_change
462 frappe.db.set_value(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530463 "Serial and Batch Entry", ledger.name, "stock_value_difference", stock_value_change
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530464 )
465
466 def calculate_valuation_rate(self):
467 if not hasattr(self, "wh_data"):
468 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530469
470 self.wh_data.stock_value = round_off_if_near_zero(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530471 self.wh_data.stock_value + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530472 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530473
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530474 if self.wh_data.qty_after_transaction:
475 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
476
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530477 self.wh_data.qty_after_transaction += self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530478
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530479 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530480 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530481
482
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530483def get_batch_nos(serial_and_batch_bundle):
484 entries = frappe.get_all(
485 "Serial and Batch Entry",
486 fields=["batch_no", "qty", "name"],
487 filters={"parent": serial_and_batch_bundle, "is_outward": 1},
488 )
489
490 return {d.batch_no: d for d in entries}
491
492
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530493def get_empty_batches_based_work_order(work_order, item_code):
Rohit Waghchaure46704642023-03-23 11:41:20 +0530494 batches = get_batches_from_work_order(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530495 if not batches:
496 return batches
497
Rohit Waghchaure46704642023-03-23 11:41:20 +0530498 entries = get_batches_from_stock_entries(work_order, item_code)
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530499 if not entries:
500 return batches
501
502 ids = [d.serial_and_batch_bundle for d in entries if d.serial_and_batch_bundle]
503 if ids:
504 set_batch_details_from_package(ids, batches)
505
506 # Will be deprecated in v16
507 for d in entries:
508 if not d.batch_no:
509 continue
510
511 batches[d.batch_no] -= d.qty
512
513 return batches
514
515
Rohit Waghchaure46704642023-03-23 11:41:20 +0530516def get_batches_from_work_order(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530517 return frappe._dict(
518 frappe.get_all(
Rohit Waghchaure46704642023-03-23 11:41:20 +0530519 "Batch",
520 fields=["name", "qty_to_produce"],
521 filters={"reference_name": work_order, "item": item_code},
522 as_list=1,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530523 )
524 )
525
526
Rohit Waghchaure46704642023-03-23 11:41:20 +0530527def get_batches_from_stock_entries(work_order, item_code):
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530528 entries = frappe.get_all(
529 "Stock Entry",
530 filters={"work_order": work_order, "docstatus": 1, "purpose": "Manufacture"},
531 fields=["name"],
532 )
533
534 return frappe.get_all(
535 "Stock Entry Detail",
536 fields=["batch_no", "qty", "serial_and_batch_bundle"],
537 filters={
538 "parent": ("in", [d.name for d in entries]),
539 "is_finished_item": 1,
Rohit Waghchaure46704642023-03-23 11:41:20 +0530540 "item_code": item_code,
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530541 },
542 )
543
544
545def set_batch_details_from_package(ids, batches):
546 entries = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +0530547 "Serial and Batch Entry",
Rohit Waghchaure16f26fb2023-03-20 22:56:06 +0530548 filters={"parent": ("in", ids), "is_outward": 0},
549 fields=["batch_no", "qty"],
550 )
551
552 for d in entries:
553 batches[d.batch_no] -= d.qty
Rohit Waghchaure46704642023-03-23 11:41:20 +0530554
555
556class SerialBatchCreation:
557 def __init__(self, args):
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530558 self.set(args)
559 self.set_item_details()
560
561 def set(self, args):
562 self.__dict__ = {}
Rohit Waghchaure46704642023-03-23 11:41:20 +0530563 for key, value in args.items():
564 setattr(self, key, value)
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530565 self.__dict__[key] = value
566
567 def get(self, key):
568 return self.__dict__.get(key)
569
570 def set_item_details(self):
571 fields = [
572 "has_batch_no",
573 "has_serial_no",
574 "item_name",
575 "item_group",
576 "serial_no_series",
577 "create_new_batch",
578 "batch_number_series",
579 "description",
580 ]
581
582 item_details = frappe.get_cached_value("Item", self.item_code, fields, as_dict=1)
583 for key, value in item_details.items():
584 setattr(self, key, value)
585
586 self.__dict__.update(item_details)
Rohit Waghchaure46704642023-03-23 11:41:20 +0530587
588 def duplicate_package(self):
589 if not self.serial_and_batch_bundle:
590 return
591
592 id = self.serial_and_batch_bundle
593 package = frappe.get_doc("Serial and Batch Bundle", id)
594 new_package = frappe.copy_doc(package)
595 new_package.type_of_transaction = self.type_of_transaction
Rohit Waghchaure0eaf6de2023-03-23 15:13:45 +0530596 new_package.returned_against = self.returned_against
Rohit Waghchaure46704642023-03-23 11:41:20 +0530597 new_package.save()
598
599 self.serial_and_batch_bundle = new_package.name
Rohit Waghchaure648efca2023-03-28 12:16:27 +0530600
601 def make_serial_and_batch_bundle(self):
602 doc = frappe.new_doc("Serial and Batch Bundle")
603 valid_columns = doc.meta.get_valid_columns()
604 for key, value in self.__dict__.items():
605 if key in valid_columns:
606 doc.set(key, value)
607
608 if self.type_of_transaction == "Outward":
609 self.set_auto_serial_batch_entries_for_outward()
610 elif self.type_of_transaction == "Inward":
611 self.set_auto_serial_batch_entries_for_inward()
612
613 self.set_serial_batch_entries(doc)
614 doc.save()
615
616 if not hasattr(self, "do_not_submit") or not self.do_not_submit:
617 doc.submit()
618
619 return doc
620
621 def set_auto_serial_batch_entries_for_outward(self):
622 from erpnext.stock.doctype.batch.batch import get_available_batches
623 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
624
625 kwargs = frappe._dict(
626 {
627 "item_code": self.item_code,
628 "warehouse": self.warehouse,
629 "qty": abs(self.total_qty),
630 "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
631 }
632 )
633
634 if self.has_serial_no and not self.get("serial_nos"):
635 self.serial_nos = get_serial_nos_for_outward(kwargs)
636 elif self.has_batch_no and not self.get("batches"):
637 self.batches = get_available_batches(kwargs)
638
639 def set_auto_serial_batch_entries_for_inward(self):
640 self.batch_no = None
641 if self.has_batch_no:
642 self.batch_no = self.create_batch()
643
644 if self.has_serial_no:
645 self.serial_nos = self.get_auto_created_serial_nos()
646 else:
647 self.batches = frappe._dict({self.batch_no: abs(self.total_qty)})
648
649 def set_serial_batch_entries(self, doc):
650 if self.get("serial_nos"):
651 serial_no_wise_batch = frappe._dict({})
652 if self.has_batch_no:
653 serial_no_wise_batch = self.get_serial_nos_batch(self.serial_nos)
654
655 qty = -1 if self.type_of_transaction == "Outward" else 1
656 for serial_no in self.serial_nos:
657 doc.append(
658 "entries",
659 {
660 "serial_no": serial_no,
661 "qty": qty,
662 "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
663 "incoming_rate": self.get("incoming_rate"),
664 },
665 )
666
667 if self.get("batches"):
668 for batch_no, batch_qty in self.batches.items():
669 doc.append(
670 "entries",
671 {
672 "batch_no": batch_no,
673 "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
674 "incoming_rate": self.get("incoming_rate"),
675 },
676 )
677
678 def get_serial_nos_batch(self, serial_nos):
679 return frappe._dict(
680 frappe.get_all(
681 "Serial No",
682 fields=["name", "batch_no"],
683 filters={"name": ("in", serial_nos)},
684 as_list=1,
685 )
686 )
687
688 def create_batch(self):
689 from erpnext.stock.doctype.batch.batch import make_batch
690
691 return make_batch(
692 frappe._dict(
693 {
694 "item": self.item_code,
695 "reference_doctype": self.voucher_type,
696 "reference_name": self.voucher_no,
697 }
698 )
699 )
700
701 def get_auto_created_serial_nos(self):
702 sr_nos = []
703 serial_nos_details = []
704
705 for i in range(abs(cint(self.total_qty))):
706 serial_no = make_autoname(self.serial_no_series, "Serial No")
707 sr_nos.append(serial_no)
708 serial_nos_details.append(
709 (
710 serial_no,
711 serial_no,
712 now(),
713 now(),
714 frappe.session.user,
715 frappe.session.user,
716 self.warehouse,
717 self.company,
718 self.item_code,
719 self.item_name,
720 self.description,
721 "Active",
722 self.batch_no,
723 )
724 )
725
726 if serial_nos_details:
727 fields = [
728 "name",
729 "serial_no",
730 "creation",
731 "modified",
732 "owner",
733 "modified_by",
734 "warehouse",
735 "company",
736 "item_code",
737 "item_name",
738 "description",
739 "status",
740 "batch_no",
741 ]
742
743 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
744
745 return sr_nos
746
747
748def get_serial_or_batch_items(items):
749 serial_or_batch_items = frappe.get_all(
750 "Item",
751 filters={"name": ("in", [d.item_code for d in items])},
752 or_filters={"has_serial_no": 1, "has_batch_no": 1},
753 )
754
755 if not serial_or_batch_items:
756 return
757 else:
758 serial_or_batch_items = [d.name for d in serial_or_batch_items]
759
760 return serial_or_batch_items