blob: 551701b47a6b9bbcab9a06ec94891d4c721c8505 [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
Akhil Narang21c3d9c2023-10-21 11:19:45 +05305import gzip
Nabin Hait26d46552013-01-09 15:23:05 +05306import json
Ankush Menatecdb4932022-04-17 19:06:13 +05307from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05308
9import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053010from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053011from frappe.model.meta import get_field_precision
Rohit Waghchaurebb954512023-06-02 00:11:43 +053012from frappe.query_builder import Case
Ankush Menate1c16872022-04-21 20:01:48 +053013from frappe.query_builder.functions import CombineDatetime, Sum
Akhil Narang21c3d9c2023-10-21 11:19:45 +053014from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json
Achilles Rasquinha361366e2018-02-14 17:08:59 +053015
Chillar Anand915b3432021-09-02 16:44:59 +053016import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053017from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
mergify[bot]27a1e3b2023-10-16 19:15:18 +053018from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
s-aga-rf0acb202023-04-12 14:13:54 +053019from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
20 get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
21)
Chillar Anand915b3432021-09-02 16:44:59 +053022from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053023 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053024 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053025 get_valuation_method,
26)
Ankush Menatb534fee2022-02-19 20:58:36 +053027from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053028
Nabin Hait97bce3a2021-07-12 13:24:43 +053029
Ankush Menat494bd9e2022-03-28 18:52:46 +053030class NegativeStockError(frappe.ValidationError):
31 pass
32
33
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053034class SerialNoExistsInFutureTransaction(frappe.ValidationError):
35 pass
Nabin Hait902e8602013-01-08 18:29:24 +053036
Anand Doshi5b004ff2013-09-25 19:55:41 +053037
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053038def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053039 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053040
Ankush Menat494bd9e2022-03-28 18:52:46 +053041 args:
42 - allow_negative_stock: disable negative stock valiations if true
43 - via_landed_cost_voucher: landed cost voucher cancels and reposts
44 entries of purchase document. This flag is used to identify if
45 cancellation and repost is happening via landed cost voucher, in
46 such cases certain validations need to be ignored (like negative
47 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053048 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053049 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053050
Nabin Haitca775742013-09-26 16:16:44 +053051 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053052 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053053 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053054 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053055 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053056
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053057 args = get_args_for_future_sle(sl_entries[0])
58 future_sle_exists(args, sl_entries)
59
Nabin Haitca775742013-09-26 16:16:44 +053060 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053061 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053062 validate_serial_no(sle)
63
Nabin Haita77b8c92020-12-21 14:45:50 +053064 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053065 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053066
Ankush Menat494bd9e2022-03-28 18:52:46 +053067 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
68 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
69 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
70 )
71 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053072
Ankush Menat494bd9e2022-03-28 18:52:46 +053073 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
74 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
75 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
76 )
77 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053078
Ankush Menat494bd9e2022-03-28 18:52:46 +053079 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053080 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053081
Nabin Haita77b8c92020-12-21 14:45:50 +053082 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053083
84 if sle.get("voucher_type") == "Stock Reconciliation":
85 # preserve previous_qty_after_transaction for qty reposting
86 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
87
Ankush Menat494bd9e2022-03-28 18:52:46 +053088 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053089 if is_stock_item:
90 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053091 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053092 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053093 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +053094 frappe.msgprint(
95 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
96 )
97
Ankush Menatcef84c22021-12-03 12:18:59 +053098
99def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
100 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
101 if not args.get("posting_date"):
102 args["posting_date"] = nowdate()
103
marination7a5fd712022-07-04 17:46:54 +0530104 if not (args.get("is_cancelled") and via_landed_cost_voucher):
105 # Reposts only current voucher SL Entries
106 # Updates valuation rate, stock value, stock queue for current transaction
107 update_entries_after(
108 {
109 "item_code": args.get("item_code"),
110 "warehouse": args.get("warehouse"),
111 "posting_date": args.get("posting_date"),
112 "posting_time": args.get("posting_time"),
113 "voucher_type": args.get("voucher_type"),
114 "voucher_no": args.get("voucher_no"),
115 "sle_id": args.get("name"),
116 "creation": args.get("creation"),
117 },
118 allow_negative_stock=allow_negative_stock,
119 via_landed_cost_voucher=via_landed_cost_voucher,
120 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530121
122 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530123 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530124 update_qty_in_future_sle(args, allow_negative_stock)
125
Nabin Haitadeb9762014-10-06 11:53:52 +0530126
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530127def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530128 return frappe._dict(
129 {
130 "voucher_type": row.get("voucher_type"),
131 "voucher_no": row.get("voucher_no"),
132 "posting_date": row.get("posting_date"),
133 "posting_time": row.get("posting_time"),
134 }
135 )
136
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530137
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530138def validate_serial_no(sle):
139 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530140
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530141 for sn in get_serial_nos(sle.serial_no):
142 args = copy.deepcopy(sle)
143 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530144 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530145
146 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530147 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530148 voucher_type = frappe.bold(row.voucher_type)
149 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530150 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530151
152 if vouchers:
153 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530154 msg = (
155 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
156 The list of the transactions are as below."""
157 + "<br><br><ul><li>"
158 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530159
Ankush Menat494bd9e2022-03-28 18:52:46 +0530160 msg += "</li><li>".join(vouchers)
161 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530162
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530164 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
165
Ankush Menat494bd9e2022-03-28 18:52:46 +0530166
Nabin Hait186a0452021-02-18 14:14:21 +0530167def validate_cancellation(args):
168 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530169 repost_entry = frappe.db.get_value(
170 "Repost Item Valuation",
171 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
172 ["name", "status"],
173 as_dict=1,
174 )
Nabin Hait186a0452021-02-18 14:14:21 +0530175
176 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530177 if repost_entry.status == "In Progress":
178 frappe.throw(
179 _(
180 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
181 )
182 )
183 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530184 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530185 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530186 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530187 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530188
Ankush Menat494bd9e2022-03-28 18:52:46 +0530189
Nabin Hait9653f602013-08-20 15:37:33 +0530190def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530191 frappe.db.sql(
192 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530193 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530194 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530195 (now(), frappe.session.user, voucher_type, voucher_no),
196 )
197
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530198
Nabin Hait54c865e2015-03-27 15:38:31 +0530199def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530200 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530201 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530202 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530203 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530204 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530205 sle.submit()
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530206
207 # Added to handle the case when the stock ledger entry is created from the repostig
208 if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
209 sle.db_set("creation", args.get("creation_time"))
210
Nabin Haita77b8c92020-12-21 14:45:50 +0530211 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530212
Ankush Menat494bd9e2022-03-28 18:52:46 +0530213
214def repost_future_sle(
215 args=None,
216 voucher_type=None,
217 voucher_no=None,
218 allow_negative_stock=None,
219 via_landed_cost_voucher=False,
220 doc=None,
221):
Nabin Haite1fa7232022-07-20 15:19:09 +0530222 if not args:
223 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530224
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530225 reposting_data = {}
226 if doc and doc.reposting_data_file:
227 reposting_data = get_reposting_data(doc.reposting_data_file)
228
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530229 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530230 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530231 )
232 if items_to_be_repost:
233 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530234
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530235 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
236 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530237
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530238 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530239 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530240 validate_item_warehouse(args[i])
241
Ankush Menat494bd9e2022-03-28 18:52:46 +0530242 obj = update_entries_after(
243 {
244 "item_code": args[i].get("item_code"),
245 "warehouse": args[i].get("warehouse"),
246 "posting_date": args[i].get("posting_date"),
247 "posting_time": args[i].get("posting_time"),
248 "creation": args[i].get("creation"),
249 "distinct_item_warehouses": distinct_item_warehouses,
250 },
251 allow_negative_stock=allow_negative_stock,
252 via_landed_cost_voucher=via_landed_cost_voucher,
253 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530254 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530255
Ankush Menat494bd9e2022-03-28 18:52:46 +0530256 distinct_item_warehouses[
257 (args[i].get("item_code"), args[i].get("warehouse"))
258 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530259
Nabin Hait97bce3a2021-07-12 13:24:43 +0530260 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530261 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530262 if ("args_idx" not in data and not data.reposting_status) or (
263 data.sle_changed and data.reposting_status
264 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530265 data.args_idx = len(args)
266 args.append(data.sle)
267 elif data.sle_changed and not data.reposting_status:
268 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530269
Nabin Hait97bce3a2021-07-12 13:24:43 +0530270 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530271 i += 1
272
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530273 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530274 update_args_in_repost_item_valuation(
275 doc, i, args, distinct_item_warehouses, affected_transactions
276 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530277
Ankush Menat494bd9e2022-03-28 18:52:46 +0530278
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530279def get_reposting_data(file_path) -> dict:
280 file_name = frappe.db.get_value(
281 "File",
282 {
283 "file_url": file_path,
284 "attached_to_field": "reposting_data_file",
285 },
286 "name",
287 )
288
289 if not file_name:
290 return frappe._dict()
291
292 attached_file = frappe.get_doc("File", file_name)
293
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530294 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530295 if data := json.loads(data.decode("utf-8")):
296 data = data
297
298 return parse_json(data)
299
300
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530301def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530302 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530303 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530304 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530305 frappe.throw(_(validation_msg))
306
Ankush Menat494bd9e2022-03-28 18:52:46 +0530307
Ankush Menatecdb4932022-04-17 19:06:13 +0530308def update_args_in_repost_item_valuation(
309 doc, index, args, distinct_item_warehouses, affected_transactions
310):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530311 if not doc.items_to_be_repost:
312 file_name = ""
313 if doc.reposting_data_file:
314 file_name = get_reposting_file_name(doc.doctype, doc.name)
315 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
316
317 doc.reposting_data_file = create_json_gz_file(
318 {
319 "items_to_be_repost": args,
320 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
321 "affected_transactions": affected_transactions,
322 },
323 doc,
324 file_name,
325 )
326
327 doc.db_set(
328 {
329 "current_index": index,
330 "total_reposting_count": len(args),
331 "reposting_data_file": doc.reposting_data_file,
332 }
333 )
334
335 else:
336 doc.db_set(
337 {
338 "items_to_be_repost": json.dumps(args, default=str),
339 "distinct_item_and_warehouse": json.dumps(
340 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
341 ),
342 "current_index": index,
343 "affected_transactions": frappe.as_json(affected_transactions),
344 }
345 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530346
Ankush Menatecdb4932022-04-17 19:06:13 +0530347 if not frappe.flags.in_test:
348 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530349
Ankush Menat494bd9e2022-03-28 18:52:46 +0530350 frappe.publish_realtime(
351 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530352 {
353 "name": doc.name,
354 "items_to_be_repost": json.dumps(args, default=str),
355 "current_index": index,
356 "total_reposting_count": len(args),
357 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530358 doctype=doc.doctype,
359 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530360 )
361
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530362
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530363def get_reposting_file_name(dt, dn):
364 return frappe.db.get_value(
365 "File",
366 {
367 "attached_to_doctype": dt,
368 "attached_to_name": dn,
369 "attached_to_field": "reposting_data_file",
370 },
371 "name",
372 )
373
374
375def create_json_gz_file(data, doc, file_name=None) -> str:
376 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530377 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530378
379 if not file_name:
380 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
381 _file = frappe.get_doc(
382 {
383 "doctype": "File",
384 "file_name": json_filename,
385 "attached_to_doctype": doc.doctype,
386 "attached_to_name": doc.name,
387 "attached_to_field": "reposting_data_file",
388 "content": compressed_content,
389 "is_private": 1,
390 }
391 )
392 _file.save(ignore_permissions=True)
393
394 return _file.file_url
395 else:
396 file_doc = frappe.get_doc("File", file_name)
397 path = file_doc.get_full_path()
398
399 with open(path, "wb") as f:
400 f.write(compressed_content)
401
402 return doc.reposting_data_file
403
404
405def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
406 if not reposting_data and doc and doc.reposting_data_file:
407 reposting_data = get_reposting_data(doc.reposting_data_file)
408
409 if reposting_data and reposting_data.items_to_be_repost:
410 return reposting_data.items_to_be_repost
411
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530412 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530413
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530414 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530415 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530416
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530417 if not items_to_be_repost and voucher_type and voucher_no:
418 items_to_be_repost = frappe.db.get_all(
419 "Stock Ledger Entry",
420 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
421 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
422 order_by="creation asc",
423 group_by="item_code, warehouse",
424 )
425
Nabin Haite1fa7232022-07-20 15:19:09 +0530426 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530427
Ankush Menat494bd9e2022-03-28 18:52:46 +0530428
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530429def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
430 if not reposting_data and doc and doc.reposting_data_file:
431 reposting_data = get_reposting_data(doc.reposting_data_file)
432
433 if reposting_data and reposting_data.distinct_item_and_warehouse:
434 return reposting_data.distinct_item_and_warehouse
435
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530436 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530437
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530438 if doc and doc.distinct_item_and_warehouse:
439 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530440 distinct_item_warehouses = {
441 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
442 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530443 else:
444 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530445 distinct_item_warehouses.setdefault(
446 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
447 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530448
449 return distinct_item_warehouses
450
Ankush Menat494bd9e2022-03-28 18:52:46 +0530451
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530452def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
453 if not reposting_data and doc and doc.reposting_data_file:
454 reposting_data = get_reposting_data(doc.reposting_data_file)
455
456 if reposting_data and reposting_data.affected_transactions:
457 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
458
Ankush Menatecdb4932022-04-17 19:06:13 +0530459 if not doc.affected_transactions:
460 return set()
461
462 transactions = frappe.parse_json(doc.affected_transactions)
463 return {tuple(transaction) for transaction in transactions}
464
465
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530466def get_current_index(doc=None):
467 if doc and doc.current_index:
468 return doc.current_index
469
Ankush Menat494bd9e2022-03-28 18:52:46 +0530470
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530471class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530472 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530473 update valution rate and qty after transaction
474 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530475
Ankush Menat494bd9e2022-03-28 18:52:46 +0530476 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530477
Ankush Menat494bd9e2022-03-28 18:52:46 +0530478 args = {
479 "item_code": "ABC",
480 "warehouse": "XYZ",
481 "posting_date": "2012-12-12",
482 "posting_time": "12:00"
483 }
Nabin Hait902e8602013-01-08 18:29:24 +0530484 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530485
486 def __init__(
487 self,
488 args,
489 allow_zero_rate=False,
490 allow_negative_stock=None,
491 via_landed_cost_voucher=False,
492 verbose=1,
493 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530494 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530495 self.verbose = verbose
496 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530497 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530498 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530499 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
500 item_code=self.item_code
501 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530502
Nabin Haita77b8c92020-12-21 14:45:50 +0530503 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530504 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530505 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530506
Nabin Haita77b8c92020-12-21 14:45:50 +0530507 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530508 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530509 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530510
511 self.new_items_found = False
512 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530513 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-rf0acb202023-04-12 14:13:54 +0530514 self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
Nabin Haita77b8c92020-12-21 14:45:50 +0530515
516 self.data = frappe._dict()
517 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530518 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530519
Maricad6078aa2022-06-17 15:13:13 +0530520 def set_precision(self):
521 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
522 self.currency_precision = get_field_precision(
523 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530524 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530525
526 def initialize_previous_data(self, args):
527 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530528 Get previous sl entries for current item for each related warehouse
529 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530530
Ankush Menat494bd9e2022-03-28 18:52:46 +0530531 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530532
Ankush Menat494bd9e2022-03-28 18:52:46 +0530533 self.data = {
534 warehouse1: {
535 'previus_sle': {},
536 'qty_after_transaction': 10,
537 'valuation_rate': 100,
538 'stock_value': 1000,
539 'prev_stock_value': 1000,
540 'stock_queue': '[[10, 100]]',
541 'stock_value_difference': 1000
542 }
543 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530544
545 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530546 self.data.setdefault(args.warehouse, frappe._dict())
547 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530548 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530549 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530550
Ankush Menatc1d986a2021-08-31 19:43:42 +0530551 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
552 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
553
Ankush Menat494bd9e2022-03-28 18:52:46 +0530554 warehouse_dict.update(
555 {
556 "prev_stock_value": previous_sle.stock_value or 0.0,
557 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
558 "stock_value_difference": 0.0,
559 }
560 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530561
Nabin Haita77b8c92020-12-21 14:45:50 +0530562 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530563 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530564
Nabin Haita77b8c92020-12-21 14:45:50 +0530565 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530566 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530567 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530568 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530569 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530570 entries_to_fix = self.get_future_entries_to_fix()
571
572 i = 0
573 while i < len(entries_to_fix):
574 sle = entries_to_fix[i]
575 i += 1
576
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530577 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530578 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530579
Nabin Haita77b8c92020-12-21 14:45:50 +0530580 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530581 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530582
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530583 if self.exceptions:
584 self.raise_exceptions()
585
Nabin Hait186a0452021-02-18 14:14:21 +0530586 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530587 sl_entries = self.get_sle_against_current_voucher()
588 for sle in sl_entries:
589 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530590
Nabin Haita77b8c92020-12-21 14:45:50 +0530591 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530592 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530593
Ankush Menat494bd9e2022-03-28 18:52:46 +0530594 return frappe.db.sql(
595 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530596 select
597 *, timestamp(posting_date, posting_time) as "timestamp"
598 from
599 `tabStock Ledger Entry`
600 where
601 item_code = %(item_code)s
602 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530603 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530604 and (
605 posting_date = %(posting_date)s and
606 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
607 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530608 order by
609 creation ASC
610 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530611 """,
612 self.args,
613 as_dict=1,
614 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530615
Nabin Haita77b8c92020-12-21 14:45:50 +0530616 def get_future_entries_to_fix(self):
617 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530618 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
619 {"item_code": self.item_code, "warehouse": self.args.warehouse}
620 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530621
Nabin Haita77b8c92020-12-21 14:45:50 +0530622 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530623
Nabin Haita77b8c92020-12-21 14:45:50 +0530624 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530625 dependant_sle = get_sle_by_voucher_detail_no(
626 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
627 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530628
Nabin Haita77b8c92020-12-21 14:45:50 +0530629 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530630 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530631 elif (
632 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
633 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530634 return entries_to_fix
635 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530636 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530637 return entries_to_fix
638 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
639 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530640 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530641 self.initialize_previous_data(dependant_sle)
642 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530643 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530644
645 def update_distinct_item_warehouses(self, dependant_sle):
646 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530647 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530648
Nabin Hait97bce3a2021-07-12 13:24:43 +0530649 if key not in self.distinct_item_warehouses:
650 self.distinct_item_warehouses[key] = val
651 self.new_items_found = True
652 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530653 existing_sle_posting_date = (
654 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
655 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530656
657 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
658
Nabin Hait97bce3a2021-07-12 13:24:43 +0530659 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
660 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530661 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
662 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530663 self.distinct_item_warehouses[key] = val
664 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530665 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
666 # Future dependent voucher needs to be repost to get the correct stock value
667 # If dependent voucher has not reposted, then add it to the list
668 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530669 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530670 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
671 self.distinct_item_warehouses[key] = val
672
673 def get_dependent_voucher_detail_nos(self, key):
674 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
675 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
676
677 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530678
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530679 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530680 # previous sle data for this warehouse
681 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530682 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530683
Anand Doshi0dc79f42015-04-06 12:59:34 +0530684 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 +0530685 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530686 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530687 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530688 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530689 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530690
Nabin Haita77b8c92020-12-21 14:45:50 +0530691 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530692 if not self.args.get("sle_id"):
693 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530694
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530695 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530696 sle.voucher_type == "Stock Reconciliation"
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530697 and (
698 sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
699 )
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530700 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530701 and not self.args.get("sle_id")
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530702 ):
703 self.reset_actual_qty_for_stock_reco(sle)
704
705 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530706 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
707 and sle.voucher_detail_no
708 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530709 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530710 ):
711 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
712
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530713 dimensions = get_inventory_dimensions()
714 has_dimensions = False
715 if dimensions:
716 for dimension in dimensions:
717 if sle.get(dimension.get("fieldname")):
718 has_dimensions = True
719
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530720 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530721 self.calculate_valuation_for_serial_batch_bundle(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530722 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530723 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530724 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530725 self.wh_data.valuation_rate = sle.valuation_rate
726 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530727 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
728 self.wh_data.valuation_rate
729 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530730 if self.valuation_method != "Moving Average":
731 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530732 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530733 if self.valuation_method == "Moving Average":
734 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530735 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530736 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
737 self.wh_data.valuation_rate
738 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530739 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530740 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530741
Rushabh Mehta54047782013-12-26 11:07:46 +0530742 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530743 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530744 if not self.wh_data.qty_after_transaction:
745 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530746
Nabin Haita77b8c92020-12-21 14:45:50 +0530747 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
748 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530749
Nabin Hait902e8602013-01-08 18:29:24 +0530750 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530751 sle.qty_after_transaction = self.wh_data.qty_after_transaction
752 sle.valuation_rate = self.wh_data.valuation_rate
753 sle.stock_value = self.wh_data.stock_value
754 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530755 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530756 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530757
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530758 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530759
Ankush Menat701878f2022-03-01 18:08:29 +0530760 if not self.args.get("sle_id"):
761 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530762
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530763 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530764 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
765 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
766
767 if sle.actual_qty < 0:
768 sle.actual_qty = (
769 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
770 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530771 )
772
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530773 if abs(sle.actual_qty) == 0.0:
774 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530775
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530776 def calculate_valuation_for_serial_batch_bundle(self, sle):
777 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
778
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530779 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530780 doc.calculate_qty_and_amount(save=True)
781
782 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
783
784 self.wh_data.qty_after_transaction += doc.total_qty
785 if self.wh_data.qty_after_transaction:
786 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
787
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530788 def validate_negative_stock(self, sle):
789 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530790 validate negative stock for entries current datetime onwards
791 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530792 """
s-aga-rf0acb202023-04-12 14:13:54 +0530793 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530794 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530795
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530796 if diff < 0 and abs(diff) > 0.0001:
797 # negative stock!
798 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530799 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530800 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530801 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530802 return True
803
Nabin Haita77b8c92020-12-21 14:45:50 +0530804 def get_dynamic_incoming_outgoing_rate(self, sle):
805 # Get updated incoming/outgoing rate from transaction
806 if sle.recalculate_rate:
807 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
808
809 if flt(sle.actual_qty) >= 0:
810 sle.incoming_rate = rate
811 else:
812 sle.outgoing_rate = rate
813
814 def get_incoming_outgoing_rate_from_transaction(self, sle):
815 rate = 0
816 # Material Transfer, Repack, Manufacturing
817 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530818 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530819 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
820 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530821 elif sle.voucher_type in (
822 "Purchase Receipt",
823 "Purchase Invoice",
824 "Delivery Note",
825 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530826 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530827 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530828 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530829 from erpnext.controllers.sales_and_purchase_return import (
830 get_rate_for_return, # don't move this import to top
831 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530832
833 rate = get_rate_for_return(
834 sle.voucher_type,
835 sle.voucher_no,
836 sle.item_code,
837 voucher_detail_no=sle.voucher_detail_no,
838 sle=sle,
839 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530840
841 elif (
842 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530843 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530844 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530845 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530846 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530847 else:
848 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530849 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530850 elif sle.voucher_type == "Subcontracting Receipt":
851 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530852 else:
853 rate_field = "incoming_rate"
854
855 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530856 item_code, incoming_rate = frappe.db.get_value(
857 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
858 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530859
860 if item_code == sle.item_code:
861 rate = incoming_rate
862 else:
863 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
864 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530865 elif sle == "Subcontracting Receipt":
866 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530867 else:
868 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530869
Ankush Menat494bd9e2022-03-28 18:52:46 +0530870 rate = frappe.db.get_value(
871 ref_doctype,
872 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
873 rate_field,
874 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530875
876 return rate
877
878 def update_outgoing_rate_on_transaction(self, sle):
879 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530880 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
881 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530882 """
883 if sle.actual_qty and sle.voucher_detail_no:
884 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
885
886 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
887 self.update_rate_on_stock_entry(sle, outgoing_rate)
888 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
889 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
890 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
891 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530892 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
893 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530894 elif sle.voucher_type == "Stock Reconciliation":
895 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530896
897 def update_rate_on_stock_entry(self, sle, outgoing_rate):
898 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
899
Ankush Menat701878f2022-03-01 18:08:29 +0530900 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
901 if not sle.dependant_sle_voucher_detail_no:
902 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530903
904 def recalculate_amounts_in_stock_entry(self, voucher_no):
905 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530906 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
907 stock_entry.db_update()
908 for d in stock_entry.items:
909 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530910
Nabin Haita77b8c92020-12-21 14:45:50 +0530911 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
912 # Update item's incoming rate on transaction
913 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
914 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530915 frappe.db.set_value(
916 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
917 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530918 else:
919 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530920 frappe.db.set_value(
921 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530922 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530923 "incoming_rate",
924 outgoing_rate,
925 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530926
927 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
928 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530929 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
930 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
931 ):
932 frappe.db.set_value(
933 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
934 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530935 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530936 frappe.db.set_value(
937 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
938 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530939
940 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530941 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530942 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530943 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530944 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530945 d.db_update()
946
Sagar Sharma323bdf82022-05-17 15:14:07 +0530947 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530948 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
949 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530950 else:
951 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530952 "Subcontracting Receipt Supplied Item",
953 sle.voucher_detail_no,
954 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530955 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530956
s-aga-ra6cb6c62023-05-03 09:51:58 +0530957 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530958 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530959 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530960 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530961 d.db_update()
962
s-aga-r88a3f652023-05-30 16:54:28 +0530963 def update_rate_on_stock_reconciliation(self, sle):
964 if not sle.serial_no and not sle.batch_no:
965 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
966
967 for item in sr.items:
968 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +0530969 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +0530970 continue
971
972 previous_sle = get_previous_sle(
973 {
974 "item_code": item.item_code,
975 "warehouse": item.warehouse,
976 "posting_date": sr.posting_date,
977 "posting_time": sr.posting_time,
978 "sle": sle.name,
979 }
980 )
981
982 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
983 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
984 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
985
986 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +0530987 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +0530988 item.amount_difference = item.amount - item.current_amount
989 else:
990 sr.difference_amount = sum([item.amount_difference for item in sr.items])
991 sr.db_update()
992
993 for item in sr.items:
994 item.db_update()
995
Nabin Hait328c4f92020-01-02 19:00:32 +0530996 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
997 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530998 all_serial_nos = frappe.get_all(
999 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1000 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301001
Ankush Menat494bd9e2022-03-28 18:52:46 +05301002 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 +05301003
1004 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301005 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301006 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301007 incoming_rate = frappe.db.sql(
1008 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301009 select incoming_rate
1010 from `tabStock Ledger Entry`
1011 where
1012 company = %s
1013 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301014 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301015 and (serial_no = %s
1016 or serial_no like %s
1017 or serial_no like %s
1018 or serial_no like %s
1019 )
1020 order by posting_date desc
1021 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301022 """,
1023 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1024 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301025
1026 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1027
1028 return incoming_values
1029
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301030 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301031 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301032 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301033 if new_stock_qty >= 0:
1034 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301035 if flt(self.wh_data.qty_after_transaction) <= 0:
1036 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301037 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301038 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1039 actual_qty * sle.incoming_rate
1040 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301041
Nabin Haita77b8c92020-12-21 14:45:50 +05301042 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301043
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301044 elif sle.outgoing_rate:
1045 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301046 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1047 actual_qty * sle.outgoing_rate
1048 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301049
Nabin Haita77b8c92020-12-21 14:45:50 +05301050 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301051 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301052 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301053 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301054 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1055 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301056
Nabin Haita77b8c92020-12-21 14:45:50 +05301057 if not self.wh_data.valuation_rate and actual_qty > 0:
1058 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301059
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301060 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001061 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301062 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301063 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1064 sle.voucher_type, sle.voucher_detail_no
1065 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001066 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301067 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301068
Ankush Menatf089d392022-02-02 12:51:21 +05301069 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301070 incoming_rate = flt(sle.incoming_rate)
1071 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301072 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301073
Ankush Menat494bd9e2022-03-28 18:52:46 +05301074 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1075 self.wh_data.qty_after_transaction + actual_qty
1076 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301077
Ankush Menat97e18a12022-01-15 17:42:25 +05301078 if self.valuation_method == "LIFO":
1079 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1080 else:
1081 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1082
Ankush Menatb534fee2022-02-19 20:58:36 +05301083 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1084
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301085 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301086 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301087 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301088
Ankush Menat4b29fb62021-12-18 18:40:22 +05301089 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301090 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1091 sle.voucher_type, sle.voucher_detail_no
1092 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301093 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301094 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301095 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301096 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301097
Ankush Menat494bd9e2022-03-28 18:52:46 +05301098 stock_queue.remove_stock(
1099 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1100 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301101
Ankush Menatb534fee2022-02-19 20:58:36 +05301102 _qty, stock_value = stock_queue.get_total_stock_and_value()
1103
1104 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301105
Ankush Menat97e18a12022-01-15 17:42:25 +05301106 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301107 self.wh_data.stock_value = round_off_if_near_zero(
1108 self.wh_data.stock_value + stock_value_difference
1109 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301110
Nabin Haita77b8c92020-12-21 14:45:50 +05301111 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301112 self.wh_data.stock_queue.append(
1113 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1114 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301115
Ankush Menatb534fee2022-02-19 20:58:36 +05301116 if self.wh_data.qty_after_transaction:
1117 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1118
Ankush Menatce0514c2022-02-15 11:41:41 +05301119 def update_batched_values(self, sle):
1120 incoming_rate = flt(sle.incoming_rate)
1121 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301122
Ankush Menat494bd9e2022-03-28 18:52:46 +05301123 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1124 self.wh_data.qty_after_transaction + actual_qty
1125 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301126
1127 if actual_qty > 0:
1128 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301129 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301130 outgoing_rate = get_batch_incoming_rate(
1131 item_code=sle.item_code,
1132 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301133 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301134 posting_date=sle.posting_date,
1135 posting_time=sle.posting_time,
1136 creation=sle.creation,
1137 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301138 if outgoing_rate is None:
1139 # This can *only* happen if qty available for the batch is zero.
1140 # in such case fall back various other rates.
1141 # future entries will correct the overall accounting as each
1142 # batch individually uses moving average rates.
1143 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301144 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301145
Ankush Menat494bd9e2022-03-28 18:52:46 +05301146 self.wh_data.stock_value = round_off_if_near_zero(
1147 self.wh_data.stock_value + stock_value_difference
1148 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301149 if self.wh_data.qty_after_transaction:
1150 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301151
Javier Wong9b11d9b2017-04-14 18:24:04 +08001152 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301153 ref_item_dt = ""
1154
1155 if voucher_type == "Stock Entry":
1156 ref_item_dt = voucher_type + " Detail"
1157 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1158 ref_item_dt = voucher_type + " Item"
1159
1160 if ref_item_dt:
1161 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1162 else:
1163 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301164
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301165 def get_fallback_rate(self, sle) -> float:
1166 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301167 This should only get used for negative stock."""
1168 return get_valuation_rate(
1169 sle.item_code,
1170 sle.warehouse,
1171 sle.voucher_type,
1172 sle.voucher_no,
1173 self.allow_zero_rate,
1174 currency=erpnext.get_company_currency(sle.company),
1175 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301176 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301177
Nabin Haita77b8c92020-12-21 14:45:50 +05301178 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301179 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301180 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1181 sle = sle[0] if sle else frappe._dict()
1182 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301183
Nabin Haita77b8c92020-12-21 14:45:50 +05301184 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301185 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301186 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301187
1188 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301189 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301190 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301191 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301192
Ankush Menat494bd9e2022-03-28 18:52:46 +05301193 if (
1194 exceptions[0]["voucher_type"],
1195 exceptions[0]["voucher_no"],
1196 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301197
Nabin Haita77b8c92020-12-21 14:45:50 +05301198 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301199 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301200 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1201 frappe.get_desk_link("Warehouse", warehouse),
1202 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301203 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301204 msg = _(
1205 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1206 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301207 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301208 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1209 frappe.get_desk_link("Warehouse", warehouse),
1210 exceptions[0]["posting_date"],
1211 exceptions[0]["posting_time"],
1212 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1213 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301214
Nabin Haita77b8c92020-12-21 14:45:50 +05301215 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301216 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301217 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301218
1219 if allowed_qty > 0:
1220 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1221 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1222 )
1223 else:
1224 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1225 msg,
1226 )
s-aga-rf0acb202023-04-12 14:13:54 +05301227
Nabin Haita77b8c92020-12-21 14:45:50 +05301228 msg_list.append(msg)
1229
1230 if msg_list:
1231 message = "\n\n".join(msg_list)
1232 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301233 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301234 else:
1235 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301236
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301237 def update_bin_data(self, sle):
1238 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301239 values_to_update = {
1240 "actual_qty": sle.qty_after_transaction,
1241 "stock_value": sle.stock_value,
1242 }
1243
1244 if sle.valuation_rate is not None:
1245 values_to_update["valuation_rate"] = sle.valuation_rate
1246
1247 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301248
Nabin Haita77b8c92020-12-21 14:45:50 +05301249 def update_bin(self):
1250 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301251 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301252 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301253
Ankush Menat494bd9e2022-03-28 18:52:46 +05301254 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301255 if data.valuation_rate is not None:
1256 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301257 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301258
marination8418c4b2021-06-22 21:35:25 +05301259
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301260def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301261 """get stock ledger entries filtered by specific posting datetime conditions"""
1262
Ankush Menat494bd9e2022-03-28 18:52:46 +05301263 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301264 if not args.get("posting_date"):
1265 args["posting_date"] = "1900-01-01"
1266 if not args.get("posting_time"):
1267 args["posting_time"] = "00:00"
1268
1269 voucher_condition = ""
1270 if exclude_current_voucher:
1271 voucher_no = args.get("voucher_no")
1272 voucher_condition = f"and voucher_no != '{voucher_no}'"
1273
Ankush Menat494bd9e2022-03-28 18:52:46 +05301274 sle = frappe.db.sql(
1275 """
marination8418c4b2021-06-22 21:35:25 +05301276 select *, timestamp(posting_date, posting_time) as "timestamp"
1277 from `tabStock Ledger Entry`
1278 where item_code = %(item_code)s
1279 and warehouse = %(warehouse)s
1280 and is_cancelled = 0
1281 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301282 and (
1283 posting_date < %(posting_date)s or
1284 (
1285 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301286 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301287 )
1288 )
marination8418c4b2021-06-22 21:35:25 +05301289 order by timestamp(posting_date, posting_time) desc, creation desc
1290 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301291 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301292 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301293 ),
1294 args,
1295 as_dict=1,
1296 )
marination8418c4b2021-06-22 21:35:25 +05301297
1298 return sle[0] if sle else frappe._dict()
1299
Ankush Menat494bd9e2022-03-28 18:52:46 +05301300
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301301def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301302 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301303 get the last sle on or before the current time-bucket,
1304 to get actual qty before transaction, this function
1305 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301306
Ankush Menat494bd9e2022-03-28 18:52:46 +05301307 args = {
1308 "item_code": "ABC",
1309 "warehouse": "XYZ",
1310 "posting_date": "2012-12-12",
1311 "posting_time": "12:00",
1312 "sle": "name of reference Stock Ledger Entry"
1313 }
Anand Doshi1b531862013-01-10 19:29:51 +05301314 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301315 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301316 sle = get_stock_ledger_entries(
1317 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1318 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301319 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301320
Ankush Menat494bd9e2022-03-28 18:52:46 +05301321
1322def get_stock_ledger_entries(
1323 previous_sle,
1324 operator=None,
1325 order="desc",
1326 limit=None,
1327 for_update=False,
1328 debug=False,
1329 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301330 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301331):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301332 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301333 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1334 operator
1335 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301336 if previous_sle.get("warehouse"):
1337 conditions += " and warehouse = %(warehouse)s"
1338 elif previous_sle.get("warehouse_condition"):
1339 conditions += " and " + previous_sle.get("warehouse_condition")
1340
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301341 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301342 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1343 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301344 conditions += (
1345 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301346 (
1347 serial_no = {0}
1348 or serial_no like {1}
1349 or serial_no like {2}
1350 or serial_no like {3}
1351 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301352 """
1353 ).format(
1354 frappe.db.escape(serial_no),
1355 frappe.db.escape("{}\n%".format(serial_no)),
1356 frappe.db.escape("%\n{}".format(serial_no)),
1357 frappe.db.escape("%\n{}\n%".format(serial_no)),
1358 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301359
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301360 if not previous_sle.get("posting_date"):
1361 previous_sle["posting_date"] = "1900-01-01"
1362 if not previous_sle.get("posting_time"):
1363 previous_sle["posting_time"] = "00:00"
1364
1365 if operator in (">", "<=") and previous_sle.get("name"):
1366 conditions += " and name!=%(name)s"
1367
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301368 if extra_cond:
1369 conditions += f"{extra_cond}"
1370
Ankush Menat494bd9e2022-03-28 18:52:46 +05301371 return frappe.db.sql(
1372 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301373 select *, timestamp(posting_date, posting_time) as "timestamp"
1374 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301375 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301376 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301377 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301378 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301379 %(limit)s %(for_update)s"""
1380 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301381 "conditions": conditions,
1382 "limit": limit or "",
1383 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301384 "order": order,
1385 },
1386 previous_sle,
1387 as_dict=1,
1388 debug=debug,
1389 )
1390
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301391
Nabin Haita77b8c92020-12-21 14:45:50 +05301392def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301393 return frappe.db.get_value(
1394 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301395 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301396 [
1397 "item_code",
1398 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301399 "actual_qty",
1400 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301401 "posting_date",
1402 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301403 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301404 "timestamp(posting_date, posting_time) as timestamp",
1405 ],
1406 as_dict=1,
1407 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301408
Ankush Menatce0514c2022-02-15 11:41:41 +05301409
Ankush Menat494bd9e2022-03-28 18:52:46 +05301410def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301411 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301412):
1413
Ankush Menat102fff22022-02-19 15:51:04 +05301414 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301415 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301416
Ankush Menate1c16872022-04-21 20:01:48 +05301417 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301418 posting_date, posting_time
1419 )
Ankush Menat102fff22022-02-19 15:51:04 +05301420 if creation:
1421 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301422 CombineDatetime(sle.posting_date, sle.posting_time)
1423 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301424 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301425
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301426 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301427 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301428 )
1429
Ankush Menat102fff22022-02-19 15:51:04 +05301430 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301431 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301432 .inner_join(batch_ledger)
1433 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1434 .select(
1435 Sum(
1436 Case()
1437 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1438 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1439 ).as_("batch_value"),
1440 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1441 "batch_qty"
1442 ),
1443 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301444 .where(
1445 (sle.item_code == item_code)
1446 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301447 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301448 & (sle.is_cancelled == 0)
1449 )
1450 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301451 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301452
1453 if batch_details and batch_details[0].batch_qty:
1454 return batch_details[0].batch_value / batch_details[0].batch_qty
1455
1456
Ankush Menat494bd9e2022-03-28 18:52:46 +05301457def get_valuation_rate(
1458 item_code,
1459 warehouse,
1460 voucher_type,
1461 voucher_no,
1462 allow_zero_rate=False,
1463 currency=None,
1464 company=None,
1465 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301466 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301467 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301468):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301469
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301470 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1471
Ankush Menatf7ffe042021-11-01 13:21:14 +05301472 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301473 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301474
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301475 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1476 table = frappe.qb.DocType("Stock Ledger Entry")
1477 query = (
1478 frappe.qb.from_(table)
1479 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1480 .where(
1481 (table.item_code == item_code)
1482 & (table.warehouse == warehouse)
1483 & (table.batch_no == batch_no)
1484 & (table.is_cancelled == 0)
1485 & (table.voucher_no != voucher_no)
1486 & (table.voucher_type != voucher_type)
1487 )
1488 )
1489
1490 last_valuation_rate = query.run()
1491 if last_valuation_rate:
1492 return flt(last_valuation_rate[0][0])
1493
Ankush Menat342d09a2022-02-19 14:28:51 +05301494 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301495 if warehouse and serial_and_batch_bundle:
1496 batch_obj = BatchNoValuation(
1497 sle=frappe._dict(
1498 {
1499 "item_code": item_code,
1500 "warehouse": warehouse,
1501 "actual_qty": -1,
1502 "serial_and_batch_bundle": serial_and_batch_bundle,
1503 }
1504 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301505 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301506
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301507 return batch_obj.get_incoming_rate()
1508
Ankush Menatf7ffe042021-11-01 13:21:14 +05301509 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301510 if last_valuation_rate := frappe.db.sql(
1511 """select valuation_rate
1512 from `tabStock Ledger Entry` force index (item_warehouse)
1513 where
1514 item_code = %s
1515 AND warehouse = %s
1516 AND valuation_rate >= 0
1517 AND is_cancelled = 0
1518 AND NOT (voucher_no = %s AND voucher_type = %s)
1519 order by posting_date desc, posting_time desc, name desc limit 1""",
1520 (item_code, warehouse, voucher_no, voucher_type),
1521 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301522 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301523
1524 # If negative stock allowed, and item delivered without any incoming entry,
1525 # system does not found any SLE, then take valuation rate from Item
1526 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301527
1528 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301529 # try Item Standard rate
1530 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301531
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301532 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301533 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301534 valuation_rate = frappe.db.get_value(
1535 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1536 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301537
Ankush Menat494bd9e2022-03-28 18:52:46 +05301538 if (
1539 not allow_zero_rate
1540 and not valuation_rate
1541 and raise_error_if_no_rate
1542 and cint(erpnext.is_perpetual_inventory_enabled(company))
1543 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301544 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301545
Ankush Menat494bd9e2022-03-28 18:52:46 +05301546 message = _(
1547 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1548 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301549 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301550 solutions = (
1551 "<li>"
1552 + _(
1553 "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."
1554 ).format(voucher_type)
1555 + "</li>"
1556 )
1557 solutions += (
1558 "<li>"
1559 + _("If not, you can Cancel / Submit this entry")
1560 + " {0} ".format(frappe.bold("after"))
1561 + _("performing either one below:")
1562 + "</li>"
1563 )
Marica97715f22020-05-11 20:45:37 +05301564 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1565 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1566 msg = message + solutions + sub_solutions + "</li>"
1567
1568 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301569
1570 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301571
Ankush Menat494bd9e2022-03-28 18:52:46 +05301572
Ankush Menate7109c12021-08-26 16:40:45 +05301573def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301574 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301575 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301576 qty_shift = args.actual_qty
1577
Ankush Menat7c839c42022-05-06 12:09:08 +05301578 args["time_format"] = "%H:%i:%s"
1579
marination8418c4b2021-06-22 21:35:25 +05301580 # find difference/shift in qty caused by stock reconciliation
1581 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301582 qty_shift = get_stock_reco_qty_shift(args)
1583
1584 # find the next nearest stock reco so that we only recalculate SLEs till that point
1585 next_stock_reco_detail = get_next_stock_reco(args)
1586 if next_stock_reco_detail:
1587 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301588
marination40389772021-07-02 17:13:45 +05301589 # add condition to update SLEs before this date & time
1590 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301591
Ankush Menat494bd9e2022-03-28 18:52:46 +05301592 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301593 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301594 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301595 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301596 where
1597 item_code = %(item_code)s
1598 and warehouse = %(warehouse)s
1599 and voucher_no != %(voucher_no)s
1600 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301601 and (
1602 posting_date > %(posting_date)s or
1603 (
1604 posting_date = %(posting_date)s and
1605 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1606 )
1607 )
marination40389772021-07-02 17:13:45 +05301608 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301609 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301610 args,
1611 )
Nabin Hait186a0452021-02-18 14:14:21 +05301612
1613 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1614
Ankush Menat494bd9e2022-03-28 18:52:46 +05301615
marination40389772021-07-02 17:13:45 +05301616def get_stock_reco_qty_shift(args):
1617 stock_reco_qty_shift = 0
1618 if args.get("is_cancelled"):
1619 if args.get("previous_qty_after_transaction"):
1620 # get qty (balance) that was set at submission
1621 last_balance = args.get("previous_qty_after_transaction")
1622 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1623 else:
1624 stock_reco_qty_shift = flt(args.actual_qty)
1625 else:
1626 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301627 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301628 "qty_after_transaction"
1629 )
marination40389772021-07-02 17:13:45 +05301630
1631 if last_balance is not None:
1632 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1633 else:
1634 stock_reco_qty_shift = args.qty_after_transaction
1635
1636 return stock_reco_qty_shift
1637
Ankush Menat494bd9e2022-03-28 18:52:46 +05301638
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301639def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301640 """Returns next nearest stock reconciliaton's details."""
1641
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301642 sle = frappe.qb.DocType("Stock Ledger Entry")
1643
1644 query = (
1645 frappe.qb.from_(sle)
1646 .select(
1647 sle.name,
1648 sle.posting_date,
1649 sle.posting_time,
1650 sle.creation,
1651 sle.voucher_no,
1652 sle.item_code,
1653 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301654 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301655 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301656 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301657 )
1658 .where(
1659 (sle.item_code == kwargs.get("item_code"))
1660 & (sle.warehouse == kwargs.get("warehouse"))
1661 & (sle.voucher_type == "Stock Reconciliation")
1662 & (sle.voucher_no != kwargs.get("voucher_no"))
1663 & (sle.is_cancelled == 0)
1664 & (
1665 (
1666 CombineDatetime(sle.posting_date, sle.posting_time)
1667 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301668 )
1669 | (
1670 (
1671 CombineDatetime(sle.posting_date, sle.posting_time)
1672 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301673 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301674 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301675 )
1676 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301677 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301678 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1679 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301680 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301681 )
1682
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301683 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301684 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301685
1686 return query.run(as_dict=True)
1687
marination40389772021-07-02 17:13:45 +05301688
1689def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301690 return f"""
1691 and
1692 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1693 or (
1694 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1695 and creation < '{detail.creation}'
1696 )
1697 )"""
1698
Ankush Menat494bd9e2022-03-28 18:52:46 +05301699
Ankush Menate7109c12021-08-26 16:40:45 +05301700def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301701 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301702 return
1703 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1704 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301705
Ankush Menat5eba5752021-12-07 23:03:52 +05301706 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301707
1708 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301709 message = _(
1710 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1711 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301712 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301713 frappe.get_desk_link("Item", args.item_code),
1714 frappe.get_desk_link("Warehouse", args.warehouse),
1715 neg_sle[0]["posting_date"],
1716 neg_sle[0]["posting_time"],
1717 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1718 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301719
Ankush Menat494bd9e2022-03-28 18:52:46 +05301720 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301721
1722 if not args.batch_no:
1723 return
1724
1725 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301726 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301727 message = _(
1728 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1729 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301730 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301731 frappe.get_desk_link("Batch", args.batch_no),
1732 frappe.get_desk_link("Warehouse", args.warehouse),
1733 neg_batch_sle[0]["posting_date"],
1734 neg_batch_sle[0]["posting_time"],
1735 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1736 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301737 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301738
Nabin Haita77b8c92020-12-21 14:45:50 +05301739
Maricad6078aa2022-06-17 15:13:13 +05301740def is_negative_with_precision(neg_sle, is_batch=False):
1741 """
1742 Returns whether system precision rounded qty is insufficient.
1743 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1744 """
1745
1746 if not neg_sle:
1747 return False
1748
1749 field = "cumulative_total" if is_batch else "qty_after_transaction"
1750 precision = cint(frappe.db.get_default("float_precision")) or 2
1751 qty_deficit = flt(neg_sle[0][field], precision)
1752
1753 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1754
1755
Nabin Haita77b8c92020-12-21 14:45:50 +05301756def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301757 return frappe.db.sql(
1758 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301759 select
1760 qty_after_transaction, posting_date, posting_time,
1761 voucher_type, voucher_no
1762 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301763 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301764 item_code = %(item_code)s
1765 and warehouse = %(warehouse)s
1766 and voucher_no != %(voucher_no)s
1767 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1768 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301769 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301770 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301771 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301772 """,
1773 args,
1774 as_dict=1,
1775 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301776
Ankush Menat5eba5752021-12-07 23:03:52 +05301777
1778def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301779 return frappe.db.sql(
1780 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301781 with batch_ledger as (
1782 select
1783 posting_date, posting_time, voucher_type, voucher_no,
1784 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1785 from `tabStock Ledger Entry`
1786 where
1787 item_code = %(item_code)s
1788 and warehouse = %(warehouse)s
1789 and batch_no=%(batch_no)s
1790 and is_cancelled = 0
1791 order by posting_date, posting_time, creation
1792 )
1793 select * from batch_ledger
1794 where
1795 cumulative_total < 0.0
1796 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1797 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301798 """,
1799 args,
1800 as_dict=1,
1801 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301802
1803
1804def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1805 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1806 return True
1807 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1808 return True
1809 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301810
1811
1812def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1813 """
1814 For inter company transfer, incoming rate is the average of the outgoing rate
1815 """
1816 rate = 0.0
1817
1818 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1819
1820 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1821
1822 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1823
1824 if reference_name:
1825 rate = frappe.get_cached_value(
1826 doctype,
1827 reference_name,
1828 "incoming_rate",
1829 )
1830
1831 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301832
1833
1834def is_internal_transfer(sle):
1835 data = frappe.get_cached_value(
1836 sle.voucher_type,
1837 sle.voucher_no,
1838 ["is_internal_supplier", "represents_company", "company"],
1839 as_dict=True,
1840 )
1841
1842 if data.is_internal_supplier and data.represents_company == data.company:
1843 return True