blob: b950f18810788fc07305568f3909200a9c1a97db [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()
Nabin Haita77b8c92020-12-21 14:45:50 +0530206 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530207
Ankush Menat494bd9e2022-03-28 18:52:46 +0530208
209def repost_future_sle(
210 args=None,
211 voucher_type=None,
212 voucher_no=None,
213 allow_negative_stock=None,
214 via_landed_cost_voucher=False,
215 doc=None,
216):
Nabin Haite1fa7232022-07-20 15:19:09 +0530217 if not args:
218 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530219
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530220 reposting_data = {}
221 if doc and doc.reposting_data_file:
222 reposting_data = get_reposting_data(doc.reposting_data_file)
223
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530224 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530225 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530226 )
227 if items_to_be_repost:
228 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530229
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530230 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
231 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530232
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530233 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530234 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530235 validate_item_warehouse(args[i])
236
Ankush Menat494bd9e2022-03-28 18:52:46 +0530237 obj = update_entries_after(
238 {
239 "item_code": args[i].get("item_code"),
240 "warehouse": args[i].get("warehouse"),
241 "posting_date": args[i].get("posting_date"),
242 "posting_time": args[i].get("posting_time"),
243 "creation": args[i].get("creation"),
244 "distinct_item_warehouses": distinct_item_warehouses,
245 },
246 allow_negative_stock=allow_negative_stock,
247 via_landed_cost_voucher=via_landed_cost_voucher,
248 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530249 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530250
Ankush Menat494bd9e2022-03-28 18:52:46 +0530251 distinct_item_warehouses[
252 (args[i].get("item_code"), args[i].get("warehouse"))
253 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530254
Nabin Hait97bce3a2021-07-12 13:24:43 +0530255 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530256 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530257 if ("args_idx" not in data and not data.reposting_status) or (
258 data.sle_changed and data.reposting_status
259 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530260 data.args_idx = len(args)
261 args.append(data.sle)
262 elif data.sle_changed and not data.reposting_status:
263 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530264
Nabin Hait97bce3a2021-07-12 13:24:43 +0530265 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530266 i += 1
267
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530268 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530269 update_args_in_repost_item_valuation(
270 doc, i, args, distinct_item_warehouses, affected_transactions
271 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530272
Ankush Menat494bd9e2022-03-28 18:52:46 +0530273
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530274def get_reposting_data(file_path) -> dict:
275 file_name = frappe.db.get_value(
276 "File",
277 {
278 "file_url": file_path,
279 "attached_to_field": "reposting_data_file",
280 },
281 "name",
282 )
283
284 if not file_name:
285 return frappe._dict()
286
287 attached_file = frappe.get_doc("File", file_name)
288
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530289 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530290 if data := json.loads(data.decode("utf-8")):
291 data = data
292
293 return parse_json(data)
294
295
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530297 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530298 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530299 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530300 frappe.throw(_(validation_msg))
301
Ankush Menat494bd9e2022-03-28 18:52:46 +0530302
Ankush Menatecdb4932022-04-17 19:06:13 +0530303def update_args_in_repost_item_valuation(
304 doc, index, args, distinct_item_warehouses, affected_transactions
305):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530306 if not doc.items_to_be_repost:
307 file_name = ""
308 if doc.reposting_data_file:
309 file_name = get_reposting_file_name(doc.doctype, doc.name)
310 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
311
312 doc.reposting_data_file = create_json_gz_file(
313 {
314 "items_to_be_repost": args,
315 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
316 "affected_transactions": affected_transactions,
317 },
318 doc,
319 file_name,
320 )
321
322 doc.db_set(
323 {
324 "current_index": index,
325 "total_reposting_count": len(args),
326 "reposting_data_file": doc.reposting_data_file,
327 }
328 )
329
330 else:
331 doc.db_set(
332 {
333 "items_to_be_repost": json.dumps(args, default=str),
334 "distinct_item_and_warehouse": json.dumps(
335 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
336 ),
337 "current_index": index,
338 "affected_transactions": frappe.as_json(affected_transactions),
339 }
340 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530341
Ankush Menatecdb4932022-04-17 19:06:13 +0530342 if not frappe.flags.in_test:
343 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530344
Ankush Menat494bd9e2022-03-28 18:52:46 +0530345 frappe.publish_realtime(
346 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530347 {
348 "name": doc.name,
349 "items_to_be_repost": json.dumps(args, default=str),
350 "current_index": index,
351 "total_reposting_count": len(args),
352 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530353 doctype=doc.doctype,
354 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530355 )
356
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530357
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530358def get_reposting_file_name(dt, dn):
359 return frappe.db.get_value(
360 "File",
361 {
362 "attached_to_doctype": dt,
363 "attached_to_name": dn,
364 "attached_to_field": "reposting_data_file",
365 },
366 "name",
367 )
368
369
370def create_json_gz_file(data, doc, file_name=None) -> str:
371 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530372 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530373
374 if not file_name:
375 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
376 _file = frappe.get_doc(
377 {
378 "doctype": "File",
379 "file_name": json_filename,
380 "attached_to_doctype": doc.doctype,
381 "attached_to_name": doc.name,
382 "attached_to_field": "reposting_data_file",
383 "content": compressed_content,
384 "is_private": 1,
385 }
386 )
387 _file.save(ignore_permissions=True)
388
389 return _file.file_url
390 else:
391 file_doc = frappe.get_doc("File", file_name)
392 path = file_doc.get_full_path()
393
394 with open(path, "wb") as f:
395 f.write(compressed_content)
396
397 return doc.reposting_data_file
398
399
400def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
401 if not reposting_data and doc and doc.reposting_data_file:
402 reposting_data = get_reposting_data(doc.reposting_data_file)
403
404 if reposting_data and reposting_data.items_to_be_repost:
405 return reposting_data.items_to_be_repost
406
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530407 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530408
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530409 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530410 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530411
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530412 if not items_to_be_repost and voucher_type and voucher_no:
413 items_to_be_repost = frappe.db.get_all(
414 "Stock Ledger Entry",
415 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
416 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
417 order_by="creation asc",
418 group_by="item_code, warehouse",
419 )
420
Nabin Haite1fa7232022-07-20 15:19:09 +0530421 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530422
Ankush Menat494bd9e2022-03-28 18:52:46 +0530423
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530424def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
425 if not reposting_data and doc and doc.reposting_data_file:
426 reposting_data = get_reposting_data(doc.reposting_data_file)
427
428 if reposting_data and reposting_data.distinct_item_and_warehouse:
429 return reposting_data.distinct_item_and_warehouse
430
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530431 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530432
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530433 if doc and doc.distinct_item_and_warehouse:
434 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530435 distinct_item_warehouses = {
436 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
437 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530438 else:
439 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530440 distinct_item_warehouses.setdefault(
441 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
442 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530443
444 return distinct_item_warehouses
445
Ankush Menat494bd9e2022-03-28 18:52:46 +0530446
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530447def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
448 if not reposting_data and doc and doc.reposting_data_file:
449 reposting_data = get_reposting_data(doc.reposting_data_file)
450
451 if reposting_data and reposting_data.affected_transactions:
452 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
453
Ankush Menatecdb4932022-04-17 19:06:13 +0530454 if not doc.affected_transactions:
455 return set()
456
457 transactions = frappe.parse_json(doc.affected_transactions)
458 return {tuple(transaction) for transaction in transactions}
459
460
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530461def get_current_index(doc=None):
462 if doc and doc.current_index:
463 return doc.current_index
464
Ankush Menat494bd9e2022-03-28 18:52:46 +0530465
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530466class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530467 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530468 update valution rate and qty after transaction
469 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530470
Ankush Menat494bd9e2022-03-28 18:52:46 +0530471 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530472
Ankush Menat494bd9e2022-03-28 18:52:46 +0530473 args = {
474 "item_code": "ABC",
475 "warehouse": "XYZ",
476 "posting_date": "2012-12-12",
477 "posting_time": "12:00"
478 }
Nabin Hait902e8602013-01-08 18:29:24 +0530479 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530480
481 def __init__(
482 self,
483 args,
484 allow_zero_rate=False,
485 allow_negative_stock=None,
486 via_landed_cost_voucher=False,
487 verbose=1,
488 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530489 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530490 self.verbose = verbose
491 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530492 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530493 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530494 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
495 item_code=self.item_code
496 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530497
Nabin Haita77b8c92020-12-21 14:45:50 +0530498 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530499 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530500 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530501
Nabin Haita77b8c92020-12-21 14:45:50 +0530502 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530503 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530504 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530505
506 self.new_items_found = False
507 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530508 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-rf0acb202023-04-12 14:13:54 +0530509 self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
Nabin Haita77b8c92020-12-21 14:45:50 +0530510
511 self.data = frappe._dict()
512 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530513 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530514
Maricad6078aa2022-06-17 15:13:13 +0530515 def set_precision(self):
516 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
517 self.currency_precision = get_field_precision(
518 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530519 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530520
521 def initialize_previous_data(self, args):
522 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530523 Get previous sl entries for current item for each related warehouse
524 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530525
Ankush Menat494bd9e2022-03-28 18:52:46 +0530526 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530527
Ankush Menat494bd9e2022-03-28 18:52:46 +0530528 self.data = {
529 warehouse1: {
530 'previus_sle': {},
531 'qty_after_transaction': 10,
532 'valuation_rate': 100,
533 'stock_value': 1000,
534 'prev_stock_value': 1000,
535 'stock_queue': '[[10, 100]]',
536 'stock_value_difference': 1000
537 }
538 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530539
540 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530541 self.data.setdefault(args.warehouse, frappe._dict())
542 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530543 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530544 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530545
Ankush Menatc1d986a2021-08-31 19:43:42 +0530546 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
547 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
548
Ankush Menat494bd9e2022-03-28 18:52:46 +0530549 warehouse_dict.update(
550 {
551 "prev_stock_value": previous_sle.stock_value or 0.0,
552 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
553 "stock_value_difference": 0.0,
554 }
555 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530556
Nabin Haita77b8c92020-12-21 14:45:50 +0530557 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530558 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530559
Nabin Haita77b8c92020-12-21 14:45:50 +0530560 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530561 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530562 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530563 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530564 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530565 entries_to_fix = self.get_future_entries_to_fix()
566
567 i = 0
568 while i < len(entries_to_fix):
569 sle = entries_to_fix[i]
570 i += 1
571
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530572 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530573 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530574
Nabin Haita77b8c92020-12-21 14:45:50 +0530575 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530576 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530577
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530578 if self.exceptions:
579 self.raise_exceptions()
580
Nabin Hait186a0452021-02-18 14:14:21 +0530581 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530582 sl_entries = self.get_sle_against_current_voucher()
583 for sle in sl_entries:
584 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530585
Nabin Haita77b8c92020-12-21 14:45:50 +0530586 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530587 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530588
Ankush Menat494bd9e2022-03-28 18:52:46 +0530589 return frappe.db.sql(
590 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530591 select
592 *, timestamp(posting_date, posting_time) as "timestamp"
593 from
594 `tabStock Ledger Entry`
595 where
596 item_code = %(item_code)s
597 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530598 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530599 and (
600 posting_date = %(posting_date)s and
601 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
602 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530603 order by
604 creation ASC
605 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530606 """,
607 self.args,
608 as_dict=1,
609 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530610
Nabin Haita77b8c92020-12-21 14:45:50 +0530611 def get_future_entries_to_fix(self):
612 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530613 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
614 {"item_code": self.item_code, "warehouse": self.args.warehouse}
615 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530616
Nabin Haita77b8c92020-12-21 14:45:50 +0530617 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530618
Nabin Haita77b8c92020-12-21 14:45:50 +0530619 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530620 dependant_sle = get_sle_by_voucher_detail_no(
621 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
622 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530623
Nabin Haita77b8c92020-12-21 14:45:50 +0530624 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530625 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530626 elif (
627 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
628 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530629 return entries_to_fix
630 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530631 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530632 return entries_to_fix
633 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
634 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530635 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530636 self.initialize_previous_data(dependant_sle)
637 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530638 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530639
640 def update_distinct_item_warehouses(self, dependant_sle):
641 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530642 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530643
Nabin Hait97bce3a2021-07-12 13:24:43 +0530644 if key not in self.distinct_item_warehouses:
645 self.distinct_item_warehouses[key] = val
646 self.new_items_found = True
647 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530648 existing_sle_posting_date = (
649 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
650 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530651
652 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
653
Nabin Hait97bce3a2021-07-12 13:24:43 +0530654 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
655 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530656 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
657 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530658 self.distinct_item_warehouses[key] = val
659 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530660 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
661 # Future dependent voucher needs to be repost to get the correct stock value
662 # If dependent voucher has not reposted, then add it to the list
663 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530664 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530665 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
666 self.distinct_item_warehouses[key] = val
667
668 def get_dependent_voucher_detail_nos(self, key):
669 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
670 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
671
672 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530673
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530674 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530675 # previous sle data for this warehouse
676 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530677 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530678
Anand Doshi0dc79f42015-04-06 12:59:34 +0530679 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 +0530680 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530681 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530682 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530683 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530684 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530685
Nabin Haita77b8c92020-12-21 14:45:50 +0530686 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530687 if not self.args.get("sle_id"):
688 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530689
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530690 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530691 sle.voucher_type == "Stock Reconciliation"
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530692 and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle))
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530693 and sle.voucher_detail_no
694 and sle.actual_qty < 0
695 ):
696 self.reset_actual_qty_for_stock_reco(sle)
697
698 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530699 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
700 and sle.voucher_detail_no
701 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530702 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530703 ):
704 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
705
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530706 dimensions = get_inventory_dimensions()
707 has_dimensions = False
708 if dimensions:
709 for dimension in dimensions:
710 if sle.get(dimension.get("fieldname")):
711 has_dimensions = True
712
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530713 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530714 self.calculate_valuation_for_serial_batch_bundle(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530715 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530716 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530717 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530718 self.wh_data.valuation_rate = sle.valuation_rate
719 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530720 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
721 self.wh_data.valuation_rate
722 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530723 if self.valuation_method != "Moving Average":
724 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530725 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530726 if self.valuation_method == "Moving Average":
727 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530728 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530729 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
730 self.wh_data.valuation_rate
731 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530732 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530733 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530734
Rushabh Mehta54047782013-12-26 11:07:46 +0530735 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530736 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530737 if not self.wh_data.qty_after_transaction:
738 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530739
Nabin Haita77b8c92020-12-21 14:45:50 +0530740 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
741 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530742
Nabin Hait902e8602013-01-08 18:29:24 +0530743 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530744 sle.qty_after_transaction = self.wh_data.qty_after_transaction
745 sle.valuation_rate = self.wh_data.valuation_rate
746 sle.stock_value = self.wh_data.stock_value
747 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530748 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530749 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530750
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530751 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530752
Ankush Menat701878f2022-03-01 18:08:29 +0530753 if not self.args.get("sle_id"):
754 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530755
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530756 def reset_actual_qty_for_stock_reco(self, sle):
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530757 if sle.serial_and_batch_bundle:
758 current_qty = frappe.get_cached_value(
759 "Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty"
760 )
761
762 if current_qty is not None:
763 current_qty = abs(current_qty)
764 else:
765 current_qty = frappe.get_cached_value(
766 "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
767 )
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530768
769 if current_qty:
770 sle.actual_qty = current_qty * -1
771 elif current_qty == 0:
772 sle.is_cancelled = 1
773
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530774 def calculate_valuation_for_serial_batch_bundle(self, sle):
775 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
776
777 doc.set_incoming_rate(save=True)
778 doc.calculate_qty_and_amount(save=True)
779
780 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
781
782 self.wh_data.qty_after_transaction += doc.total_qty
783 if self.wh_data.qty_after_transaction:
784 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
785
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530786 def validate_negative_stock(self, sle):
787 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530788 validate negative stock for entries current datetime onwards
789 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530790 """
s-aga-rf0acb202023-04-12 14:13:54 +0530791 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530792 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530793
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530794 if diff < 0 and abs(diff) > 0.0001:
795 # negative stock!
796 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530797 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530798 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530799 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530800 return True
801
Nabin Haita77b8c92020-12-21 14:45:50 +0530802 def get_dynamic_incoming_outgoing_rate(self, sle):
803 # Get updated incoming/outgoing rate from transaction
804 if sle.recalculate_rate:
805 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
806
807 if flt(sle.actual_qty) >= 0:
808 sle.incoming_rate = rate
809 else:
810 sle.outgoing_rate = rate
811
812 def get_incoming_outgoing_rate_from_transaction(self, sle):
813 rate = 0
814 # Material Transfer, Repack, Manufacturing
815 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530816 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530817 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
818 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530819 elif sle.voucher_type in (
820 "Purchase Receipt",
821 "Purchase Invoice",
822 "Delivery Note",
823 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530824 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530825 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530826 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530827 from erpnext.controllers.sales_and_purchase_return import (
828 get_rate_for_return, # don't move this import to top
829 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530830
831 rate = get_rate_for_return(
832 sle.voucher_type,
833 sle.voucher_no,
834 sle.item_code,
835 voucher_detail_no=sle.voucher_detail_no,
836 sle=sle,
837 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530838
839 elif (
840 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530841 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530842 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530843 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530844 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530845 else:
846 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530847 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530848 elif sle.voucher_type == "Subcontracting Receipt":
849 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530850 else:
851 rate_field = "incoming_rate"
852
853 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530854 item_code, incoming_rate = frappe.db.get_value(
855 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
856 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530857
858 if item_code == sle.item_code:
859 rate = incoming_rate
860 else:
861 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
862 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530863 elif sle == "Subcontracting Receipt":
864 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530865 else:
866 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530867
Ankush Menat494bd9e2022-03-28 18:52:46 +0530868 rate = frappe.db.get_value(
869 ref_doctype,
870 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
871 rate_field,
872 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530873
874 return rate
875
876 def update_outgoing_rate_on_transaction(self, sle):
877 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530878 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
879 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530880 """
881 if sle.actual_qty and sle.voucher_detail_no:
882 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
883
884 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
885 self.update_rate_on_stock_entry(sle, outgoing_rate)
886 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
887 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
888 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
889 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530890 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
891 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530892 elif sle.voucher_type == "Stock Reconciliation":
893 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530894
895 def update_rate_on_stock_entry(self, sle, outgoing_rate):
896 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
897
Ankush Menat701878f2022-03-01 18:08:29 +0530898 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
899 if not sle.dependant_sle_voucher_detail_no:
900 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530901
902 def recalculate_amounts_in_stock_entry(self, voucher_no):
903 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530904 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
905 stock_entry.db_update()
906 for d in stock_entry.items:
907 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530908
Nabin Haita77b8c92020-12-21 14:45:50 +0530909 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
910 # Update item's incoming rate on transaction
911 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
912 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530913 frappe.db.set_value(
914 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
915 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530916 else:
917 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530918 frappe.db.set_value(
919 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530920 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530921 "incoming_rate",
922 outgoing_rate,
923 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530924
925 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
926 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530927 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
928 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
929 ):
930 frappe.db.set_value(
931 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
932 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530933 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530934 frappe.db.set_value(
935 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
936 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530937
938 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530939 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530940 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530941 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530942 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530943 d.db_update()
944
Sagar Sharma323bdf82022-05-17 15:14:07 +0530945 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530946 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
947 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530948 else:
949 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530950 "Subcontracting Receipt Supplied Item",
951 sle.voucher_detail_no,
952 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530953 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530954
s-aga-ra6cb6c62023-05-03 09:51:58 +0530955 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530956 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530957 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530958 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530959 d.db_update()
960
s-aga-r88a3f652023-05-30 16:54:28 +0530961 def update_rate_on_stock_reconciliation(self, sle):
962 if not sle.serial_no and not sle.batch_no:
963 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
964
965 for item in sr.items:
966 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +0530967 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +0530968 continue
969
970 previous_sle = get_previous_sle(
971 {
972 "item_code": item.item_code,
973 "warehouse": item.warehouse,
974 "posting_date": sr.posting_date,
975 "posting_time": sr.posting_time,
976 "sle": sle.name,
977 }
978 )
979
980 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
981 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
982 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
983
984 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +0530985 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +0530986 item.amount_difference = item.amount - item.current_amount
987 else:
988 sr.difference_amount = sum([item.amount_difference for item in sr.items])
989 sr.db_update()
990
991 for item in sr.items:
992 item.db_update()
993
Nabin Hait328c4f92020-01-02 19:00:32 +0530994 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
995 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530996 all_serial_nos = frappe.get_all(
997 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
998 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530999
Ankush Menat494bd9e2022-03-28 18:52:46 +05301000 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 +05301001
1002 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301003 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301004 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301005 incoming_rate = frappe.db.sql(
1006 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301007 select incoming_rate
1008 from `tabStock Ledger Entry`
1009 where
1010 company = %s
1011 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301012 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301013 and (serial_no = %s
1014 or serial_no like %s
1015 or serial_no like %s
1016 or serial_no like %s
1017 )
1018 order by posting_date desc
1019 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301020 """,
1021 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1022 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301023
1024 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1025
1026 return incoming_values
1027
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301028 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301029 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301030 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301031 if new_stock_qty >= 0:
1032 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301033 if flt(self.wh_data.qty_after_transaction) <= 0:
1034 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301035 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301036 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1037 actual_qty * sle.incoming_rate
1038 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301039
Nabin Haita77b8c92020-12-21 14:45:50 +05301040 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301041
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301042 elif sle.outgoing_rate:
1043 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301044 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1045 actual_qty * sle.outgoing_rate
1046 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301047
Nabin Haita77b8c92020-12-21 14:45:50 +05301048 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301049 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301050 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301051 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301052 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1053 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301054
Nabin Haita77b8c92020-12-21 14:45:50 +05301055 if not self.wh_data.valuation_rate and actual_qty > 0:
1056 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301057
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301058 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001059 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301060 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301061 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1062 sle.voucher_type, sle.voucher_detail_no
1063 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001064 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301065 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301066
Ankush Menatf089d392022-02-02 12:51:21 +05301067 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301068 incoming_rate = flt(sle.incoming_rate)
1069 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301070 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301071
Ankush Menat494bd9e2022-03-28 18:52:46 +05301072 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1073 self.wh_data.qty_after_transaction + actual_qty
1074 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301075
Ankush Menat97e18a12022-01-15 17:42:25 +05301076 if self.valuation_method == "LIFO":
1077 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1078 else:
1079 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1080
Ankush Menatb534fee2022-02-19 20:58:36 +05301081 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1082
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301083 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301084 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301085 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301086
Ankush Menat4b29fb62021-12-18 18:40:22 +05301087 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301088 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1089 sle.voucher_type, sle.voucher_detail_no
1090 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301091 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301092 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301093 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301094 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301095
Ankush Menat494bd9e2022-03-28 18:52:46 +05301096 stock_queue.remove_stock(
1097 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1098 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301099
Ankush Menatb534fee2022-02-19 20:58:36 +05301100 _qty, stock_value = stock_queue.get_total_stock_and_value()
1101
1102 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301103
Ankush Menat97e18a12022-01-15 17:42:25 +05301104 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301105 self.wh_data.stock_value = round_off_if_near_zero(
1106 self.wh_data.stock_value + stock_value_difference
1107 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301108
Nabin Haita77b8c92020-12-21 14:45:50 +05301109 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301110 self.wh_data.stock_queue.append(
1111 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1112 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301113
Ankush Menatb534fee2022-02-19 20:58:36 +05301114 if self.wh_data.qty_after_transaction:
1115 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1116
Ankush Menatce0514c2022-02-15 11:41:41 +05301117 def update_batched_values(self, sle):
1118 incoming_rate = flt(sle.incoming_rate)
1119 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301120
Ankush Menat494bd9e2022-03-28 18:52:46 +05301121 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1122 self.wh_data.qty_after_transaction + actual_qty
1123 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301124
1125 if actual_qty > 0:
1126 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301127 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301128 outgoing_rate = get_batch_incoming_rate(
1129 item_code=sle.item_code,
1130 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301131 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301132 posting_date=sle.posting_date,
1133 posting_time=sle.posting_time,
1134 creation=sle.creation,
1135 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301136 if outgoing_rate is None:
1137 # This can *only* happen if qty available for the batch is zero.
1138 # in such case fall back various other rates.
1139 # future entries will correct the overall accounting as each
1140 # batch individually uses moving average rates.
1141 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301142 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301143
Ankush Menat494bd9e2022-03-28 18:52:46 +05301144 self.wh_data.stock_value = round_off_if_near_zero(
1145 self.wh_data.stock_value + stock_value_difference
1146 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301147 if self.wh_data.qty_after_transaction:
1148 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301149
Javier Wong9b11d9b2017-04-14 18:24:04 +08001150 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301151 ref_item_dt = ""
1152
1153 if voucher_type == "Stock Entry":
1154 ref_item_dt = voucher_type + " Detail"
1155 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1156 ref_item_dt = voucher_type + " Item"
1157
1158 if ref_item_dt:
1159 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1160 else:
1161 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301162
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301163 def get_fallback_rate(self, sle) -> float:
1164 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301165 This should only get used for negative stock."""
1166 return get_valuation_rate(
1167 sle.item_code,
1168 sle.warehouse,
1169 sle.voucher_type,
1170 sle.voucher_no,
1171 self.allow_zero_rate,
1172 currency=erpnext.get_company_currency(sle.company),
1173 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301174 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301175
Nabin Haita77b8c92020-12-21 14:45:50 +05301176 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301177 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301178 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1179 sle = sle[0] if sle else frappe._dict()
1180 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301181
Nabin Haita77b8c92020-12-21 14:45:50 +05301182 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301183 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301184 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301185
1186 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301187 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301188 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301189 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301190
Ankush Menat494bd9e2022-03-28 18:52:46 +05301191 if (
1192 exceptions[0]["voucher_type"],
1193 exceptions[0]["voucher_no"],
1194 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301195
Nabin Haita77b8c92020-12-21 14:45:50 +05301196 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301197 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301198 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1199 frappe.get_desk_link("Warehouse", warehouse),
1200 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301201 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301202 msg = _(
1203 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1204 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301205 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301206 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1207 frappe.get_desk_link("Warehouse", warehouse),
1208 exceptions[0]["posting_date"],
1209 exceptions[0]["posting_time"],
1210 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1211 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301212
Nabin Haita77b8c92020-12-21 14:45:50 +05301213 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301214 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301215 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301216
1217 if allowed_qty > 0:
1218 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1219 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1220 )
1221 else:
1222 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1223 msg,
1224 )
s-aga-rf0acb202023-04-12 14:13:54 +05301225
Nabin Haita77b8c92020-12-21 14:45:50 +05301226 msg_list.append(msg)
1227
1228 if msg_list:
1229 message = "\n\n".join(msg_list)
1230 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301231 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301232 else:
1233 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301234
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301235 def update_bin_data(self, sle):
1236 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301237 values_to_update = {
1238 "actual_qty": sle.qty_after_transaction,
1239 "stock_value": sle.stock_value,
1240 }
1241
1242 if sle.valuation_rate is not None:
1243 values_to_update["valuation_rate"] = sle.valuation_rate
1244
1245 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301246
Nabin Haita77b8c92020-12-21 14:45:50 +05301247 def update_bin(self):
1248 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301249 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301250 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301251
Ankush Menat494bd9e2022-03-28 18:52:46 +05301252 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301253 if data.valuation_rate is not None:
1254 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301255 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301256
marination8418c4b2021-06-22 21:35:25 +05301257
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301258def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301259 """get stock ledger entries filtered by specific posting datetime conditions"""
1260
Ankush Menat494bd9e2022-03-28 18:52:46 +05301261 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301262 if not args.get("posting_date"):
1263 args["posting_date"] = "1900-01-01"
1264 if not args.get("posting_time"):
1265 args["posting_time"] = "00:00"
1266
1267 voucher_condition = ""
1268 if exclude_current_voucher:
1269 voucher_no = args.get("voucher_no")
1270 voucher_condition = f"and voucher_no != '{voucher_no}'"
1271
Ankush Menat494bd9e2022-03-28 18:52:46 +05301272 sle = frappe.db.sql(
1273 """
marination8418c4b2021-06-22 21:35:25 +05301274 select *, timestamp(posting_date, posting_time) as "timestamp"
1275 from `tabStock Ledger Entry`
1276 where item_code = %(item_code)s
1277 and warehouse = %(warehouse)s
1278 and is_cancelled = 0
1279 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301280 and (
1281 posting_date < %(posting_date)s or
1282 (
1283 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301284 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301285 )
1286 )
marination8418c4b2021-06-22 21:35:25 +05301287 order by timestamp(posting_date, posting_time) desc, creation desc
1288 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301289 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301290 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301291 ),
1292 args,
1293 as_dict=1,
1294 )
marination8418c4b2021-06-22 21:35:25 +05301295
1296 return sle[0] if sle else frappe._dict()
1297
Ankush Menat494bd9e2022-03-28 18:52:46 +05301298
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301299def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301300 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301301 get the last sle on or before the current time-bucket,
1302 to get actual qty before transaction, this function
1303 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301304
Ankush Menat494bd9e2022-03-28 18:52:46 +05301305 args = {
1306 "item_code": "ABC",
1307 "warehouse": "XYZ",
1308 "posting_date": "2012-12-12",
1309 "posting_time": "12:00",
1310 "sle": "name of reference Stock Ledger Entry"
1311 }
Anand Doshi1b531862013-01-10 19:29:51 +05301312 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301313 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301314 sle = get_stock_ledger_entries(
1315 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1316 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301317 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301318
Ankush Menat494bd9e2022-03-28 18:52:46 +05301319
1320def get_stock_ledger_entries(
1321 previous_sle,
1322 operator=None,
1323 order="desc",
1324 limit=None,
1325 for_update=False,
1326 debug=False,
1327 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301328 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301329):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301330 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301331 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1332 operator
1333 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301334 if previous_sle.get("warehouse"):
1335 conditions += " and warehouse = %(warehouse)s"
1336 elif previous_sle.get("warehouse_condition"):
1337 conditions += " and " + previous_sle.get("warehouse_condition")
1338
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301339 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301340 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1341 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301342 conditions += (
1343 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301344 (
1345 serial_no = {0}
1346 or serial_no like {1}
1347 or serial_no like {2}
1348 or serial_no like {3}
1349 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301350 """
1351 ).format(
1352 frappe.db.escape(serial_no),
1353 frappe.db.escape("{}\n%".format(serial_no)),
1354 frappe.db.escape("%\n{}".format(serial_no)),
1355 frappe.db.escape("%\n{}\n%".format(serial_no)),
1356 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301357
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301358 if not previous_sle.get("posting_date"):
1359 previous_sle["posting_date"] = "1900-01-01"
1360 if not previous_sle.get("posting_time"):
1361 previous_sle["posting_time"] = "00:00"
1362
1363 if operator in (">", "<=") and previous_sle.get("name"):
1364 conditions += " and name!=%(name)s"
1365
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301366 if extra_cond:
1367 conditions += f"{extra_cond}"
1368
Ankush Menat494bd9e2022-03-28 18:52:46 +05301369 return frappe.db.sql(
1370 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301371 select *, timestamp(posting_date, posting_time) as "timestamp"
1372 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301373 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301374 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301375 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301376 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301377 %(limit)s %(for_update)s"""
1378 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301379 "conditions": conditions,
1380 "limit": limit or "",
1381 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301382 "order": order,
1383 },
1384 previous_sle,
1385 as_dict=1,
1386 debug=debug,
1387 )
1388
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301389
Nabin Haita77b8c92020-12-21 14:45:50 +05301390def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301391 return frappe.db.get_value(
1392 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301393 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301394 [
1395 "item_code",
1396 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301397 "actual_qty",
1398 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301399 "posting_date",
1400 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301401 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301402 "timestamp(posting_date, posting_time) as timestamp",
1403 ],
1404 as_dict=1,
1405 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301406
Ankush Menatce0514c2022-02-15 11:41:41 +05301407
Ankush Menat494bd9e2022-03-28 18:52:46 +05301408def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301409 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301410):
1411
Ankush Menat102fff22022-02-19 15:51:04 +05301412 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301413 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301414
Ankush Menate1c16872022-04-21 20:01:48 +05301415 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301416 posting_date, posting_time
1417 )
Ankush Menat102fff22022-02-19 15:51:04 +05301418 if creation:
1419 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301420 CombineDatetime(sle.posting_date, sle.posting_time)
1421 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301422 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301423
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301424 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301425 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301426 )
1427
Ankush Menat102fff22022-02-19 15:51:04 +05301428 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301429 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301430 .inner_join(batch_ledger)
1431 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1432 .select(
1433 Sum(
1434 Case()
1435 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1436 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1437 ).as_("batch_value"),
1438 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1439 "batch_qty"
1440 ),
1441 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301442 .where(
1443 (sle.item_code == item_code)
1444 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301445 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301446 & (sle.is_cancelled == 0)
1447 )
1448 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301449 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301450
1451 if batch_details and batch_details[0].batch_qty:
1452 return batch_details[0].batch_value / batch_details[0].batch_qty
1453
1454
Ankush Menat494bd9e2022-03-28 18:52:46 +05301455def get_valuation_rate(
1456 item_code,
1457 warehouse,
1458 voucher_type,
1459 voucher_no,
1460 allow_zero_rate=False,
1461 currency=None,
1462 company=None,
1463 raise_error_if_no_rate=True,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301464 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301465):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301466
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301467 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1468
Ankush Menatf7ffe042021-11-01 13:21:14 +05301469 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301470 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301471
Ankush Menat342d09a2022-02-19 14:28:51 +05301472 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301473 if warehouse and serial_and_batch_bundle:
1474 batch_obj = BatchNoValuation(
1475 sle=frappe._dict(
1476 {
1477 "item_code": item_code,
1478 "warehouse": warehouse,
1479 "actual_qty": -1,
1480 "serial_and_batch_bundle": serial_and_batch_bundle,
1481 }
1482 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301483 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301484
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301485 return batch_obj.get_incoming_rate()
1486
Ankush Menatf7ffe042021-11-01 13:21:14 +05301487 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301488 if last_valuation_rate := frappe.db.sql(
1489 """select valuation_rate
1490 from `tabStock Ledger Entry` force index (item_warehouse)
1491 where
1492 item_code = %s
1493 AND warehouse = %s
1494 AND valuation_rate >= 0
1495 AND is_cancelled = 0
1496 AND NOT (voucher_no = %s AND voucher_type = %s)
1497 order by posting_date desc, posting_time desc, name desc limit 1""",
1498 (item_code, warehouse, voucher_no, voucher_type),
1499 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301500 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301501
1502 # If negative stock allowed, and item delivered without any incoming entry,
1503 # system does not found any SLE, then take valuation rate from Item
1504 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301505
1506 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301507 # try Item Standard rate
1508 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301509
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301510 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301511 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301512 valuation_rate = frappe.db.get_value(
1513 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1514 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301515
Ankush Menat494bd9e2022-03-28 18:52:46 +05301516 if (
1517 not allow_zero_rate
1518 and not valuation_rate
1519 and raise_error_if_no_rate
1520 and cint(erpnext.is_perpetual_inventory_enabled(company))
1521 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301522 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301523
Ankush Menat494bd9e2022-03-28 18:52:46 +05301524 message = _(
1525 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1526 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301527 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301528 solutions = (
1529 "<li>"
1530 + _(
1531 "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."
1532 ).format(voucher_type)
1533 + "</li>"
1534 )
1535 solutions += (
1536 "<li>"
1537 + _("If not, you can Cancel / Submit this entry")
1538 + " {0} ".format(frappe.bold("after"))
1539 + _("performing either one below:")
1540 + "</li>"
1541 )
Marica97715f22020-05-11 20:45:37 +05301542 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1543 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1544 msg = message + solutions + sub_solutions + "</li>"
1545
1546 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301547
1548 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301549
Ankush Menat494bd9e2022-03-28 18:52:46 +05301550
Ankush Menate7109c12021-08-26 16:40:45 +05301551def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301552 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301553 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301554 qty_shift = args.actual_qty
1555
Ankush Menat7c839c42022-05-06 12:09:08 +05301556 args["time_format"] = "%H:%i:%s"
1557
marination8418c4b2021-06-22 21:35:25 +05301558 # find difference/shift in qty caused by stock reconciliation
1559 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301560 qty_shift = get_stock_reco_qty_shift(args)
1561
1562 # find the next nearest stock reco so that we only recalculate SLEs till that point
1563 next_stock_reco_detail = get_next_stock_reco(args)
1564 if next_stock_reco_detail:
1565 detail = next_stock_reco_detail[0]
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301566 if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no):
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301567 regenerate_sle_for_batch_stock_reco(detail)
1568
marination40389772021-07-02 17:13:45 +05301569 # add condition to update SLEs before this date & time
1570 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301571
Ankush Menat494bd9e2022-03-28 18:52:46 +05301572 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301573 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301574 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301575 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301576 where
1577 item_code = %(item_code)s
1578 and warehouse = %(warehouse)s
1579 and voucher_no != %(voucher_no)s
1580 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301581 and (
1582 posting_date > %(posting_date)s or
1583 (
1584 posting_date = %(posting_date)s and
1585 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1586 )
1587 )
marination40389772021-07-02 17:13:45 +05301588 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301589 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301590 args,
1591 )
Nabin Hait186a0452021-02-18 14:14:21 +05301592
1593 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1594
Ankush Menat494bd9e2022-03-28 18:52:46 +05301595
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301596def regenerate_sle_for_batch_stock_reco(detail):
1597 doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301598 doc.recalculate_current_qty(detail.item_code, detail.batch_no)
Rohit Waghchauref2253dd2023-04-22 11:16:12 +05301599
1600 if not frappe.db.exists(
1601 "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
1602 ):
Rohit Waghchaure2d5ccc02023-05-01 21:17:18 +05301603 doc.repost_future_sle_and_gle(force=True)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301604
1605
marination40389772021-07-02 17:13:45 +05301606def get_stock_reco_qty_shift(args):
1607 stock_reco_qty_shift = 0
1608 if args.get("is_cancelled"):
1609 if args.get("previous_qty_after_transaction"):
1610 # get qty (balance) that was set at submission
1611 last_balance = args.get("previous_qty_after_transaction")
1612 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1613 else:
1614 stock_reco_qty_shift = flt(args.actual_qty)
1615 else:
1616 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301617 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301618 "qty_after_transaction"
1619 )
marination40389772021-07-02 17:13:45 +05301620
1621 if last_balance is not None:
1622 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1623 else:
1624 stock_reco_qty_shift = args.qty_after_transaction
1625
1626 return stock_reco_qty_shift
1627
Ankush Menat494bd9e2022-03-28 18:52:46 +05301628
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301629def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301630 """Returns next nearest stock reconciliaton's details."""
1631
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301632 sle = frappe.qb.DocType("Stock Ledger Entry")
1633
1634 query = (
1635 frappe.qb.from_(sle)
1636 .select(
1637 sle.name,
1638 sle.posting_date,
1639 sle.posting_time,
1640 sle.creation,
1641 sle.voucher_no,
1642 sle.item_code,
1643 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301644 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301645 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301646 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301647 )
1648 .where(
1649 (sle.item_code == kwargs.get("item_code"))
1650 & (sle.warehouse == kwargs.get("warehouse"))
1651 & (sle.voucher_type == "Stock Reconciliation")
1652 & (sle.voucher_no != kwargs.get("voucher_no"))
1653 & (sle.is_cancelled == 0)
1654 & (
1655 (
1656 CombineDatetime(sle.posting_date, sle.posting_time)
1657 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301658 )
1659 | (
1660 (
1661 CombineDatetime(sle.posting_date, sle.posting_time)
1662 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301663 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301664 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301665 )
1666 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301667 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301668 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1669 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301670 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301671 )
1672
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301673 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301674 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301675
1676 return query.run(as_dict=True)
1677
marination40389772021-07-02 17:13:45 +05301678
1679def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301680 return f"""
1681 and
1682 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1683 or (
1684 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1685 and creation < '{detail.creation}'
1686 )
1687 )"""
1688
Ankush Menat494bd9e2022-03-28 18:52:46 +05301689
Ankush Menate7109c12021-08-26 16:40:45 +05301690def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301691 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301692 return
1693 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1694 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301695
Ankush Menat5eba5752021-12-07 23:03:52 +05301696 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301697
1698 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301699 message = _(
1700 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1701 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301702 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301703 frappe.get_desk_link("Item", args.item_code),
1704 frappe.get_desk_link("Warehouse", args.warehouse),
1705 neg_sle[0]["posting_date"],
1706 neg_sle[0]["posting_time"],
1707 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1708 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301709
Ankush Menat494bd9e2022-03-28 18:52:46 +05301710 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301711
1712 if not args.batch_no:
1713 return
1714
1715 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301716 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301717 message = _(
1718 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1719 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301720 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301721 frappe.get_desk_link("Batch", args.batch_no),
1722 frappe.get_desk_link("Warehouse", args.warehouse),
1723 neg_batch_sle[0]["posting_date"],
1724 neg_batch_sle[0]["posting_time"],
1725 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1726 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301727 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301728
Nabin Haita77b8c92020-12-21 14:45:50 +05301729
Maricad6078aa2022-06-17 15:13:13 +05301730def is_negative_with_precision(neg_sle, is_batch=False):
1731 """
1732 Returns whether system precision rounded qty is insufficient.
1733 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1734 """
1735
1736 if not neg_sle:
1737 return False
1738
1739 field = "cumulative_total" if is_batch else "qty_after_transaction"
1740 precision = cint(frappe.db.get_default("float_precision")) or 2
1741 qty_deficit = flt(neg_sle[0][field], precision)
1742
1743 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1744
1745
Nabin Haita77b8c92020-12-21 14:45:50 +05301746def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301747 return frappe.db.sql(
1748 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301749 select
1750 qty_after_transaction, posting_date, posting_time,
1751 voucher_type, voucher_no
1752 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301753 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301754 item_code = %(item_code)s
1755 and warehouse = %(warehouse)s
1756 and voucher_no != %(voucher_no)s
1757 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1758 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301759 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301760 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301761 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301762 """,
1763 args,
1764 as_dict=1,
1765 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301766
Ankush Menat5eba5752021-12-07 23:03:52 +05301767
1768def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301769 return frappe.db.sql(
1770 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301771 with batch_ledger as (
1772 select
1773 posting_date, posting_time, voucher_type, voucher_no,
1774 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1775 from `tabStock Ledger Entry`
1776 where
1777 item_code = %(item_code)s
1778 and warehouse = %(warehouse)s
1779 and batch_no=%(batch_no)s
1780 and is_cancelled = 0
1781 order by posting_date, posting_time, creation
1782 )
1783 select * from batch_ledger
1784 where
1785 cumulative_total < 0.0
1786 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1787 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301788 """,
1789 args,
1790 as_dict=1,
1791 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301792
1793
1794def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1795 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1796 return True
1797 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1798 return True
1799 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301800
1801
1802def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1803 """
1804 For inter company transfer, incoming rate is the average of the outgoing rate
1805 """
1806 rate = 0.0
1807
1808 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1809
1810 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1811
1812 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1813
1814 if reference_name:
1815 rate = frappe.get_cached_value(
1816 doctype,
1817 reference_name,
1818 "incoming_rate",
1819 )
1820
1821 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301822
1823
1824def is_internal_transfer(sle):
1825 data = frappe.get_cached_value(
1826 sle.voucher_type,
1827 sle.voucher_no,
1828 ["is_internal_supplier", "represents_company", "company"],
1829 as_dict=True,
1830 )
1831
1832 if data.is_internal_supplier and data.represents_company == data.company:
1833 return True