blob: 5abb8e827f68554c97004300871e8945a271b34f [file] [log] [blame]
Maricad6078aa2022-06-17 15:13:13 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait902e8602013-01-08 18:29:24 +05303
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05304import copy
Nabin Hait26d46552013-01-09 15:23:05 +05305import json
Ankush Menatecdb4932022-04-17 19:06:13 +05306from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +05309from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053010from frappe.model.meta import get_field_precision
Rohit Waghchaurebb954512023-06-02 00:11:43 +053011from frappe.query_builder import Case
Ankush Menate1c16872022-04-21 20:01:48 +053012from frappe.query_builder.functions import CombineDatetime, Sum
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053013from frappe.utils import (
14 cint,
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053015 flt,
16 get_link_to_form,
17 getdate,
18 gzip_compress,
19 gzip_decompress,
20 now,
21 nowdate,
22 parse_json,
23)
Achilles Rasquinha361366e2018-02-14 17:08:59 +053024
Chillar Anand915b3432021-09-02 16:44:59 +053025import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053026from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
s-aga-rf0acb202023-04-12 14:13:54 +053027from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
28 get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
29)
Chillar Anand915b3432021-09-02 16:44:59 +053030from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053031 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053032 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053033 get_valuation_method,
34)
Ankush Menatb534fee2022-02-19 20:58:36 +053035from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053036
Nabin Hait97bce3a2021-07-12 13:24:43 +053037
Ankush Menat494bd9e2022-03-28 18:52:46 +053038class NegativeStockError(frappe.ValidationError):
39 pass
40
41
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053042class SerialNoExistsInFutureTransaction(frappe.ValidationError):
43 pass
Nabin Hait902e8602013-01-08 18:29:24 +053044
Anand Doshi5b004ff2013-09-25 19:55:41 +053045
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053046def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053047 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053048
Ankush Menat494bd9e2022-03-28 18:52:46 +053049 args:
50 - allow_negative_stock: disable negative stock valiations if true
51 - via_landed_cost_voucher: landed cost voucher cancels and reposts
52 entries of purchase document. This flag is used to identify if
53 cancellation and repost is happening via landed cost voucher, in
54 such cases certain validations need to be ignored (like negative
55 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053056 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053057 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053058
Nabin Haitca775742013-09-26 16:16:44 +053059 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053060 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053061 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053062 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053063 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053064
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053065 args = get_args_for_future_sle(sl_entries[0])
66 future_sle_exists(args, sl_entries)
67
Nabin Haitca775742013-09-26 16:16:44 +053068 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053069 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053070 validate_serial_no(sle)
71
Nabin Haita77b8c92020-12-21 14:45:50 +053072 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053073 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053074
Ankush Menat494bd9e2022-03-28 18:52:46 +053075 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
76 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
77 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
78 )
79 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053080
Ankush Menat494bd9e2022-03-28 18:52:46 +053081 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
82 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
83 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
84 )
85 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053086
Ankush Menat494bd9e2022-03-28 18:52:46 +053087 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053088 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053089
Nabin Haita77b8c92020-12-21 14:45:50 +053090 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053091
92 if sle.get("voucher_type") == "Stock Reconciliation":
93 # preserve previous_qty_after_transaction for qty reposting
94 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
95
Ankush Menat494bd9e2022-03-28 18:52:46 +053096 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053097 if is_stock_item:
98 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053099 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +0530100 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +0530101 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530102 frappe.msgprint(
103 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
104 )
105
Ankush Menatcef84c22021-12-03 12:18:59 +0530106
107def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
108 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
109 if not args.get("posting_date"):
110 args["posting_date"] = nowdate()
111
marination7a5fd712022-07-04 17:46:54 +0530112 if not (args.get("is_cancelled") and via_landed_cost_voucher):
113 # Reposts only current voucher SL Entries
114 # Updates valuation rate, stock value, stock queue for current transaction
115 update_entries_after(
116 {
117 "item_code": args.get("item_code"),
118 "warehouse": args.get("warehouse"),
119 "posting_date": args.get("posting_date"),
120 "posting_time": args.get("posting_time"),
121 "voucher_type": args.get("voucher_type"),
122 "voucher_no": args.get("voucher_no"),
123 "sle_id": args.get("name"),
124 "creation": args.get("creation"),
125 },
126 allow_negative_stock=allow_negative_stock,
127 via_landed_cost_voucher=via_landed_cost_voucher,
128 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530129
130 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530131 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530132 update_qty_in_future_sle(args, allow_negative_stock)
133
Nabin Haitadeb9762014-10-06 11:53:52 +0530134
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530135def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530136 return frappe._dict(
137 {
138 "voucher_type": row.get("voucher_type"),
139 "voucher_no": row.get("voucher_no"),
140 "posting_date": row.get("posting_date"),
141 "posting_time": row.get("posting_time"),
142 }
143 )
144
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530145
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530146def validate_serial_no(sle):
147 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530148
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530149 for sn in get_serial_nos(sle.serial_no):
150 args = copy.deepcopy(sle)
151 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530152 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530153
154 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530155 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530156 voucher_type = frappe.bold(row.voucher_type)
157 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530158 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530159
160 if vouchers:
161 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530162 msg = (
163 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
164 The list of the transactions are as below."""
165 + "<br><br><ul><li>"
166 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530167
Ankush Menat494bd9e2022-03-28 18:52:46 +0530168 msg += "</li><li>".join(vouchers)
169 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530170
Ankush Menat494bd9e2022-03-28 18:52:46 +0530171 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530172 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
173
Ankush Menat494bd9e2022-03-28 18:52:46 +0530174
Nabin Hait186a0452021-02-18 14:14:21 +0530175def validate_cancellation(args):
176 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530177 repost_entry = frappe.db.get_value(
178 "Repost Item Valuation",
179 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
180 ["name", "status"],
181 as_dict=1,
182 )
Nabin Hait186a0452021-02-18 14:14:21 +0530183
184 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530185 if repost_entry.status == "In Progress":
186 frappe.throw(
187 _(
188 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
189 )
190 )
191 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530192 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530193 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530194 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530195 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530196
Ankush Menat494bd9e2022-03-28 18:52:46 +0530197
Nabin Hait9653f602013-08-20 15:37:33 +0530198def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530199 frappe.db.sql(
200 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530201 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530202 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530203 (now(), frappe.session.user, voucher_type, voucher_no),
204 )
205
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530206
Nabin Hait54c865e2015-03-27 15:38:31 +0530207def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530208 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530209 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530210 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530211 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530212 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530213 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530214 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530215
Ankush Menat494bd9e2022-03-28 18:52:46 +0530216
217def repost_future_sle(
218 args=None,
219 voucher_type=None,
220 voucher_no=None,
221 allow_negative_stock=None,
222 via_landed_cost_voucher=False,
223 doc=None,
224):
Nabin Haite1fa7232022-07-20 15:19:09 +0530225 if not args:
226 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530227
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530228 reposting_data = {}
229 if doc and doc.reposting_data_file:
230 reposting_data = get_reposting_data(doc.reposting_data_file)
231
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530232 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530233 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530234 )
235 if items_to_be_repost:
236 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530237
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530238 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
239 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530240
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530241 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530242 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530243 validate_item_warehouse(args[i])
244
Ankush Menat494bd9e2022-03-28 18:52:46 +0530245 obj = update_entries_after(
246 {
247 "item_code": args[i].get("item_code"),
248 "warehouse": args[i].get("warehouse"),
249 "posting_date": args[i].get("posting_date"),
250 "posting_time": args[i].get("posting_time"),
251 "creation": args[i].get("creation"),
252 "distinct_item_warehouses": distinct_item_warehouses,
253 },
254 allow_negative_stock=allow_negative_stock,
255 via_landed_cost_voucher=via_landed_cost_voucher,
256 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530257 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530258
Ankush Menat494bd9e2022-03-28 18:52:46 +0530259 distinct_item_warehouses[
260 (args[i].get("item_code"), args[i].get("warehouse"))
261 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530262
Nabin Hait97bce3a2021-07-12 13:24:43 +0530263 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530264 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530265 if ("args_idx" not in data and not data.reposting_status) or (
266 data.sle_changed and data.reposting_status
267 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530268 data.args_idx = len(args)
269 args.append(data.sle)
270 elif data.sle_changed and not data.reposting_status:
271 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530272
Nabin Hait97bce3a2021-07-12 13:24:43 +0530273 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530274 i += 1
275
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530276 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530277 update_args_in_repost_item_valuation(
278 doc, i, args, distinct_item_warehouses, affected_transactions
279 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530280
Ankush Menat494bd9e2022-03-28 18:52:46 +0530281
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530282def get_reposting_data(file_path) -> dict:
283 file_name = frappe.db.get_value(
284 "File",
285 {
286 "file_url": file_path,
287 "attached_to_field": "reposting_data_file",
288 },
289 "name",
290 )
291
292 if not file_name:
293 return frappe._dict()
294
295 attached_file = frappe.get_doc("File", file_name)
296
297 data = gzip_decompress(attached_file.get_content())
298 if data := json.loads(data.decode("utf-8")):
299 data = data
300
301 return parse_json(data)
302
303
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530304def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530305 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530306 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530307 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530308 frappe.throw(_(validation_msg))
309
Ankush Menat494bd9e2022-03-28 18:52:46 +0530310
Ankush Menatecdb4932022-04-17 19:06:13 +0530311def update_args_in_repost_item_valuation(
312 doc, index, args, distinct_item_warehouses, affected_transactions
313):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530314 if not doc.items_to_be_repost:
315 file_name = ""
316 if doc.reposting_data_file:
317 file_name = get_reposting_file_name(doc.doctype, doc.name)
318 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
319
320 doc.reposting_data_file = create_json_gz_file(
321 {
322 "items_to_be_repost": args,
323 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
324 "affected_transactions": affected_transactions,
325 },
326 doc,
327 file_name,
328 )
329
330 doc.db_set(
331 {
332 "current_index": index,
333 "total_reposting_count": len(args),
334 "reposting_data_file": doc.reposting_data_file,
335 }
336 )
337
338 else:
339 doc.db_set(
340 {
341 "items_to_be_repost": json.dumps(args, default=str),
342 "distinct_item_and_warehouse": json.dumps(
343 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
344 ),
345 "current_index": index,
346 "affected_transactions": frappe.as_json(affected_transactions),
347 }
348 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530349
Ankush Menatecdb4932022-04-17 19:06:13 +0530350 if not frappe.flags.in_test:
351 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530352
Ankush Menat494bd9e2022-03-28 18:52:46 +0530353 frappe.publish_realtime(
354 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530355 {
356 "name": doc.name,
357 "items_to_be_repost": json.dumps(args, default=str),
358 "current_index": index,
359 "total_reposting_count": len(args),
360 },
Ankush Menat494bd9e2022-03-28 18:52:46 +0530361 )
362
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530363
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530364def get_reposting_file_name(dt, dn):
365 return frappe.db.get_value(
366 "File",
367 {
368 "attached_to_doctype": dt,
369 "attached_to_name": dn,
370 "attached_to_field": "reposting_data_file",
371 },
372 "name",
373 )
374
375
376def create_json_gz_file(data, doc, file_name=None) -> str:
377 encoded_content = frappe.safe_encode(frappe.as_json(data))
378 compressed_content = gzip_compress(encoded_content)
379
380 if not file_name:
381 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
382 _file = frappe.get_doc(
383 {
384 "doctype": "File",
385 "file_name": json_filename,
386 "attached_to_doctype": doc.doctype,
387 "attached_to_name": doc.name,
388 "attached_to_field": "reposting_data_file",
389 "content": compressed_content,
390 "is_private": 1,
391 }
392 )
393 _file.save(ignore_permissions=True)
394
395 return _file.file_url
396 else:
397 file_doc = frappe.get_doc("File", file_name)
398 path = file_doc.get_full_path()
399
400 with open(path, "wb") as f:
401 f.write(compressed_content)
402
403 return doc.reposting_data_file
404
405
406def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
407 if not reposting_data and doc and doc.reposting_data_file:
408 reposting_data = get_reposting_data(doc.reposting_data_file)
409
410 if reposting_data and reposting_data.items_to_be_repost:
411 return reposting_data.items_to_be_repost
412
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530413 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530414
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530415 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530416 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530417
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530418 if not items_to_be_repost and voucher_type and voucher_no:
419 items_to_be_repost = frappe.db.get_all(
420 "Stock Ledger Entry",
421 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
422 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
423 order_by="creation asc",
424 group_by="item_code, warehouse",
425 )
426
Nabin Haite1fa7232022-07-20 15:19:09 +0530427 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530428
Ankush Menat494bd9e2022-03-28 18:52:46 +0530429
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530430def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
431 if not reposting_data and doc and doc.reposting_data_file:
432 reposting_data = get_reposting_data(doc.reposting_data_file)
433
434 if reposting_data and reposting_data.distinct_item_and_warehouse:
435 return reposting_data.distinct_item_and_warehouse
436
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530437 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530438
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530439 if doc and doc.distinct_item_and_warehouse:
440 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530441 distinct_item_warehouses = {
442 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
443 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530444 else:
445 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530446 distinct_item_warehouses.setdefault(
447 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
448 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530449
450 return distinct_item_warehouses
451
Ankush Menat494bd9e2022-03-28 18:52:46 +0530452
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530453def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
454 if not reposting_data and doc and doc.reposting_data_file:
455 reposting_data = get_reposting_data(doc.reposting_data_file)
456
457 if reposting_data and reposting_data.affected_transactions:
458 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
459
Ankush Menatecdb4932022-04-17 19:06:13 +0530460 if not doc.affected_transactions:
461 return set()
462
463 transactions = frappe.parse_json(doc.affected_transactions)
464 return {tuple(transaction) for transaction in transactions}
465
466
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530467def get_current_index(doc=None):
468 if doc and doc.current_index:
469 return doc.current_index
470
Ankush Menat494bd9e2022-03-28 18:52:46 +0530471
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530472class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530473 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530474 update valution rate and qty after transaction
475 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530476
Ankush Menat494bd9e2022-03-28 18:52:46 +0530477 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530478
Ankush Menat494bd9e2022-03-28 18:52:46 +0530479 args = {
480 "item_code": "ABC",
481 "warehouse": "XYZ",
482 "posting_date": "2012-12-12",
483 "posting_time": "12:00"
484 }
Nabin Hait902e8602013-01-08 18:29:24 +0530485 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530486
487 def __init__(
488 self,
489 args,
490 allow_zero_rate=False,
491 allow_negative_stock=None,
492 via_landed_cost_voucher=False,
493 verbose=1,
494 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530495 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530496 self.verbose = verbose
497 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530498 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530499 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530500 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
501 item_code=self.item_code
502 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530503
Nabin Haita77b8c92020-12-21 14:45:50 +0530504 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530505 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530506 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530507
Nabin Haita77b8c92020-12-21 14:45:50 +0530508 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530509 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530510 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530511
512 self.new_items_found = False
513 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530514 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-rf0acb202023-04-12 14:13:54 +0530515 self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
Nabin Haita77b8c92020-12-21 14:45:50 +0530516
517 self.data = frappe._dict()
518 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530519 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530520
Maricad6078aa2022-06-17 15:13:13 +0530521 def set_precision(self):
522 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
523 self.currency_precision = get_field_precision(
524 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530525 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530526
527 def initialize_previous_data(self, args):
528 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530529 Get previous sl entries for current item for each related warehouse
530 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530531
Ankush Menat494bd9e2022-03-28 18:52:46 +0530532 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530533
Ankush Menat494bd9e2022-03-28 18:52:46 +0530534 self.data = {
535 warehouse1: {
536 'previus_sle': {},
537 'qty_after_transaction': 10,
538 'valuation_rate': 100,
539 'stock_value': 1000,
540 'prev_stock_value': 1000,
541 'stock_queue': '[[10, 100]]',
542 'stock_value_difference': 1000
543 }
544 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530545
546 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530547 self.data.setdefault(args.warehouse, frappe._dict())
548 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530549 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530550 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530551
Ankush Menatc1d986a2021-08-31 19:43:42 +0530552 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
553 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
554
Ankush Menat494bd9e2022-03-28 18:52:46 +0530555 warehouse_dict.update(
556 {
557 "prev_stock_value": previous_sle.stock_value or 0.0,
558 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
559 "stock_value_difference": 0.0,
560 }
561 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530562
Nabin Haita77b8c92020-12-21 14:45:50 +0530563 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530564 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530565
Nabin Haita77b8c92020-12-21 14:45:50 +0530566 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530567 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530568 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530569 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530570 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530571 entries_to_fix = self.get_future_entries_to_fix()
572
573 i = 0
574 while i < len(entries_to_fix):
575 sle = entries_to_fix[i]
576 i += 1
577
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530578 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530579 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530580
Nabin Haita77b8c92020-12-21 14:45:50 +0530581 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530582 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530583
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530584 if self.exceptions:
585 self.raise_exceptions()
586
Nabin Hait186a0452021-02-18 14:14:21 +0530587 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530588 sl_entries = self.get_sle_against_current_voucher()
589 for sle in sl_entries:
590 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530591
Nabin Haita77b8c92020-12-21 14:45:50 +0530592 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530593 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530594
Ankush Menat494bd9e2022-03-28 18:52:46 +0530595 return frappe.db.sql(
596 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530597 select
598 *, timestamp(posting_date, posting_time) as "timestamp"
599 from
600 `tabStock Ledger Entry`
601 where
602 item_code = %(item_code)s
603 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530604 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530605 and (
606 posting_date = %(posting_date)s and
607 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
608 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530609 order by
610 creation ASC
611 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530612 """,
613 self.args,
614 as_dict=1,
615 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530616
Nabin Haita77b8c92020-12-21 14:45:50 +0530617 def get_future_entries_to_fix(self):
618 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530619 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
620 {"item_code": self.item_code, "warehouse": self.args.warehouse}
621 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530622
Nabin Haita77b8c92020-12-21 14:45:50 +0530623 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530624
Nabin Haita77b8c92020-12-21 14:45:50 +0530625 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530626 dependant_sle = get_sle_by_voucher_detail_no(
627 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
628 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530629
Nabin Haita77b8c92020-12-21 14:45:50 +0530630 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530631 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530632 elif (
633 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
634 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530635 return entries_to_fix
636 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530637 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530638 return entries_to_fix
639 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
640 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530641 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530642 self.initialize_previous_data(dependant_sle)
643 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530644 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530645
646 def update_distinct_item_warehouses(self, dependant_sle):
647 key = (dependant_sle.item_code, dependant_sle.warehouse)
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530648 val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530649
Nabin Hait97bce3a2021-07-12 13:24:43 +0530650 if key not in self.distinct_item_warehouses:
651 self.distinct_item_warehouses[key] = val
652 self.new_items_found = True
653 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530654 existing_sle_posting_date = (
655 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
656 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530657
658 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
659
Nabin Hait97bce3a2021-07-12 13:24:43 +0530660 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
661 val.sle_changed = True
662 self.distinct_item_warehouses[key] = val
663 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530664 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
665 # Future dependent voucher needs to be repost to get the correct stock value
666 # If dependent voucher has not reposted, then add it to the list
667 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530668 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530669 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
670 self.distinct_item_warehouses[key] = val
671
672 def get_dependent_voucher_detail_nos(self, key):
673 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
674 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
675
676 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530677
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530678 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530679 # previous sle data for this warehouse
680 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530681 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530682
Anand Doshi0dc79f42015-04-06 12:59:34 +0530683 if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530684 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530685 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530686 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530687 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530688 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530689
Nabin Haita77b8c92020-12-21 14:45:50 +0530690 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530691 if not self.args.get("sle_id"):
692 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530693
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530694 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530695 sle.voucher_type == "Stock Reconciliation"
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530696 and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle))
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530697 and sle.voucher_detail_no
698 and sle.actual_qty < 0
699 ):
700 self.reset_actual_qty_for_stock_reco(sle)
701
702 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530703 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
704 and sle.voucher_detail_no
705 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530706 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530707 ):
708 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
709
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530710 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530711 self.calculate_valuation_for_serial_batch_bundle(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530712 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530713 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530714 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530715 self.wh_data.valuation_rate = sle.valuation_rate
716 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530717 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
718 self.wh_data.valuation_rate
719 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530720 if self.valuation_method != "Moving Average":
721 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530722 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530723 if self.valuation_method == "Moving Average":
724 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530725 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530726 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
727 self.wh_data.valuation_rate
728 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530729 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530730 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530731
Rushabh Mehta54047782013-12-26 11:07:46 +0530732 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530733 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530734 if not self.wh_data.qty_after_transaction:
735 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530736
Nabin Haita77b8c92020-12-21 14:45:50 +0530737 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
738 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530739
Nabin Hait902e8602013-01-08 18:29:24 +0530740 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530741 sle.qty_after_transaction = self.wh_data.qty_after_transaction
742 sle.valuation_rate = self.wh_data.valuation_rate
743 sle.stock_value = self.wh_data.stock_value
744 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530745 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530746 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530747
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530748 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530749
Ankush Menat701878f2022-03-01 18:08:29 +0530750 if not self.args.get("sle_id"):
751 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530752
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530753 def reset_actual_qty_for_stock_reco(self, sle):
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530754 if sle.serial_and_batch_bundle:
755 current_qty = frappe.get_cached_value(
756 "Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty"
757 )
758
759 if current_qty is not None:
760 current_qty = abs(current_qty)
761 else:
762 current_qty = frappe.get_cached_value(
763 "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
764 )
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530765
766 if current_qty:
767 sle.actual_qty = current_qty * -1
768 elif current_qty == 0:
769 sle.is_cancelled = 1
770
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530771 def calculate_valuation_for_serial_batch_bundle(self, sle):
772 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
773
774 doc.set_incoming_rate(save=True)
775 doc.calculate_qty_and_amount(save=True)
776
777 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
778
779 self.wh_data.qty_after_transaction += doc.total_qty
780 if self.wh_data.qty_after_transaction:
781 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
782
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530783 def validate_negative_stock(self, sle):
784 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530785 validate negative stock for entries current datetime onwards
786 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530787 """
s-aga-rf0acb202023-04-12 14:13:54 +0530788 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530789 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530790
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530791 if diff < 0 and abs(diff) > 0.0001:
792 # negative stock!
793 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530794 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530795 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530796 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530797 return True
798
Nabin Haita77b8c92020-12-21 14:45:50 +0530799 def get_dynamic_incoming_outgoing_rate(self, sle):
800 # Get updated incoming/outgoing rate from transaction
801 if sle.recalculate_rate:
802 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
803
804 if flt(sle.actual_qty) >= 0:
805 sle.incoming_rate = rate
806 else:
807 sle.outgoing_rate = rate
808
809 def get_incoming_outgoing_rate_from_transaction(self, sle):
810 rate = 0
811 # Material Transfer, Repack, Manufacturing
812 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530813 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530814 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
815 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530816 elif sle.voucher_type in (
817 "Purchase Receipt",
818 "Purchase Invoice",
819 "Delivery Note",
820 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530821 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530822 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530823 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530824 from erpnext.controllers.sales_and_purchase_return import (
825 get_rate_for_return, # don't move this import to top
826 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530827
828 rate = get_rate_for_return(
829 sle.voucher_type,
830 sle.voucher_no,
831 sle.item_code,
832 voucher_detail_no=sle.voucher_detail_no,
833 sle=sle,
834 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530835
836 elif (
837 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530838 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530839 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530840 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530841 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530842 else:
843 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530844 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530845 elif sle.voucher_type == "Subcontracting Receipt":
846 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530847 else:
848 rate_field = "incoming_rate"
849
850 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530851 item_code, incoming_rate = frappe.db.get_value(
852 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
853 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530854
855 if item_code == sle.item_code:
856 rate = incoming_rate
857 else:
858 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
859 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530860 elif sle == "Subcontracting Receipt":
861 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530862 else:
863 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530864
Ankush Menat494bd9e2022-03-28 18:52:46 +0530865 rate = frappe.db.get_value(
866 ref_doctype,
867 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
868 rate_field,
869 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530870
871 return rate
872
873 def update_outgoing_rate_on_transaction(self, sle):
874 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530875 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
876 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530877 """
878 if sle.actual_qty and sle.voucher_detail_no:
879 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
880
881 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
882 self.update_rate_on_stock_entry(sle, outgoing_rate)
883 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
884 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
885 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
886 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530887 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
888 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530889 elif sle.voucher_type == "Stock Reconciliation":
890 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530891
892 def update_rate_on_stock_entry(self, sle, outgoing_rate):
893 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
894
Ankush Menat701878f2022-03-01 18:08:29 +0530895 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
896 if not sle.dependant_sle_voucher_detail_no:
897 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530898
899 def recalculate_amounts_in_stock_entry(self, voucher_no):
900 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530901 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
902 stock_entry.db_update()
903 for d in stock_entry.items:
904 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530905
Nabin Haita77b8c92020-12-21 14:45:50 +0530906 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
907 # Update item's incoming rate on transaction
908 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
909 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530910 frappe.db.set_value(
911 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
912 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530913 else:
914 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530915 frappe.db.set_value(
916 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530917 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530918 "incoming_rate",
919 outgoing_rate,
920 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530921
922 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
923 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530924 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
925 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
926 ):
927 frappe.db.set_value(
928 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
929 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530930 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530931 frappe.db.set_value(
932 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
933 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530934
935 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530936 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530937 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530938 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530939 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530940 d.db_update()
941
Sagar Sharma323bdf82022-05-17 15:14:07 +0530942 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530943 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
944 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530945 else:
946 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530947 "Subcontracting Receipt Supplied Item",
948 sle.voucher_detail_no,
949 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530950 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530951
s-aga-ra6cb6c62023-05-03 09:51:58 +0530952 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530953 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530954 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530955 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530956 d.db_update()
957
s-aga-r88a3f652023-05-30 16:54:28 +0530958 def update_rate_on_stock_reconciliation(self, sle):
959 if not sle.serial_no and not sle.batch_no:
960 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
961
962 for item in sr.items:
963 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +0530964 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +0530965 continue
966
967 previous_sle = get_previous_sle(
968 {
969 "item_code": item.item_code,
970 "warehouse": item.warehouse,
971 "posting_date": sr.posting_date,
972 "posting_time": sr.posting_time,
973 "sle": sle.name,
974 }
975 )
976
977 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
978 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
979 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
980
981 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +0530982 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +0530983 item.amount_difference = item.amount - item.current_amount
984 else:
985 sr.difference_amount = sum([item.amount_difference for item in sr.items])
986 sr.db_update()
987
988 for item in sr.items:
989 item.db_update()
990
Nabin Hait328c4f92020-01-02 19:00:32 +0530991 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
992 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530993 all_serial_nos = frappe.get_all(
994 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
995 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530996
Ankush Menat494bd9e2022-03-28 18:52:46 +0530997 incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company == sle.company)
Nabin Hait328c4f92020-01-02 19:00:32 +0530998
999 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301000 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301001 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301002 incoming_rate = frappe.db.sql(
1003 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301004 select incoming_rate
1005 from `tabStock Ledger Entry`
1006 where
1007 company = %s
1008 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301009 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301010 and (serial_no = %s
1011 or serial_no like %s
1012 or serial_no like %s
1013 or serial_no like %s
1014 )
1015 order by posting_date desc
1016 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301017 """,
1018 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1019 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301020
1021 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1022
1023 return incoming_values
1024
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301025 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301026 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301027 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301028 if new_stock_qty >= 0:
1029 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301030 if flt(self.wh_data.qty_after_transaction) <= 0:
1031 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301032 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301033 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1034 actual_qty * sle.incoming_rate
1035 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301036
Nabin Haita77b8c92020-12-21 14:45:50 +05301037 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301038
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301039 elif sle.outgoing_rate:
1040 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301041 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1042 actual_qty * sle.outgoing_rate
1043 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301044
Nabin Haita77b8c92020-12-21 14:45:50 +05301045 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301046 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301047 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301048 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301049 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1050 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301051
Nabin Haita77b8c92020-12-21 14:45:50 +05301052 if not self.wh_data.valuation_rate and actual_qty > 0:
1053 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301054
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301055 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001056 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301057 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301058 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1059 sle.voucher_type, sle.voucher_detail_no
1060 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001061 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301062 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301063
Ankush Menatf089d392022-02-02 12:51:21 +05301064 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301065 incoming_rate = flt(sle.incoming_rate)
1066 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301067 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301068
Ankush Menat494bd9e2022-03-28 18:52:46 +05301069 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1070 self.wh_data.qty_after_transaction + actual_qty
1071 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301072
Ankush Menat97e18a12022-01-15 17:42:25 +05301073 if self.valuation_method == "LIFO":
1074 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1075 else:
1076 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1077
Ankush Menatb534fee2022-02-19 20:58:36 +05301078 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1079
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301080 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301081 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301082 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301083
Ankush Menat4b29fb62021-12-18 18:40:22 +05301084 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301085 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1086 sle.voucher_type, sle.voucher_detail_no
1087 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301088 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301089 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301090 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301091 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301092
Ankush Menat494bd9e2022-03-28 18:52:46 +05301093 stock_queue.remove_stock(
1094 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1095 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301096
Ankush Menatb534fee2022-02-19 20:58:36 +05301097 _qty, stock_value = stock_queue.get_total_stock_and_value()
1098
1099 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301100
Ankush Menat97e18a12022-01-15 17:42:25 +05301101 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301102 self.wh_data.stock_value = round_off_if_near_zero(
1103 self.wh_data.stock_value + stock_value_difference
1104 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301105
Nabin Haita77b8c92020-12-21 14:45:50 +05301106 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301107 self.wh_data.stock_queue.append(
1108 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1109 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301110
Ankush Menatb534fee2022-02-19 20:58:36 +05301111 if self.wh_data.qty_after_transaction:
1112 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1113
Ankush Menatce0514c2022-02-15 11:41:41 +05301114 def update_batched_values(self, sle):
1115 incoming_rate = flt(sle.incoming_rate)
1116 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301117
Ankush Menat494bd9e2022-03-28 18:52:46 +05301118 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1119 self.wh_data.qty_after_transaction + actual_qty
1120 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301121
1122 if actual_qty > 0:
1123 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301124 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301125 outgoing_rate = get_batch_incoming_rate(
1126 item_code=sle.item_code,
1127 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301128 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301129 posting_date=sle.posting_date,
1130 posting_time=sle.posting_time,
1131 creation=sle.creation,
1132 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301133 if outgoing_rate is None:
1134 # This can *only* happen if qty available for the batch is zero.
1135 # in such case fall back various other rates.
1136 # future entries will correct the overall accounting as each
1137 # batch individually uses moving average rates.
1138 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301139 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301140
Ankush Menat494bd9e2022-03-28 18:52:46 +05301141 self.wh_data.stock_value = round_off_if_near_zero(
1142 self.wh_data.stock_value + stock_value_difference
1143 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301144 if self.wh_data.qty_after_transaction:
1145 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301146
Javier Wong9b11d9b2017-04-14 18:24:04 +08001147 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301148 ref_item_dt = ""
1149
1150 if voucher_type == "Stock Entry":
1151 ref_item_dt = voucher_type + " Detail"
1152 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1153 ref_item_dt = voucher_type + " Item"
1154
1155 if ref_item_dt:
1156 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1157 else:
1158 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301159
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301160 def get_fallback_rate(self, sle) -> float:
1161 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301162 This should only get used for negative stock."""
1163 return get_valuation_rate(
1164 sle.item_code,
1165 sle.warehouse,
1166 sle.voucher_type,
1167 sle.voucher_no,
1168 self.allow_zero_rate,
1169 currency=erpnext.get_company_currency(sle.company),
1170 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301171 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301172
Nabin Haita77b8c92020-12-21 14:45:50 +05301173 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301174 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301175 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1176 sle = sle[0] if sle else frappe._dict()
1177 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301178
Nabin Haita77b8c92020-12-21 14:45:50 +05301179 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301180 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301181 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301182
1183 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301184 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301185 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301186 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301187
Ankush Menat494bd9e2022-03-28 18:52:46 +05301188 if (
1189 exceptions[0]["voucher_type"],
1190 exceptions[0]["voucher_no"],
1191 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301192
Nabin Haita77b8c92020-12-21 14:45:50 +05301193 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301194 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301195 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1196 frappe.get_desk_link("Warehouse", warehouse),
1197 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301198 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301199 msg = _(
1200 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1201 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301202 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301203 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1204 frappe.get_desk_link("Warehouse", warehouse),
1205 exceptions[0]["posting_date"],
1206 exceptions[0]["posting_time"],
1207 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1208 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301209
Nabin Haita77b8c92020-12-21 14:45:50 +05301210 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301211 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301212 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
1213 msg = "{0} As {1} units are reserved, you are allowed to consume only {2} units.".format(
1214 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1215 )
s-aga-rf0acb202023-04-12 14:13:54 +05301216
Nabin Haita77b8c92020-12-21 14:45:50 +05301217 msg_list.append(msg)
1218
1219 if msg_list:
1220 message = "\n\n".join(msg_list)
1221 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301222 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301223 else:
1224 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301225
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301226 def update_bin_data(self, sle):
1227 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301228 values_to_update = {
1229 "actual_qty": sle.qty_after_transaction,
1230 "stock_value": sle.stock_value,
1231 }
1232
1233 if sle.valuation_rate is not None:
1234 values_to_update["valuation_rate"] = sle.valuation_rate
1235
1236 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301237
Nabin Haita77b8c92020-12-21 14:45:50 +05301238 def update_bin(self):
1239 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301240 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301241 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301242
Ankush Menat494bd9e2022-03-28 18:52:46 +05301243 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301244 if data.valuation_rate is not None:
1245 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301246 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301247
marination8418c4b2021-06-22 21:35:25 +05301248
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301249def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301250 """get stock ledger entries filtered by specific posting datetime conditions"""
1251
Ankush Menat494bd9e2022-03-28 18:52:46 +05301252 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301253 if not args.get("posting_date"):
1254 args["posting_date"] = "1900-01-01"
1255 if not args.get("posting_time"):
1256 args["posting_time"] = "00:00"
1257
1258 voucher_condition = ""
1259 if exclude_current_voucher:
1260 voucher_no = args.get("voucher_no")
1261 voucher_condition = f"and voucher_no != '{voucher_no}'"
1262
Ankush Menat494bd9e2022-03-28 18:52:46 +05301263 sle = frappe.db.sql(
1264 """
marination8418c4b2021-06-22 21:35:25 +05301265 select *, timestamp(posting_date, posting_time) as "timestamp"
1266 from `tabStock Ledger Entry`
1267 where item_code = %(item_code)s
1268 and warehouse = %(warehouse)s
1269 and is_cancelled = 0
1270 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301271 and (
1272 posting_date < %(posting_date)s or
1273 (
1274 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301275 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301276 )
1277 )
marination8418c4b2021-06-22 21:35:25 +05301278 order by timestamp(posting_date, posting_time) desc, creation desc
1279 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301280 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301281 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301282 ),
1283 args,
1284 as_dict=1,
1285 )
marination8418c4b2021-06-22 21:35:25 +05301286
1287 return sle[0] if sle else frappe._dict()
1288
Ankush Menat494bd9e2022-03-28 18:52:46 +05301289
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301290def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301291 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301292 get the last sle on or before the current time-bucket,
1293 to get actual qty before transaction, this function
1294 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301295
Ankush Menat494bd9e2022-03-28 18:52:46 +05301296 args = {
1297 "item_code": "ABC",
1298 "warehouse": "XYZ",
1299 "posting_date": "2012-12-12",
1300 "posting_time": "12:00",
1301 "sle": "name of reference Stock Ledger Entry"
1302 }
Anand Doshi1b531862013-01-10 19:29:51 +05301303 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301304 args["name"] = args.get("sle", None) or ""
1305 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301306 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301307
Ankush Menat494bd9e2022-03-28 18:52:46 +05301308
1309def get_stock_ledger_entries(
1310 previous_sle,
1311 operator=None,
1312 order="desc",
1313 limit=None,
1314 for_update=False,
1315 debug=False,
1316 check_serial_no=True,
1317):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301318 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301319 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1320 operator
1321 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301322 if previous_sle.get("warehouse"):
1323 conditions += " and warehouse = %(warehouse)s"
1324 elif previous_sle.get("warehouse_condition"):
1325 conditions += " and " + previous_sle.get("warehouse_condition")
1326
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301327 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301328 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1329 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301330 conditions += (
1331 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301332 (
1333 serial_no = {0}
1334 or serial_no like {1}
1335 or serial_no like {2}
1336 or serial_no like {3}
1337 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301338 """
1339 ).format(
1340 frappe.db.escape(serial_no),
1341 frappe.db.escape("{}\n%".format(serial_no)),
1342 frappe.db.escape("%\n{}".format(serial_no)),
1343 frappe.db.escape("%\n{}\n%".format(serial_no)),
1344 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301345
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301346 if not previous_sle.get("posting_date"):
1347 previous_sle["posting_date"] = "1900-01-01"
1348 if not previous_sle.get("posting_time"):
1349 previous_sle["posting_time"] = "00:00"
1350
1351 if operator in (">", "<=") and previous_sle.get("name"):
1352 conditions += " and name!=%(name)s"
1353
Ankush Menat494bd9e2022-03-28 18:52:46 +05301354 return frappe.db.sql(
1355 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301356 select *, timestamp(posting_date, posting_time) as "timestamp"
1357 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301358 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301359 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301360 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301361 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301362 %(limit)s %(for_update)s"""
1363 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301364 "conditions": conditions,
1365 "limit": limit or "",
1366 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301367 "order": order,
1368 },
1369 previous_sle,
1370 as_dict=1,
1371 debug=debug,
1372 )
1373
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301374
Nabin Haita77b8c92020-12-21 14:45:50 +05301375def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301376 return frappe.db.get_value(
1377 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301378 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301379 [
1380 "item_code",
1381 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301382 "actual_qty",
1383 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301384 "posting_date",
1385 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301386 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301387 "timestamp(posting_date, posting_time) as timestamp",
1388 ],
1389 as_dict=1,
1390 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301391
Ankush Menatce0514c2022-02-15 11:41:41 +05301392
Ankush Menat494bd9e2022-03-28 18:52:46 +05301393def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301394 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301395):
1396
Ankush Menat102fff22022-02-19 15:51:04 +05301397 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301398 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301399
Ankush Menate1c16872022-04-21 20:01:48 +05301400 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301401 posting_date, posting_time
1402 )
Ankush Menat102fff22022-02-19 15:51:04 +05301403 if creation:
1404 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301405 CombineDatetime(sle.posting_date, sle.posting_time)
1406 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301407 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301408
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301409 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301410 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301411 )
1412
Ankush Menat102fff22022-02-19 15:51:04 +05301413 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301414 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301415 .inner_join(batch_ledger)
1416 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1417 .select(
1418 Sum(
1419 Case()
1420 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1421 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1422 ).as_("batch_value"),
1423 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1424 "batch_qty"
1425 ),
1426 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301427 .where(
1428 (sle.item_code == item_code)
1429 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301430 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301431 & (sle.is_cancelled == 0)
1432 )
1433 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301434 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301435
1436 if batch_details and batch_details[0].batch_qty:
1437 return batch_details[0].batch_value / batch_details[0].batch_qty
1438
1439
Ankush Menat494bd9e2022-03-28 18:52:46 +05301440def get_valuation_rate(
1441 item_code,
1442 warehouse,
1443 voucher_type,
1444 voucher_no,
1445 allow_zero_rate=False,
1446 currency=None,
1447 company=None,
1448 raise_error_if_no_rate=True,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301449 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301450):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301451
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301452 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1453
Ankush Menatf7ffe042021-11-01 13:21:14 +05301454 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301455 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301456
Ankush Menat342d09a2022-02-19 14:28:51 +05301457 last_valuation_rate = None
1458
1459 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301460 if warehouse and serial_and_batch_bundle:
1461 batch_obj = BatchNoValuation(
1462 sle=frappe._dict(
1463 {
1464 "item_code": item_code,
1465 "warehouse": warehouse,
1466 "actual_qty": -1,
1467 "serial_and_batch_bundle": serial_and_batch_bundle,
1468 }
1469 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301470 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301471
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301472 return batch_obj.get_incoming_rate()
1473
Ankush Menatf7ffe042021-11-01 13:21:14 +05301474 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301475 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301476 last_valuation_rate = frappe.db.sql(
1477 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301478 from `tabStock Ledger Entry` force index (item_warehouse)
1479 where
1480 item_code = %s
1481 AND warehouse = %s
1482 AND valuation_rate >= 0
1483 AND is_cancelled = 0
1484 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301485 order by posting_date desc, posting_time desc, name desc limit 1""",
1486 (item_code, warehouse, voucher_no, voucher_type),
1487 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301488
Nabin Haita645f362018-03-01 10:31:24 +05301489 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301490 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301491
1492 # If negative stock allowed, and item delivered without any incoming entry,
1493 # system does not found any SLE, then take valuation rate from Item
1494 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301495
1496 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301497 # try Item Standard rate
1498 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301499
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301500 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301501 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301502 valuation_rate = frappe.db.get_value(
1503 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1504 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301505
Ankush Menat494bd9e2022-03-28 18:52:46 +05301506 if (
1507 not allow_zero_rate
1508 and not valuation_rate
1509 and raise_error_if_no_rate
1510 and cint(erpnext.is_perpetual_inventory_enabled(company))
1511 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301512 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301513
Ankush Menat494bd9e2022-03-28 18:52:46 +05301514 message = _(
1515 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1516 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301517 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301518 solutions = (
1519 "<li>"
1520 + _(
1521 "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table."
1522 ).format(voucher_type)
1523 + "</li>"
1524 )
1525 solutions += (
1526 "<li>"
1527 + _("If not, you can Cancel / Submit this entry")
1528 + " {0} ".format(frappe.bold("after"))
1529 + _("performing either one below:")
1530 + "</li>"
1531 )
Marica97715f22020-05-11 20:45:37 +05301532 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1533 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1534 msg = message + solutions + sub_solutions + "</li>"
1535
1536 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301537
1538 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301539
Ankush Menat494bd9e2022-03-28 18:52:46 +05301540
Ankush Menate7109c12021-08-26 16:40:45 +05301541def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301542 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301543 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301544 qty_shift = args.actual_qty
1545
Ankush Menat7c839c42022-05-06 12:09:08 +05301546 args["time_format"] = "%H:%i:%s"
1547
marination8418c4b2021-06-22 21:35:25 +05301548 # find difference/shift in qty caused by stock reconciliation
1549 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301550 qty_shift = get_stock_reco_qty_shift(args)
1551
1552 # find the next nearest stock reco so that we only recalculate SLEs till that point
1553 next_stock_reco_detail = get_next_stock_reco(args)
1554 if next_stock_reco_detail:
1555 detail = next_stock_reco_detail[0]
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301556 if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no):
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301557 regenerate_sle_for_batch_stock_reco(detail)
1558
marination40389772021-07-02 17:13:45 +05301559 # add condition to update SLEs before this date & time
1560 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301561
Ankush Menat494bd9e2022-03-28 18:52:46 +05301562 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301563 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301564 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301565 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301566 where
1567 item_code = %(item_code)s
1568 and warehouse = %(warehouse)s
1569 and voucher_no != %(voucher_no)s
1570 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301571 and (
1572 posting_date > %(posting_date)s or
1573 (
1574 posting_date = %(posting_date)s and
1575 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1576 )
1577 )
marination40389772021-07-02 17:13:45 +05301578 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301579 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301580 args,
1581 )
Nabin Hait186a0452021-02-18 14:14:21 +05301582
1583 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1584
Ankush Menat494bd9e2022-03-28 18:52:46 +05301585
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301586def regenerate_sle_for_batch_stock_reco(detail):
1587 doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301588 doc.recalculate_current_qty(detail.item_code, detail.batch_no)
Rohit Waghchauref2253dd2023-04-22 11:16:12 +05301589
1590 if not frappe.db.exists(
1591 "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
1592 ):
Rohit Waghchaure2d5ccc02023-05-01 21:17:18 +05301593 doc.repost_future_sle_and_gle(force=True)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301594
1595
marination40389772021-07-02 17:13:45 +05301596def get_stock_reco_qty_shift(args):
1597 stock_reco_qty_shift = 0
1598 if args.get("is_cancelled"):
1599 if args.get("previous_qty_after_transaction"):
1600 # get qty (balance) that was set at submission
1601 last_balance = args.get("previous_qty_after_transaction")
1602 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1603 else:
1604 stock_reco_qty_shift = flt(args.actual_qty)
1605 else:
1606 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301607 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301608 "qty_after_transaction"
1609 )
marination40389772021-07-02 17:13:45 +05301610
1611 if last_balance is not None:
1612 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1613 else:
1614 stock_reco_qty_shift = args.qty_after_transaction
1615
1616 return stock_reco_qty_shift
1617
Ankush Menat494bd9e2022-03-28 18:52:46 +05301618
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301619def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301620 """Returns next nearest stock reconciliaton's details."""
1621
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301622 sle = frappe.qb.DocType("Stock Ledger Entry")
1623
1624 query = (
1625 frappe.qb.from_(sle)
1626 .select(
1627 sle.name,
1628 sle.posting_date,
1629 sle.posting_time,
1630 sle.creation,
1631 sle.voucher_no,
1632 sle.item_code,
1633 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301634 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301635 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301636 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301637 )
1638 .where(
1639 (sle.item_code == kwargs.get("item_code"))
1640 & (sle.warehouse == kwargs.get("warehouse"))
1641 & (sle.voucher_type == "Stock Reconciliation")
1642 & (sle.voucher_no != kwargs.get("voucher_no"))
1643 & (sle.is_cancelled == 0)
1644 & (
1645 (
1646 CombineDatetime(sle.posting_date, sle.posting_time)
1647 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301648 )
1649 | (
1650 (
1651 CombineDatetime(sle.posting_date, sle.posting_time)
1652 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301653 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301654 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301655 )
1656 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301657 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301658 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1659 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301660 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301661 )
1662
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301663 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301664 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301665
1666 return query.run(as_dict=True)
1667
marination40389772021-07-02 17:13:45 +05301668
1669def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301670 return f"""
1671 and
1672 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1673 or (
1674 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1675 and creation < '{detail.creation}'
1676 )
1677 )"""
1678
Ankush Menat494bd9e2022-03-28 18:52:46 +05301679
Ankush Menate7109c12021-08-26 16:40:45 +05301680def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301681 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301682 return
1683 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1684 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301685
Ankush Menat5eba5752021-12-07 23:03:52 +05301686 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301687
1688 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301689 message = _(
1690 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1691 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301692 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301693 frappe.get_desk_link("Item", args.item_code),
1694 frappe.get_desk_link("Warehouse", args.warehouse),
1695 neg_sle[0]["posting_date"],
1696 neg_sle[0]["posting_time"],
1697 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1698 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301699
Ankush Menat494bd9e2022-03-28 18:52:46 +05301700 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301701
1702 if not args.batch_no:
1703 return
1704
1705 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301706 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301707 message = _(
1708 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1709 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301710 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301711 frappe.get_desk_link("Batch", args.batch_no),
1712 frappe.get_desk_link("Warehouse", args.warehouse),
1713 neg_batch_sle[0]["posting_date"],
1714 neg_batch_sle[0]["posting_time"],
1715 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1716 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301717 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301718
Nabin Haita77b8c92020-12-21 14:45:50 +05301719
Maricad6078aa2022-06-17 15:13:13 +05301720def is_negative_with_precision(neg_sle, is_batch=False):
1721 """
1722 Returns whether system precision rounded qty is insufficient.
1723 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1724 """
1725
1726 if not neg_sle:
1727 return False
1728
1729 field = "cumulative_total" if is_batch else "qty_after_transaction"
1730 precision = cint(frappe.db.get_default("float_precision")) or 2
1731 qty_deficit = flt(neg_sle[0][field], precision)
1732
1733 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1734
1735
Nabin Haita77b8c92020-12-21 14:45:50 +05301736def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301737 return frappe.db.sql(
1738 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301739 select
1740 qty_after_transaction, posting_date, posting_time,
1741 voucher_type, voucher_no
1742 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301743 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301744 item_code = %(item_code)s
1745 and warehouse = %(warehouse)s
1746 and voucher_no != %(voucher_no)s
1747 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1748 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301749 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301750 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301751 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301752 """,
1753 args,
1754 as_dict=1,
1755 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301756
Ankush Menat5eba5752021-12-07 23:03:52 +05301757
1758def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301759 return frappe.db.sql(
1760 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301761 with batch_ledger as (
1762 select
1763 posting_date, posting_time, voucher_type, voucher_no,
1764 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1765 from `tabStock Ledger Entry`
1766 where
1767 item_code = %(item_code)s
1768 and warehouse = %(warehouse)s
1769 and batch_no=%(batch_no)s
1770 and is_cancelled = 0
1771 order by posting_date, posting_time, creation
1772 )
1773 select * from batch_ledger
1774 where
1775 cumulative_total < 0.0
1776 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1777 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301778 """,
1779 args,
1780 as_dict=1,
1781 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301782
1783
1784def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1785 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1786 return True
1787 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1788 return True
1789 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301790
1791
1792def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1793 """
1794 For inter company transfer, incoming rate is the average of the outgoing rate
1795 """
1796 rate = 0.0
1797
1798 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1799
1800 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1801
1802 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1803
1804 if reference_name:
1805 rate = frappe.get_cached_value(
1806 doctype,
1807 reference_name,
1808 "incoming_rate",
1809 )
1810
1811 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301812
1813
1814def is_internal_transfer(sle):
1815 data = frappe.get_cached_value(
1816 sle.voucher_type,
1817 sle.voucher_no,
1818 ["is_internal_supplier", "represents_company", "company"],
1819 as_dict=True,
1820 )
1821
1822 if data.is_internal_supplier and data.represents_company == data.company:
1823 return True