blob: 7c4f062038f185c4c729e12f0d07c3bbb21146f7 [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
7from frappe.query_builder.functions import Sum
8from 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
52 and self.sle.actual_qty > 0
53 and self.item_details.has_serial_no == 1
54 and self.item_details.serial_no_series
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053055 and self.allow_to_make_auto_bundle()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053056 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053057 self.make_serial_batch_no_bundle()
58 elif not self.sle.is_cancelled:
59 self.validate_item_and_warehouse()
Rohit Waghchauref1b59662023-03-06 12:08:28 +053060
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053061 def auto_create_serial_nos(self, batch_no=None):
Rohit Waghchauref1b59662023-03-06 12:08:28 +053062 sr_nos = []
63 serial_nos_details = []
64
65 for i in range(cint(self.sle.actual_qty)):
66 serial_no = make_autoname(self.item_details.serial_no_series, "Serial No")
67 sr_nos.append(serial_no)
68 serial_nos_details.append(
69 (
70 serial_no,
71 serial_no,
72 now(),
73 now(),
74 frappe.session.user,
75 frappe.session.user,
76 self.warehouse,
77 self.company,
78 self.item_code,
79 self.item_details.item_name,
80 self.item_details.description,
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053081 "Active",
82 batch_no,
Rohit Waghchauref1b59662023-03-06 12:08:28 +053083 )
84 )
85
86 if serial_nos_details:
87 fields = [
88 "name",
89 "serial_no",
90 "creation",
91 "modified",
92 "owner",
93 "modified_by",
94 "warehouse",
95 "company",
96 "item_code",
97 "item_name",
98 "description",
Rohit Waghchauree6143ab2023-03-13 14:51:43 +053099 "status",
100 "batch_no",
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530101 ]
102
103 frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
104
105 return sr_nos
106
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530107 def make_serial_batch_no_bundle(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530108 sn_doc = frappe.new_doc("Serial and Batch Bundle")
109 sn_doc.item_code = self.item_code
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530110 sn_doc.warehouse = self.warehouse
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530111 sn_doc.item_name = self.item_details.item_name
112 sn_doc.item_group = self.item_details.item_group
113 sn_doc.has_serial_no = self.item_details.has_serial_no
114 sn_doc.has_batch_no = self.item_details.has_batch_no
115 sn_doc.voucher_type = self.sle.voucher_type
116 sn_doc.voucher_no = self.sle.voucher_no
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530117 sn_doc.voucher_detail_no = self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530118 sn_doc.total_qty = self.sle.actual_qty
119 sn_doc.avg_rate = self.sle.incoming_rate
120 sn_doc.total_amount = flt(self.sle.actual_qty) * flt(self.sle.incoming_rate)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530121 sn_doc.type_of_transaction = "Inward"
122 sn_doc.posting_date = self.sle.posting_date
123 sn_doc.posting_time = self.sle.posting_time
124 sn_doc.is_rejected = self.is_rejected_entry()
125
126 sn_doc.flags.ignore_mandatory = True
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530127 sn_doc.insert()
128
129 batch_no = ""
130 if self.item_details.has_batch_no:
131 batch_no = self.create_batch()
132
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530133 incoming_rate = self.sle.incoming_rate
134 if not incoming_rate:
135 incoming_rate = frappe.get_cached_value(
136 self.child_doctype, self.sle.voucher_detail_no, "valuation_rate"
137 )
138
139 if self.item_details.has_serial_no:
140 sr_nos = self.auto_create_serial_nos(batch_no)
141 self.add_serial_no_to_bundle(sn_doc, sr_nos, incoming_rate, batch_no)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530142 elif self.item_details.has_batch_no:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530143 self.add_batch_no_to_bundle(sn_doc, batch_no, incoming_rate)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530144 sn_doc.save()
145
146 sn_doc.load_from_db()
147 sn_doc.flags.ignore_validate = True
148 sn_doc.flags.ignore_mandatory = True
149
150 sn_doc.submit()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530151 self.set_serial_and_batch_bundle(sn_doc)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530152
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530153 def set_serial_and_batch_bundle(self, sn_doc):
154 self.sle.db_set("serial_and_batch_bundle", sn_doc.name)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530155
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530156 if sn_doc.is_rejected:
157 frappe.db.set_value(
158 self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name
159 )
160 else:
161 frappe.db.set_value(
162 self.child_doctype, self.sle.voucher_detail_no, "serial_and_batch_bundle", sn_doc.name
163 )
164
165 @property
166 def child_doctype(self):
167 child_doctype = self.sle.voucher_type + " Item"
168 if self.sle.voucher_type == "Stock Entry":
169 child_doctype = "Stock Entry Detail"
170
171 return child_doctype
172
173 def is_rejected_entry(self):
174 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
175
176 def add_serial_no_to_bundle(self, sn_doc, serial_nos, incoming_rate, batch_no=None):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530177 ledgers = []
178
179 fields = [
180 "name",
181 "serial_no",
182 "batch_no",
183 "warehouse",
184 "item_code",
185 "qty",
186 "incoming_rate",
187 "parent",
188 "parenttype",
189 "parentfield",
190 ]
191
192 for serial_no in serial_nos:
193 ledgers.append(
194 (
195 frappe.generate_hash("Serial and Batch Ledger", 10),
196 serial_no,
197 batch_no,
198 self.warehouse,
199 self.item_details.item_code,
200 1,
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530201 incoming_rate,
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530202 sn_doc.name,
203 sn_doc.doctype,
204 "ledgers",
205 )
206 )
207
208 frappe.db.bulk_insert("Serial and Batch Ledger", fields=fields, values=set(ledgers))
209
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530210 def add_batch_no_to_bundle(self, sn_doc, batch_no, incoming_rate):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530211 stock_value_difference = flt(self.sle.actual_qty) * flt(incoming_rate)
212
213 if self.sle.actual_qty < 0:
214 stock_value_difference *= -1
215
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530216 sn_doc.append(
217 "ledgers",
218 {
219 "batch_no": batch_no,
220 "qty": self.sle.actual_qty,
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530221 "incoming_rate": incoming_rate,
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530222 "stock_value_difference": stock_value_difference,
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530223 },
224 )
225
226 def create_batch(self):
227 from erpnext.stock.doctype.batch.batch import make_batch
228
229 return make_batch(
230 frappe._dict(
231 {
232 "item": self.item_code,
233 "reference_doctype": self.sle.voucher_type,
234 "reference_name": self.sle.voucher_no,
235 }
236 )
237 )
238
239 def process_batch_no(self):
240 if (
241 not self.sle.is_cancelled
242 and not self.sle.serial_and_batch_bundle
243 and self.sle.actual_qty > 0
244 and self.item_details.has_batch_no == 1
245 and self.item_details.create_new_batch
246 and self.item_details.batch_number_series
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530247 and self.allow_to_make_auto_bundle()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530248 ):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530249 self.make_serial_batch_no_bundle()
250 elif not self.sle.is_cancelled:
251 self.validate_item_and_warehouse()
252
253 def validate_item_and_warehouse(self):
254
255 data = frappe.db.get_value(
256 "Serial and Batch Bundle",
257 self.sle.serial_and_batch_bundle,
258 ["item_code", "warehouse", "voucher_no"],
259 as_dict=1,
260 )
261
262 if self.sle.serial_and_batch_bundle and not frappe.db.exists(
263 "Serial and Batch Bundle",
264 {
265 "name": self.sle.serial_and_batch_bundle,
266 "item_code": self.item_code,
267 "warehouse": self.warehouse,
268 "voucher_no": self.sle.voucher_no,
269 },
270 ):
271 msg = f"""
272 The Serial and Batch Bundle
273 {bold(self.sle.serial_and_batch_bundle)}
274 does not belong to Item {bold(self.item_code)}
275 or Warehouse {bold(self.warehouse)}
276 or {self.sle.voucher_type} no {bold(self.sle.voucher_no)}
277 """
278
279 frappe.throw(_(msg))
280
281 def delink_serial_and_batch_bundle(self):
282 update_values = {
283 "serial_and_batch_bundle": "",
284 }
285
286 if is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse):
287 update_values["rejected_serial_and_batch_bundle"] = ""
288
289 frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values)
290
291 frappe.db.set_value(
292 "Serial and Batch Bundle",
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530293 {"voucher_no": self.sle.voucher_no, "voucher_type": self.sle.voucher_type},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530294 {"is_cancelled": 1, "voucher_no": ""},
295 )
296
297 def allow_to_make_auto_bundle(self):
298 if self.sle.voucher_type in ["Stock Entry", "Purchase Receipt", "Purchase Invoice"]:
299 if self.sle.voucher_type == "Stock Entry":
300 stock_entry_type = frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose")
301
302 if stock_entry_type in ["Material Receipt", "Manufacture", "Repack"]:
303 return True
304
305 return True
306
307 return False
308
309 def post_process(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530310 if self.item_details.has_serial_no == 1:
311 self.set_warehouse_and_status_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530312
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530313 if (
314 self.sle.actual_qty > 0
315 and self.item_details.has_serial_no == 1
316 and self.item_details.has_batch_no == 1
317 ):
318 self.set_batch_no_in_serial_nos()
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530319
320 def set_warehouse_and_status_in_serial_nos(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530321 serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle, check_outward=False)
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530322 warehouse = self.warehouse if self.sle.actual_qty > 0 else None
323
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530324 if not serial_nos:
325 return
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530326
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530327 sn_table = frappe.qb.DocType("Serial No")
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530328 (
329 frappe.qb.update(sn_table)
330 .set(sn_table.warehouse, warehouse)
331 .set(sn_table.status, "Active" if warehouse else "Inactive")
332 .where(sn_table.name.isin(serial_nos))
333 ).run()
334
335 def set_batch_no_in_serial_nos(self):
336 ledgers = frappe.get_all(
337 "Serial and Batch Ledger",
338 fields=["serial_no", "batch_no"],
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530339 filters={"parent": self.sle.serial_and_batch_bundle},
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530340 )
341
342 batch_serial_nos = {}
343 for ledger in ledgers:
344 batch_serial_nos.setdefault(ledger.batch_no, []).append(ledger.serial_no)
345
346 for batch_no, serial_nos in batch_serial_nos.items():
347 sn_table = frappe.qb.DocType("Serial No")
348 (
349 frappe.qb.update(sn_table)
350 .set(sn_table.batch_no, batch_no)
351 .where(sn_table.name.isin(serial_nos))
352 ).run()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530353
354
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530355def get_serial_nos(serial_and_batch_bundle, check_outward=True):
356 filters = {"parent": serial_and_batch_bundle}
357 if check_outward:
358 filters["is_outward"] = 1
359
360 ledgers = frappe.get_all("Serial and Batch Ledger", fields=["serial_no"], filters=filters)
361
362 return [d.serial_no for d in ledgers]
363
364
365class SerialNoBundleValuation(DeprecatedSerialNoValuation):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530366 def __init__(self, **kwargs):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530367 for key, value in kwargs.items():
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530368 setattr(self, key, value)
369
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530370 self.calculate_stock_value_change()
371 self.calculate_valuation_rate()
372
373 def calculate_stock_value_change(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530374 if self.sle.actual_qty > 0:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530375 self.stock_value_change = frappe.get_cached_value(
376 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
377 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530378
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530379 else:
380 ledgers = self.get_serial_no_ledgers()
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530381
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530382 self.serial_no_incoming_rate = defaultdict(float)
383 self.stock_value_change = 0.0
384
385 for ledger in ledgers:
386 self.stock_value_change += ledger.incoming_rate * -1
387 self.serial_no_incoming_rate[ledger.serial_no] = ledger.incoming_rate
388
389 self.calculate_stock_value_from_deprecarated_ledgers()
390
391 def get_serial_no_ledgers(self):
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530392 serial_nos = self.get_serial_nos()
393
394 subquery = f"""
395 SELECT
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530396 MAX(
397 TIMESTAMP(
398 parent.posting_date, parent.posting_time
399 )
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530400 ), child.name, child.serial_no, child.warehouse
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530401 FROM
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530402 `tabSerial and Batch Bundle` as parent,
403 `tabSerial and Batch Ledger` as child
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530404 WHERE
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530405 parent.name = child.parent
406 AND child.serial_no IN ({', '.join([frappe.db.escape(s) for s in serial_nos])})
407 AND child.is_outward = 0
408 AND parent.docstatus < 2
409 AND parent.is_cancelled = 0
410 AND child.warehouse = {frappe.db.escape(self.sle.warehouse)}
411 AND parent.item_code = {frappe.db.escape(self.sle.item_code)}
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530412 AND (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530413 parent.posting_date < '{self.sle.posting_date}'
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530414 OR (
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530415 parent.posting_date = '{self.sle.posting_date}'
416 AND parent.posting_time <= '{self.sle.posting_time}'
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530417 )
418 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530419 GROUP BY
420 child.serial_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530421 """
422
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530423 return frappe.db.sql(
424 f"""
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530425 SELECT
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530426 ledger.serial_no, ledger.incoming_rate, ledger.warehouse
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530427 FROM
428 `tabSerial and Batch Ledger` AS ledger,
429 ({subquery}) AS SubQuery
430 WHERE
431 ledger.name = SubQuery.name
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530432 AND ledger.serial_no = SubQuery.serial_no
433 AND ledger.warehouse = SubQuery.warehouse
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530434 GROUP BY
435 ledger.serial_no
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530436 Order By
437 ledger.creation
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530438 """,
439 as_dict=1,
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530440 )
441
442 def get_serial_nos(self):
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530443 if self.sle.get("serial_nos"):
444 return self.sle.serial_nos
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530445
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530446 return get_serial_nos(self.sle.serial_and_batch_bundle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530447
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530448 def calculate_valuation_rate(self):
449 if not hasattr(self, "wh_data"):
450 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530451
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530452 new_stock_qty = self.wh_data.qty_after_transaction + self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530453
454 if new_stock_qty > 0:
455 new_stock_value = (
456 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530457 ) + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530458 if new_stock_value >= 0:
459 # calculate new valuation rate only if stock value is positive
460 # else it remains the same as that of previous entry
461 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
462
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530463 if (
464 not self.wh_data.valuation_rate and self.sle.voucher_detail_no and not self.is_rejected_entry()
465 ):
466 allow_zero_rate = self.sle_self.check_if_allow_zero_valuation_rate(
467 self.sle.voucher_type, self.sle.voucher_detail_no
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530468 )
469 if not allow_zero_rate:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530470 self.wh_data.valuation_rate = self.sle_self.get_fallback_rate(self.sle)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530471
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530472 self.wh_data.qty_after_transaction += self.sle.actual_qty
473 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
474 self.wh_data.valuation_rate
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530475 )
476
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530477 def is_rejected_entry(self):
478 return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse)
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530479
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530480 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530481 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530482
483
484def is_rejected(voucher_type, voucher_detail_no, warehouse):
485 if voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
486 return warehouse == frappe.get_cached_value(
487 voucher_type + " Item", voucher_detail_no, "rejected_warehouse"
488 )
489
490 return False
491
492
493class BatchNoBundleValuation(DeprecatedBatchNoValuation):
494 def __init__(self, **kwargs):
495 for key, value in kwargs.items():
496 setattr(self, key, value)
497
498 self.batch_nos = self.get_batch_nos()
499 self.calculate_avg_rate()
500 self.calculate_valuation_rate()
501
502 def calculate_avg_rate(self):
503 if self.sle.actual_qty > 0:
504 self.stock_value_change = frappe.get_cached_value(
505 "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "total_amount"
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530506 )
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530507 else:
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530508 ledgers = self.get_batch_no_ledgers()
509
510 self.batch_avg_rate = defaultdict(float)
511 for ledger in ledgers:
512 self.batch_avg_rate[ledger.batch_no] += flt(ledger.incoming_rate) / flt(ledger.qty)
513
514 self.calculate_avg_rate_from_deprecarated_ledgers()
515 self.set_stock_value_difference()
516
517 def get_batch_no_ledgers(self) -> List[dict]:
518 parent = frappe.qb.DocType("Serial and Batch Bundle")
519 child = frappe.qb.DocType("Serial and Batch Ledger")
520
521 batch_nos = list(self.batch_nos.keys())
522
523 return (
524 frappe.qb.from_(parent)
525 .inner_join(child)
526 .on(parent.name == child.parent)
527 .select(
528 child.batch_no,
529 Sum(child.stock_value_difference).as_("incoming_rate"),
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530530 Sum(child.qty).as_("qty"),
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530531 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530532 .where(
533 (child.batch_no.isin(batch_nos))
534 & (child.parent != self.sle.serial_and_batch_bundle)
535 & (parent.warehouse == self.sle.warehouse)
536 & (parent.item_code == self.sle.item_code)
537 & (parent.is_cancelled == 0)
538 )
539 .groupby(child.batch_no)
540 ).run(as_dict=True)
541
542 def get_batch_nos(self) -> list:
543 if self.sle.get("batch_nos"):
544 return self.sle.batch_nos
545
546 ledgers = frappe.get_all(
547 "Serial and Batch Ledger",
548 fields=["batch_no", "qty", "name"],
549 filters={"parent": self.sle.serial_and_batch_bundle, "is_outward": 1},
550 )
551
552 return {d.batch_no: d for d in ledgers}
553
554 def set_stock_value_difference(self):
555 self.stock_value_change = 0
556 for batch_no, ledger in self.batch_nos.items():
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530557 stock_value_change = self.batch_avg_rate[batch_no] * ledger.qty
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530558 self.stock_value_change += stock_value_change
559 frappe.db.set_value(
560 "Serial and Batch Ledger", ledger.name, "stock_value_difference", stock_value_change
561 )
562
563 def calculate_valuation_rate(self):
564 if not hasattr(self, "wh_data"):
565 return
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530566
567 self.wh_data.stock_value = round_off_if_near_zero(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530568 self.wh_data.stock_value + self.stock_value_change
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530569 )
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530570
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530571 if self.wh_data.qty_after_transaction:
572 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
573
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530574 self.wh_data.qty_after_transaction += self.sle.actual_qty
Rohit Waghchauref1b59662023-03-06 12:08:28 +0530575
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530576 def get_incoming_rate(self):
Rohit Waghchaure5ddd55a2023-03-16 12:58:48 +0530577 return abs(flt(self.stock_value_change) / flt(self.sle.actual_qty))