blob: 80256c3e672e6be714a01afdb3dc575530d1e0c1 [file] [log] [blame]
Maricad6078aa2022-06-17 15:13:13 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait902e8602013-01-08 18:29:24 +05303
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05304import copy
Nabin Hait26d46552013-01-09 15:23:05 +05305import json
Ankush Menatecdb4932022-04-17 19:06:13 +05306from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
9from frappe import _
10from frappe.model.meta import get_field_precision
Ankush Menate1c16872022-04-21 20:01:48 +053011from frappe.query_builder.functions import CombineDatetime, Sum
Ankush Menatcef84c22021-12-03 12:18:59 +053012from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
Achilles Rasquinha361366e2018-02-14 17:08:59 +053013
Chillar Anand915b3432021-09-02 16:44:59 +053014import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053015from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
Chillar Anand915b3432021-09-02 16:44:59 +053016from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053017 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053018 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053019 get_valuation_method,
20)
Ankush Menatb534fee2022-02-19 20:58:36 +053021from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053022
Nabin Hait97bce3a2021-07-12 13:24:43 +053023
Ankush Menat494bd9e2022-03-28 18:52:46 +053024class NegativeStockError(frappe.ValidationError):
25 pass
26
27
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053028class SerialNoExistsInFutureTransaction(frappe.ValidationError):
29 pass
Nabin Hait902e8602013-01-08 18:29:24 +053030
Anand Doshi5b004ff2013-09-25 19:55:41 +053031
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053032def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053033 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053034
Ankush Menat494bd9e2022-03-28 18:52:46 +053035 args:
36 - allow_negative_stock: disable negative stock valiations if true
37 - via_landed_cost_voucher: landed cost voucher cancels and reposts
38 entries of purchase document. This flag is used to identify if
39 cancellation and repost is happening via landed cost voucher, in
40 such cases certain validations need to be ignored (like negative
41 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053042 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053043 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053044
Nabin Haitca775742013-09-26 16:16:44 +053045 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053046 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053047 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053048 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053049 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053050
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053051 args = get_args_for_future_sle(sl_entries[0])
52 future_sle_exists(args, sl_entries)
53
Nabin Haitca775742013-09-26 16:16:44 +053054 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053055 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053056 validate_serial_no(sle)
57
Nabin Haita77b8c92020-12-21 14:45:50 +053058 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053059 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053060
Ankush Menat494bd9e2022-03-28 18:52:46 +053061 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
62 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
63 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
64 )
65 sle["incoming_rate"] = 0.0
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("incoming_rate"):
68 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
69 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
70 )
71 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053072
Ankush Menat494bd9e2022-03-28 18:52:46 +053073 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053074 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053075
Nabin Haita77b8c92020-12-21 14:45:50 +053076 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053077
78 if sle.get("voucher_type") == "Stock Reconciliation":
79 # preserve previous_qty_after_transaction for qty reposting
80 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
81
Ankush Menat494bd9e2022-03-28 18:52:46 +053082 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053083 if is_stock_item:
84 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053085 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053086 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053087 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +053088 frappe.msgprint(
89 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
90 )
91
Ankush Menatcef84c22021-12-03 12:18:59 +053092
93def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
94 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
95 if not args.get("posting_date"):
96 args["posting_date"] = nowdate()
97
marination7a5fd712022-07-04 17:46:54 +053098 if not (args.get("is_cancelled") and via_landed_cost_voucher):
99 # Reposts only current voucher SL Entries
100 # Updates valuation rate, stock value, stock queue for current transaction
101 update_entries_after(
102 {
103 "item_code": args.get("item_code"),
104 "warehouse": args.get("warehouse"),
105 "posting_date": args.get("posting_date"),
106 "posting_time": args.get("posting_time"),
107 "voucher_type": args.get("voucher_type"),
108 "voucher_no": args.get("voucher_no"),
109 "sle_id": args.get("name"),
110 "creation": args.get("creation"),
111 },
112 allow_negative_stock=allow_negative_stock,
113 via_landed_cost_voucher=via_landed_cost_voucher,
114 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530115
116 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530117 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530118 update_qty_in_future_sle(args, allow_negative_stock)
119
Nabin Haitadeb9762014-10-06 11:53:52 +0530120
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530121def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530122 return frappe._dict(
123 {
124 "voucher_type": row.get("voucher_type"),
125 "voucher_no": row.get("voucher_no"),
126 "posting_date": row.get("posting_date"),
127 "posting_time": row.get("posting_time"),
128 }
129 )
130
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530131
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530132def validate_serial_no(sle):
133 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530134
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530135 for sn in get_serial_nos(sle.serial_no):
136 args = copy.deepcopy(sle)
137 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530138 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530139
140 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530141 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530142 voucher_type = frappe.bold(row.voucher_type)
143 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530144 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530145
146 if vouchers:
147 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530148 msg = (
149 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
150 The list of the transactions are as below."""
151 + "<br><br><ul><li>"
152 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530153
Ankush Menat494bd9e2022-03-28 18:52:46 +0530154 msg += "</li><li>".join(vouchers)
155 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530156
Ankush Menat494bd9e2022-03-28 18:52:46 +0530157 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530158 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
159
Ankush Menat494bd9e2022-03-28 18:52:46 +0530160
Nabin Hait186a0452021-02-18 14:14:21 +0530161def validate_cancellation(args):
162 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163 repost_entry = frappe.db.get_value(
164 "Repost Item Valuation",
165 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
166 ["name", "status"],
167 as_dict=1,
168 )
Nabin Hait186a0452021-02-18 14:14:21 +0530169
170 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530171 if repost_entry.status == "In Progress":
172 frappe.throw(
173 _(
174 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
175 )
176 )
177 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530178 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530179 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530180 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530181 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530182
Ankush Menat494bd9e2022-03-28 18:52:46 +0530183
Nabin Hait9653f602013-08-20 15:37:33 +0530184def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530185 frappe.db.sql(
186 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530187 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530188 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530189 (now(), frappe.session.user, voucher_type, voucher_no),
190 )
191
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530192
Nabin Hait54c865e2015-03-27 15:38:31 +0530193def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530194 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530195 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530196 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530197 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530198 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530199 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530200 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530201
Ankush Menat494bd9e2022-03-28 18:52:46 +0530202
203def repost_future_sle(
204 args=None,
205 voucher_type=None,
206 voucher_no=None,
207 allow_negative_stock=None,
208 via_landed_cost_voucher=False,
209 doc=None,
210):
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530211
212 items_to_be_repost = get_items_to_be_repost(
213 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
214 )
215 if items_to_be_repost:
216 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530217
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530218 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Ankush Menatecdb4932022-04-17 19:06:13 +0530219 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530220
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530221 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530222 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530223 validate_item_warehouse(args[i])
224
Ankush Menat494bd9e2022-03-28 18:52:46 +0530225 obj = update_entries_after(
226 {
227 "item_code": args[i].get("item_code"),
228 "warehouse": args[i].get("warehouse"),
229 "posting_date": args[i].get("posting_date"),
230 "posting_time": args[i].get("posting_time"),
231 "creation": args[i].get("creation"),
232 "distinct_item_warehouses": distinct_item_warehouses,
233 },
234 allow_negative_stock=allow_negative_stock,
235 via_landed_cost_voucher=via_landed_cost_voucher,
236 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530237 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530238
Ankush Menat494bd9e2022-03-28 18:52:46 +0530239 distinct_item_warehouses[
240 (args[i].get("item_code"), args[i].get("warehouse"))
241 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530242
Nabin Hait97bce3a2021-07-12 13:24:43 +0530243 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530244 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530245 if ("args_idx" not in data and not data.reposting_status) or (
246 data.sle_changed and data.reposting_status
247 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530248 data.args_idx = len(args)
249 args.append(data.sle)
250 elif data.sle_changed and not data.reposting_status:
251 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530252
Nabin Hait97bce3a2021-07-12 13:24:43 +0530253 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530254 i += 1
255
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530256 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530257 update_args_in_repost_item_valuation(
258 doc, i, args, distinct_item_warehouses, affected_transactions
259 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530260
Ankush Menat494bd9e2022-03-28 18:52:46 +0530261
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530262def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530263 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530264 if not args.get(field):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530265 validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530266 frappe.throw(_(validation_msg))
267
Ankush Menat494bd9e2022-03-28 18:52:46 +0530268
Ankush Menatecdb4932022-04-17 19:06:13 +0530269def update_args_in_repost_item_valuation(
270 doc, index, args, distinct_item_warehouses, affected_transactions
271):
Ankush Menatecdb4932022-04-17 19:06:13 +0530272 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530273 {
274 "items_to_be_repost": json.dumps(args, default=str),
275 "distinct_item_and_warehouse": json.dumps(
276 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
277 ),
278 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530279 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530280 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530281 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530282
Ankush Menatecdb4932022-04-17 19:06:13 +0530283 if not frappe.flags.in_test:
284 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530285
Ankush Menat494bd9e2022-03-28 18:52:46 +0530286 frappe.publish_realtime(
287 "item_reposting_progress",
288 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
289 )
290
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530291
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530292def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
293 items_to_be_repost = []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530294 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530295 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530297 if not items_to_be_repost and voucher_type and voucher_no:
298 items_to_be_repost = frappe.db.get_all(
299 "Stock Ledger Entry",
300 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
301 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
302 order_by="creation asc",
303 group_by="item_code, warehouse",
304 )
305
306 return items_to_be_repost
Nabin Hait74c281c2013-08-19 16:17:18 +0530307
Ankush Menat494bd9e2022-03-28 18:52:46 +0530308
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530309def get_distinct_item_warehouse(args=None, doc=None):
310 distinct_item_warehouses = {}
311 if doc and doc.distinct_item_and_warehouse:
312 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530313 distinct_item_warehouses = {
314 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
315 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530316 else:
317 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530318 distinct_item_warehouses.setdefault(
319 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
320 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530321
322 return distinct_item_warehouses
323
Ankush Menat494bd9e2022-03-28 18:52:46 +0530324
Ankush Menatecdb4932022-04-17 19:06:13 +0530325def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
326 if not doc.affected_transactions:
327 return set()
328
329 transactions = frappe.parse_json(doc.affected_transactions)
330 return {tuple(transaction) for transaction in transactions}
331
332
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530333def get_current_index(doc=None):
334 if doc and doc.current_index:
335 return doc.current_index
336
Ankush Menat494bd9e2022-03-28 18:52:46 +0530337
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530338class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530339 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530340 update valution rate and qty after transaction
341 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530342
Ankush Menat494bd9e2022-03-28 18:52:46 +0530343 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530344
Ankush Menat494bd9e2022-03-28 18:52:46 +0530345 args = {
346 "item_code": "ABC",
347 "warehouse": "XYZ",
348 "posting_date": "2012-12-12",
349 "posting_time": "12:00"
350 }
Nabin Hait902e8602013-01-08 18:29:24 +0530351 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530352
353 def __init__(
354 self,
355 args,
356 allow_zero_rate=False,
357 allow_negative_stock=None,
358 via_landed_cost_voucher=False,
359 verbose=1,
360 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530361 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530362 self.verbose = verbose
363 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530364 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530365 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530366 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
367 item_code=self.item_code
368 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530369
Nabin Haita77b8c92020-12-21 14:45:50 +0530370 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530371 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530372 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530373
Nabin Haita77b8c92020-12-21 14:45:50 +0530374 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530375 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530376 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530377
378 self.new_items_found = False
379 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530380 self.affected_transactions: Set[Tuple[str, str]] = set()
Nabin Haita77b8c92020-12-21 14:45:50 +0530381
382 self.data = frappe._dict()
383 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530384 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530385
Maricad6078aa2022-06-17 15:13:13 +0530386 def set_precision(self):
387 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
388 self.currency_precision = get_field_precision(
389 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530390 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530391
392 def initialize_previous_data(self, args):
393 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530394 Get previous sl entries for current item for each related warehouse
395 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530396
Ankush Menat494bd9e2022-03-28 18:52:46 +0530397 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530398
Ankush Menat494bd9e2022-03-28 18:52:46 +0530399 self.data = {
400 warehouse1: {
401 'previus_sle': {},
402 'qty_after_transaction': 10,
403 'valuation_rate': 100,
404 'stock_value': 1000,
405 'prev_stock_value': 1000,
406 'stock_queue': '[[10, 100]]',
407 'stock_value_difference': 1000
408 }
409 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530410
411 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530412 self.data.setdefault(args.warehouse, frappe._dict())
413 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530414 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530415 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530416
Ankush Menatc1d986a2021-08-31 19:43:42 +0530417 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
418 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
419
Ankush Menat494bd9e2022-03-28 18:52:46 +0530420 warehouse_dict.update(
421 {
422 "prev_stock_value": previous_sle.stock_value or 0.0,
423 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
424 "stock_value_difference": 0.0,
425 }
426 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530427
Nabin Haita77b8c92020-12-21 14:45:50 +0530428 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530429 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530430
Nabin Haita77b8c92020-12-21 14:45:50 +0530431 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530432 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530433 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530434 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530435 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530436 entries_to_fix = self.get_future_entries_to_fix()
437
438 i = 0
439 while i < len(entries_to_fix):
440 sle = entries_to_fix[i]
441 i += 1
442
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530443 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530444
Nabin Haita77b8c92020-12-21 14:45:50 +0530445 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530446 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530447
Nabin Hait186a0452021-02-18 14:14:21 +0530448 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530449
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530450 if self.exceptions:
451 self.raise_exceptions()
452
Nabin Hait186a0452021-02-18 14:14:21 +0530453 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530454 sl_entries = self.get_sle_against_current_voucher()
455 for sle in sl_entries:
456 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530457
Nabin Haita77b8c92020-12-21 14:45:50 +0530458 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530459 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530460
Ankush Menat494bd9e2022-03-28 18:52:46 +0530461 return frappe.db.sql(
462 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530463 select
464 *, timestamp(posting_date, posting_time) as "timestamp"
465 from
466 `tabStock Ledger Entry`
467 where
468 item_code = %(item_code)s
469 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530470 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530471 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
472
Nabin Haita77b8c92020-12-21 14:45:50 +0530473 order by
474 creation ASC
475 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530476 """,
477 self.args,
478 as_dict=1,
479 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530480
Nabin Haita77b8c92020-12-21 14:45:50 +0530481 def get_future_entries_to_fix(self):
482 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530483 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
484 {"item_code": self.item_code, "warehouse": self.args.warehouse}
485 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530486
Nabin Haita77b8c92020-12-21 14:45:50 +0530487 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530488
Nabin Haita77b8c92020-12-21 14:45:50 +0530489 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530490 dependant_sle = get_sle_by_voucher_detail_no(
491 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
492 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530493
Nabin Haita77b8c92020-12-21 14:45:50 +0530494 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530495 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530496 elif (
497 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
498 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530499 return entries_to_fix
500 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530501 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530502 return entries_to_fix
503 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
504 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530505 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530506 self.initialize_previous_data(dependant_sle)
507 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530508 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530509
510 def update_distinct_item_warehouses(self, dependant_sle):
511 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530512 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530513 if key not in self.distinct_item_warehouses:
514 self.distinct_item_warehouses[key] = val
515 self.new_items_found = True
516 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530517 existing_sle_posting_date = (
518 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
519 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530520 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
521 val.sle_changed = True
522 self.distinct_item_warehouses[key] = val
523 self.new_items_found = True
524
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530525 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530526 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
527
Nabin Haita77b8c92020-12-21 14:45:50 +0530528 # previous sle data for this warehouse
529 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530530 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530531
Anand Doshi0dc79f42015-04-06 12:59:34 +0530532 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 +0530533 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530534 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530535 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530536 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530537 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530538
Nabin Haita77b8c92020-12-21 14:45:50 +0530539 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530540 if not self.args.get("sle_id"):
541 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530542
Ankush Menat66bf21f2022-01-16 20:45:59 +0530543 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530544 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530545 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530546 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530547 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530548
Ankush Menat494bd9e2022-03-28 18:52:46 +0530549 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
550 self.wh_data.valuation_rate
551 )
552 elif sle.batch_no and frappe.db.get_value(
553 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
554 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530555 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530556 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530557 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530558 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530559 self.wh_data.valuation_rate = sle.valuation_rate
560 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530561 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
562 self.wh_data.valuation_rate
563 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530564 if self.valuation_method != "Moving Average":
565 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530566 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530567 if self.valuation_method == "Moving Average":
568 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530569 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530570 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
571 self.wh_data.valuation_rate
572 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530573 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530574 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530575
Rushabh Mehta54047782013-12-26 11:07:46 +0530576 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530577 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530578 if not self.wh_data.qty_after_transaction:
579 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530580 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
581 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530582
Nabin Hait902e8602013-01-08 18:29:24 +0530583 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530584 sle.qty_after_transaction = self.wh_data.qty_after_transaction
585 sle.valuation_rate = self.wh_data.valuation_rate
586 sle.stock_value = self.wh_data.stock_value
587 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530588 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530589 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530590 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530591
Ankush Menat701878f2022-03-01 18:08:29 +0530592 if not self.args.get("sle_id"):
593 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530594
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530595 def validate_negative_stock(self, sle):
596 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530597 validate negative stock for entries current datetime onwards
598 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530599 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530600 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Maricad6078aa2022-06-17 15:13:13 +0530601 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530602
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530603 if diff < 0 and abs(diff) > 0.0001:
604 # negative stock!
605 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530606 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530607 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530608 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530609 return True
610
Nabin Haita77b8c92020-12-21 14:45:50 +0530611 def get_dynamic_incoming_outgoing_rate(self, sle):
612 # Get updated incoming/outgoing rate from transaction
613 if sle.recalculate_rate:
614 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
615
616 if flt(sle.actual_qty) >= 0:
617 sle.incoming_rate = rate
618 else:
619 sle.outgoing_rate = rate
620
621 def get_incoming_outgoing_rate_from_transaction(self, sle):
622 rate = 0
623 # Material Transfer, Repack, Manufacturing
624 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530625 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530626 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
627 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530628 elif sle.voucher_type in (
629 "Purchase Receipt",
630 "Purchase Invoice",
631 "Delivery Note",
632 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530633 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530634 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530635 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530636 from erpnext.controllers.sales_and_purchase_return import (
637 get_rate_for_return, # don't move this import to top
638 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530639
640 rate = get_rate_for_return(
641 sle.voucher_type,
642 sle.voucher_no,
643 sle.item_code,
644 voucher_detail_no=sle.voucher_detail_no,
645 sle=sle,
646 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530647 else:
648 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530649 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530650 elif sle.voucher_type == "Subcontracting Receipt":
651 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530652 else:
653 rate_field = "incoming_rate"
654
655 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530656 item_code, incoming_rate = frappe.db.get_value(
657 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
658 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530659
660 if item_code == sle.item_code:
661 rate = incoming_rate
662 else:
663 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
664 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530665 elif sle == "Subcontracting Receipt":
666 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530667 else:
668 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530669
Ankush Menat494bd9e2022-03-28 18:52:46 +0530670 rate = frappe.db.get_value(
671 ref_doctype,
672 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
673 rate_field,
674 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530675
676 return rate
677
678 def update_outgoing_rate_on_transaction(self, sle):
679 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530680 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
681 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530682 """
683 if sle.actual_qty and sle.voucher_detail_no:
684 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
685
686 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
687 self.update_rate_on_stock_entry(sle, outgoing_rate)
688 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
689 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
690 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
691 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530692 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
693 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530694
695 def update_rate_on_stock_entry(self, sle, outgoing_rate):
696 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
697
Ankush Menat701878f2022-03-01 18:08:29 +0530698 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
699 if not sle.dependant_sle_voucher_detail_no:
700 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530701
702 def recalculate_amounts_in_stock_entry(self, voucher_no):
703 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530704 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
705 stock_entry.db_update()
706 for d in stock_entry.items:
707 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530708
Nabin Haita77b8c92020-12-21 14:45:50 +0530709 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
710 # Update item's incoming rate on transaction
711 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
712 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530713 frappe.db.set_value(
714 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
715 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530716 else:
717 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530718 frappe.db.set_value(
719 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530720 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530721 "incoming_rate",
722 outgoing_rate,
723 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530724
725 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
726 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530727 frappe.db.set_value(
728 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
729 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530730 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530731 frappe.db.set_value(
732 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
733 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530734
735 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530736 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530737 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530738 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530739 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530740 d.db_update()
741
Sagar Sharma323bdf82022-05-17 15:14:07 +0530742 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
743 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
744 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
745 else:
746 frappe.db.set_value(
747 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
748 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530749
750 def get_serialized_values(self, sle):
751 incoming_rate = flt(sle.incoming_rate)
752 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530753 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530754
755 if incoming_rate < 0:
756 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530757 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530758
Nabin Hait2620bf42016-02-29 11:30:27 +0530759 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530760 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530761 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530762 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530763 # In case of delivery/stock issue, get average purchase rate
764 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530765 if not sle.is_cancelled:
766 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
767 stock_value_change = -1 * outgoing_value
768 else:
769 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530770
Nabin Haita77b8c92020-12-21 14:45:50 +0530771 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530772
Nabin Hait2620bf42016-02-29 11:30:27 +0530773 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530774 new_stock_value = (
775 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
776 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530777 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530778 # calculate new valuation rate only if stock value is positive
779 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530780 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530781
Nabin Haita77b8c92020-12-21 14:45:50 +0530782 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530783 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
784 sle.voucher_type, sle.voucher_detail_no
785 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530786 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530787 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530788
Nabin Hait328c4f92020-01-02 19:00:32 +0530789 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
790 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530791 all_serial_nos = frappe.get_all(
792 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
793 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530794
Ankush Menat494bd9e2022-03-28 18:52:46 +0530795 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 +0530796
797 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530798 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530799 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530800 incoming_rate = frappe.db.sql(
801 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530802 select incoming_rate
803 from `tabStock Ledger Entry`
804 where
805 company = %s
806 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530807 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530808 and (serial_no = %s
809 or serial_no like %s
810 or serial_no like %s
811 or serial_no like %s
812 )
813 order by posting_date desc
814 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530815 """,
816 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
817 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530818
819 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
820
821 return incoming_values
822
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530823 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530824 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530825 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530826 if new_stock_qty >= 0:
827 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530828 if flt(self.wh_data.qty_after_transaction) <= 0:
829 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530830 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530831 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
832 actual_qty * sle.incoming_rate
833 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530834
Nabin Haita77b8c92020-12-21 14:45:50 +0530835 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530836
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530837 elif sle.outgoing_rate:
838 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530839 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
840 actual_qty * sle.outgoing_rate
841 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530842
Nabin Haita77b8c92020-12-21 14:45:50 +0530843 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530844 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530845 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530846 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530847 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
848 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530849
Nabin Haita77b8c92020-12-21 14:45:50 +0530850 if not self.wh_data.valuation_rate and actual_qty > 0:
851 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530852
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530853 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800854 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530855 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530856 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
857 sle.voucher_type, sle.voucher_detail_no
858 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800859 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530860 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530861
Ankush Menatf089d392022-02-02 12:51:21 +0530862 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530863 incoming_rate = flt(sle.incoming_rate)
864 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530865 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530866
Ankush Menat494bd9e2022-03-28 18:52:46 +0530867 self.wh_data.qty_after_transaction = round_off_if_near_zero(
868 self.wh_data.qty_after_transaction + actual_qty
869 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530870
Ankush Menat97e18a12022-01-15 17:42:25 +0530871 if self.valuation_method == "LIFO":
872 stock_queue = LIFOValuation(self.wh_data.stock_queue)
873 else:
874 stock_queue = FIFOValuation(self.wh_data.stock_queue)
875
Ankush Menatb534fee2022-02-19 20:58:36 +0530876 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
877
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530878 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530879 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530880 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530881
Ankush Menat4b29fb62021-12-18 18:40:22 +0530882 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530883 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
884 sle.voucher_type, sle.voucher_detail_no
885 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530886 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530887 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530888 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530889 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530890
Ankush Menat494bd9e2022-03-28 18:52:46 +0530891 stock_queue.remove_stock(
892 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
893 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530894
Ankush Menatb534fee2022-02-19 20:58:36 +0530895 _qty, stock_value = stock_queue.get_total_stock_and_value()
896
897 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530898
Ankush Menat97e18a12022-01-15 17:42:25 +0530899 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530900 self.wh_data.stock_value = round_off_if_near_zero(
901 self.wh_data.stock_value + stock_value_difference
902 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530903
Nabin Haita77b8c92020-12-21 14:45:50 +0530904 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530905 self.wh_data.stock_queue.append(
906 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
907 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530908
Ankush Menatb534fee2022-02-19 20:58:36 +0530909 if self.wh_data.qty_after_transaction:
910 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
911
Ankush Menatce0514c2022-02-15 11:41:41 +0530912 def update_batched_values(self, sle):
913 incoming_rate = flt(sle.incoming_rate)
914 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530915
Ankush Menat494bd9e2022-03-28 18:52:46 +0530916 self.wh_data.qty_after_transaction = round_off_if_near_zero(
917 self.wh_data.qty_after_transaction + actual_qty
918 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530919
920 if actual_qty > 0:
921 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530922 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530923 outgoing_rate = get_batch_incoming_rate(
924 item_code=sle.item_code,
925 warehouse=sle.warehouse,
926 batch_no=sle.batch_no,
927 posting_date=sle.posting_date,
928 posting_time=sle.posting_time,
929 creation=sle.creation,
930 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530931 if outgoing_rate is None:
932 # This can *only* happen if qty available for the batch is zero.
933 # in such case fall back various other rates.
934 # future entries will correct the overall accounting as each
935 # batch individually uses moving average rates.
936 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530937 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530938
Ankush Menat494bd9e2022-03-28 18:52:46 +0530939 self.wh_data.stock_value = round_off_if_near_zero(
940 self.wh_data.stock_value + stock_value_difference
941 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530942 if self.wh_data.qty_after_transaction:
943 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530944
Javier Wong9b11d9b2017-04-14 18:24:04 +0800945 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530946 ref_item_dt = ""
947
948 if voucher_type == "Stock Entry":
949 ref_item_dt = voucher_type + " Detail"
950 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
951 ref_item_dt = voucher_type + " Item"
952
953 if ref_item_dt:
954 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
955 else:
956 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530957
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530958 def get_fallback_rate(self, sle) -> float:
959 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530960 This should only get used for negative stock."""
961 return get_valuation_rate(
962 sle.item_code,
963 sle.warehouse,
964 sle.voucher_type,
965 sle.voucher_no,
966 self.allow_zero_rate,
967 currency=erpnext.get_company_currency(sle.company),
968 company=sle.company,
969 batch_no=sle.batch_no,
970 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530971
Nabin Haita77b8c92020-12-21 14:45:50 +0530972 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530973 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530974 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
975 sle = sle[0] if sle else frappe._dict()
976 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530977
Nabin Haita77b8c92020-12-21 14:45:50 +0530978 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530979 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530980 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530981
982 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530983 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530984 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530985 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530986
Ankush Menat494bd9e2022-03-28 18:52:46 +0530987 if (
988 exceptions[0]["voucher_type"],
989 exceptions[0]["voucher_no"],
990 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +0530991
Nabin Haita77b8c92020-12-21 14:45:50 +0530992 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530993 abs(deficiency),
994 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
995 frappe.get_desk_link("Warehouse", warehouse),
996 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530997 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530998 msg = _(
999 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1000 ).format(
1001 abs(deficiency),
1002 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1003 frappe.get_desk_link("Warehouse", warehouse),
1004 exceptions[0]["posting_date"],
1005 exceptions[0]["posting_time"],
1006 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1007 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301008
Nabin Haita77b8c92020-12-21 14:45:50 +05301009 if msg:
1010 msg_list.append(msg)
1011
1012 if msg_list:
1013 message = "\n\n".join(msg_list)
1014 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301015 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301016 else:
1017 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301018
Nabin Haita77b8c92020-12-21 14:45:50 +05301019 def update_bin(self):
1020 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301021 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301022 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301023
Ankush Menat494bd9e2022-03-28 18:52:46 +05301024 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301025 if data.valuation_rate is not None:
1026 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301027 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301028
marination8418c4b2021-06-22 21:35:25 +05301029
1030def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1031 """get stock ledger entries filtered by specific posting datetime conditions"""
1032
Ankush Menat494bd9e2022-03-28 18:52:46 +05301033 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301034 if not args.get("posting_date"):
1035 args["posting_date"] = "1900-01-01"
1036 if not args.get("posting_time"):
1037 args["posting_time"] = "00:00"
1038
1039 voucher_condition = ""
1040 if exclude_current_voucher:
1041 voucher_no = args.get("voucher_no")
1042 voucher_condition = f"and voucher_no != '{voucher_no}'"
1043
Ankush Menat494bd9e2022-03-28 18:52:46 +05301044 sle = frappe.db.sql(
1045 """
marination8418c4b2021-06-22 21:35:25 +05301046 select *, timestamp(posting_date, posting_time) as "timestamp"
1047 from `tabStock Ledger Entry`
1048 where item_code = %(item_code)s
1049 and warehouse = %(warehouse)s
1050 and is_cancelled = 0
1051 {voucher_condition}
1052 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1053 order by timestamp(posting_date, posting_time) desc, creation desc
1054 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301055 for update""".format(
1056 voucher_condition=voucher_condition
1057 ),
1058 args,
1059 as_dict=1,
1060 )
marination8418c4b2021-06-22 21:35:25 +05301061
1062 return sle[0] if sle else frappe._dict()
1063
Ankush Menat494bd9e2022-03-28 18:52:46 +05301064
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301065def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301066 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301067 get the last sle on or before the current time-bucket,
1068 to get actual qty before transaction, this function
1069 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301070
Ankush Menat494bd9e2022-03-28 18:52:46 +05301071 args = {
1072 "item_code": "ABC",
1073 "warehouse": "XYZ",
1074 "posting_date": "2012-12-12",
1075 "posting_time": "12:00",
1076 "sle": "name of reference Stock Ledger Entry"
1077 }
Anand Doshi1b531862013-01-10 19:29:51 +05301078 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301079 args["name"] = args.get("sle", None) or ""
1080 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301081 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301082
Ankush Menat494bd9e2022-03-28 18:52:46 +05301083
1084def get_stock_ledger_entries(
1085 previous_sle,
1086 operator=None,
1087 order="desc",
1088 limit=None,
1089 for_update=False,
1090 debug=False,
1091 check_serial_no=True,
1092):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301093 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301094 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1095 operator
1096 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301097 if previous_sle.get("warehouse"):
1098 conditions += " and warehouse = %(warehouse)s"
1099 elif previous_sle.get("warehouse_condition"):
1100 conditions += " and " + previous_sle.get("warehouse_condition")
1101
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301102 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301103 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1104 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301105 conditions += (
1106 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301107 (
1108 serial_no = {0}
1109 or serial_no like {1}
1110 or serial_no like {2}
1111 or serial_no like {3}
1112 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301113 """
1114 ).format(
1115 frappe.db.escape(serial_no),
1116 frappe.db.escape("{}\n%".format(serial_no)),
1117 frappe.db.escape("%\n{}".format(serial_no)),
1118 frappe.db.escape("%\n{}\n%".format(serial_no)),
1119 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301120
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301121 if not previous_sle.get("posting_date"):
1122 previous_sle["posting_date"] = "1900-01-01"
1123 if not previous_sle.get("posting_time"):
1124 previous_sle["posting_time"] = "00:00"
1125
1126 if operator in (">", "<=") and previous_sle.get("name"):
1127 conditions += " and name!=%(name)s"
1128
Ankush Menat494bd9e2022-03-28 18:52:46 +05301129 return frappe.db.sql(
1130 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301131 select *, timestamp(posting_date, posting_time) as "timestamp"
1132 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301133 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301134 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301135 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301136 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301137 %(limit)s %(for_update)s"""
1138 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301139 "conditions": conditions,
1140 "limit": limit or "",
1141 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301142 "order": order,
1143 },
1144 previous_sle,
1145 as_dict=1,
1146 debug=debug,
1147 )
1148
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301149
Nabin Haita77b8c92020-12-21 14:45:50 +05301150def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301151 return frappe.db.get_value(
1152 "Stock Ledger Entry",
1153 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1154 [
1155 "item_code",
1156 "warehouse",
1157 "posting_date",
1158 "posting_time",
1159 "timestamp(posting_date, posting_time) as timestamp",
1160 ],
1161 as_dict=1,
1162 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301163
Ankush Menatce0514c2022-02-15 11:41:41 +05301164
Ankush Menat494bd9e2022-03-28 18:52:46 +05301165def get_batch_incoming_rate(
1166 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1167):
1168
Ankush Menat102fff22022-02-19 15:51:04 +05301169 sle = frappe.qb.DocType("Stock Ledger Entry")
1170
Ankush Menate1c16872022-04-21 20:01:48 +05301171 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301172 posting_date, posting_time
1173 )
Ankush Menat102fff22022-02-19 15:51:04 +05301174 if creation:
1175 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301176 CombineDatetime(sle.posting_date, sle.posting_time)
1177 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301178 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301179
1180 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301181 frappe.qb.from_(sle)
1182 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1183 .where(
1184 (sle.item_code == item_code)
1185 & (sle.warehouse == warehouse)
1186 & (sle.batch_no == batch_no)
1187 & (sle.is_cancelled == 0)
1188 )
1189 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301190 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301191
1192 if batch_details and batch_details[0].batch_qty:
1193 return batch_details[0].batch_value / batch_details[0].batch_qty
1194
1195
Ankush Menat494bd9e2022-03-28 18:52:46 +05301196def get_valuation_rate(
1197 item_code,
1198 warehouse,
1199 voucher_type,
1200 voucher_no,
1201 allow_zero_rate=False,
1202 currency=None,
1203 company=None,
1204 raise_error_if_no_rate=True,
1205 batch_no=None,
1206):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301207
Ankush Menatf7ffe042021-11-01 13:21:14 +05301208 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301209 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301210
Ankush Menat342d09a2022-02-19 14:28:51 +05301211 last_valuation_rate = None
1212
1213 # Get moving average rate of a specific batch number
1214 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301215 last_valuation_rate = frappe.db.sql(
1216 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301217 select sum(stock_value_difference) / sum(actual_qty)
1218 from `tabStock Ledger Entry`
1219 where
1220 item_code = %s
1221 AND warehouse = %s
1222 AND batch_no = %s
1223 AND is_cancelled = 0
1224 AND NOT (voucher_no = %s AND voucher_type = %s)
1225 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301226 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1227 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301228
Ankush Menatf7ffe042021-11-01 13:21:14 +05301229 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301230 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301231 last_valuation_rate = frappe.db.sql(
1232 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301233 from `tabStock Ledger Entry` force index (item_warehouse)
1234 where
1235 item_code = %s
1236 AND warehouse = %s
1237 AND valuation_rate >= 0
1238 AND is_cancelled = 0
1239 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301240 order by posting_date desc, posting_time desc, name desc limit 1""",
1241 (item_code, warehouse, voucher_no, voucher_type),
1242 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301243
1244 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301245 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301246 last_valuation_rate = frappe.db.sql(
1247 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301248 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301249 where
1250 item_code = %s
1251 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301252 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301253 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301254 order by posting_date desc, posting_time desc, name desc limit 1""",
1255 (item_code, voucher_no, voucher_type),
1256 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301257
Nabin Haita645f362018-03-01 10:31:24 +05301258 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301259 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301260
1261 # If negative stock allowed, and item delivered without any incoming entry,
1262 # system does not found any SLE, then take valuation rate from Item
1263 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301264
1265 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301266 # try Item Standard rate
1267 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301268
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301269 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301270 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301271 valuation_rate = frappe.db.get_value(
1272 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1273 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301274
Ankush Menat494bd9e2022-03-28 18:52:46 +05301275 if (
1276 not allow_zero_rate
1277 and not valuation_rate
1278 and raise_error_if_no_rate
1279 and cint(erpnext.is_perpetual_inventory_enabled(company))
1280 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301281 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301282
Ankush Menat494bd9e2022-03-28 18:52:46 +05301283 message = _(
1284 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1285 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301286 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301287 solutions = (
1288 "<li>"
1289 + _(
1290 "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."
1291 ).format(voucher_type)
1292 + "</li>"
1293 )
1294 solutions += (
1295 "<li>"
1296 + _("If not, you can Cancel / Submit this entry")
1297 + " {0} ".format(frappe.bold("after"))
1298 + _("performing either one below:")
1299 + "</li>"
1300 )
Marica97715f22020-05-11 20:45:37 +05301301 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1302 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1303 msg = message + solutions + sub_solutions + "</li>"
1304
1305 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301306
1307 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301308
Ankush Menat494bd9e2022-03-28 18:52:46 +05301309
Ankush Menate7109c12021-08-26 16:40:45 +05301310def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301311 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301312 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301313 qty_shift = args.actual_qty
1314
Ankush Menat7c839c42022-05-06 12:09:08 +05301315 args["time_format"] = "%H:%i:%s"
1316
marination8418c4b2021-06-22 21:35:25 +05301317 # find difference/shift in qty caused by stock reconciliation
1318 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301319 qty_shift = get_stock_reco_qty_shift(args)
1320
1321 # find the next nearest stock reco so that we only recalculate SLEs till that point
1322 next_stock_reco_detail = get_next_stock_reco(args)
1323 if next_stock_reco_detail:
1324 detail = next_stock_reco_detail[0]
1325 # add condition to update SLEs before this date & time
1326 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301327
Ankush Menat494bd9e2022-03-28 18:52:46 +05301328 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301329 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301330 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301331 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301332 where
1333 item_code = %(item_code)s
1334 and warehouse = %(warehouse)s
1335 and voucher_no != %(voucher_no)s
1336 and is_cancelled = 0
Ankush Menat7c839c42022-05-06 12:09:08 +05301337 and timestamp(posting_date, time_format(posting_time, %(time_format)s))
1338 > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
marination40389772021-07-02 17:13:45 +05301339 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301340 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301341 args,
1342 )
Nabin Hait186a0452021-02-18 14:14:21 +05301343
1344 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1345
Ankush Menat494bd9e2022-03-28 18:52:46 +05301346
marination40389772021-07-02 17:13:45 +05301347def get_stock_reco_qty_shift(args):
1348 stock_reco_qty_shift = 0
1349 if args.get("is_cancelled"):
1350 if args.get("previous_qty_after_transaction"):
1351 # get qty (balance) that was set at submission
1352 last_balance = args.get("previous_qty_after_transaction")
1353 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1354 else:
1355 stock_reco_qty_shift = flt(args.actual_qty)
1356 else:
1357 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301358 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1359 "qty_after_transaction"
1360 )
marination40389772021-07-02 17:13:45 +05301361
1362 if last_balance is not None:
1363 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1364 else:
1365 stock_reco_qty_shift = args.qty_after_transaction
1366
1367 return stock_reco_qty_shift
1368
Ankush Menat494bd9e2022-03-28 18:52:46 +05301369
marination40389772021-07-02 17:13:45 +05301370def get_next_stock_reco(args):
1371 """Returns next nearest stock reconciliaton's details."""
1372
Ankush Menat494bd9e2022-03-28 18:52:46 +05301373 return frappe.db.sql(
1374 """
marination40389772021-07-02 17:13:45 +05301375 select
1376 name, posting_date, posting_time, creation, voucher_no
1377 from
marination8c441262021-07-02 17:46:05 +05301378 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301379 where
1380 item_code = %(item_code)s
1381 and warehouse = %(warehouse)s
1382 and voucher_type = 'Stock Reconciliation'
1383 and voucher_no != %(voucher_no)s
1384 and is_cancelled = 0
1385 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1386 or (
1387 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1388 and creation > %(creation)s
1389 )
1390 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301391 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301392 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301393 """,
1394 args,
1395 as_dict=1,
1396 )
1397
marination40389772021-07-02 17:13:45 +05301398
1399def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301400 return f"""
1401 and
1402 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1403 or (
1404 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1405 and creation < '{detail.creation}'
1406 )
1407 )"""
1408
Ankush Menat494bd9e2022-03-28 18:52:46 +05301409
Ankush Menate7109c12021-08-26 16:40:45 +05301410def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301411 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301412 return
1413 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1414 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301415
Ankush Menat5eba5752021-12-07 23:03:52 +05301416 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301417
1418 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301419 message = _(
1420 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1421 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301422 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301423 frappe.get_desk_link("Item", args.item_code),
1424 frappe.get_desk_link("Warehouse", args.warehouse),
1425 neg_sle[0]["posting_date"],
1426 neg_sle[0]["posting_time"],
1427 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1428 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301429
Ankush Menat494bd9e2022-03-28 18:52:46 +05301430 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301431
1432 if not args.batch_no:
1433 return
1434
1435 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301436 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301437 message = _(
1438 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1439 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301440 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301441 frappe.get_desk_link("Batch", args.batch_no),
1442 frappe.get_desk_link("Warehouse", args.warehouse),
1443 neg_batch_sle[0]["posting_date"],
1444 neg_batch_sle[0]["posting_time"],
1445 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1446 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301447 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301448
Nabin Haita77b8c92020-12-21 14:45:50 +05301449
Maricad6078aa2022-06-17 15:13:13 +05301450def is_negative_with_precision(neg_sle, is_batch=False):
1451 """
1452 Returns whether system precision rounded qty is insufficient.
1453 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1454 """
1455
1456 if not neg_sle:
1457 return False
1458
1459 field = "cumulative_total" if is_batch else "qty_after_transaction"
1460 precision = cint(frappe.db.get_default("float_precision")) or 2
1461 qty_deficit = flt(neg_sle[0][field], precision)
1462
1463 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1464
1465
Nabin Haita77b8c92020-12-21 14:45:50 +05301466def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301467 return frappe.db.sql(
1468 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301469 select
1470 qty_after_transaction, posting_date, posting_time,
1471 voucher_type, voucher_no
1472 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301473 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301474 item_code = %(item_code)s
1475 and warehouse = %(warehouse)s
1476 and voucher_no != %(voucher_no)s
1477 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1478 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301479 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301480 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301481 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301482 """,
1483 args,
1484 as_dict=1,
1485 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301486
Ankush Menat5eba5752021-12-07 23:03:52 +05301487
1488def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301489 return frappe.db.sql(
1490 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301491 with batch_ledger as (
1492 select
1493 posting_date, posting_time, voucher_type, voucher_no,
1494 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1495 from `tabStock Ledger Entry`
1496 where
1497 item_code = %(item_code)s
1498 and warehouse = %(warehouse)s
1499 and batch_no=%(batch_no)s
1500 and is_cancelled = 0
1501 order by posting_date, posting_time, creation
1502 )
1503 select * from batch_ledger
1504 where
1505 cumulative_total < 0.0
1506 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1507 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301508 """,
1509 args,
1510 as_dict=1,
1511 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301512
1513
1514def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1515 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1516 return True
1517 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1518 return True
1519 return False