blob: 9ca40c3675fb47efe808c9d7841e65d50e9d6072 [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):
Nabin Haite1fa7232022-07-20 15:19:09 +0530211 if not args:
212 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530213
214 items_to_be_repost = get_items_to_be_repost(
215 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
216 )
217 if items_to_be_repost:
218 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530219
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530220 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Ankush Menatecdb4932022-04-17 19:06:13 +0530221 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530222
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530223 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530224 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530225 validate_item_warehouse(args[i])
226
Ankush Menat494bd9e2022-03-28 18:52:46 +0530227 obj = update_entries_after(
228 {
229 "item_code": args[i].get("item_code"),
230 "warehouse": args[i].get("warehouse"),
231 "posting_date": args[i].get("posting_date"),
232 "posting_time": args[i].get("posting_time"),
233 "creation": args[i].get("creation"),
234 "distinct_item_warehouses": distinct_item_warehouses,
235 },
236 allow_negative_stock=allow_negative_stock,
237 via_landed_cost_voucher=via_landed_cost_voucher,
238 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530239 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530240
Ankush Menat494bd9e2022-03-28 18:52:46 +0530241 distinct_item_warehouses[
242 (args[i].get("item_code"), args[i].get("warehouse"))
243 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530244
Nabin Hait97bce3a2021-07-12 13:24:43 +0530245 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530246 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530247 if ("args_idx" not in data and not data.reposting_status) or (
248 data.sle_changed and data.reposting_status
249 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530250 data.args_idx = len(args)
251 args.append(data.sle)
252 elif data.sle_changed and not data.reposting_status:
253 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530254
Nabin Hait97bce3a2021-07-12 13:24:43 +0530255 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530256 i += 1
257
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530258 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530259 update_args_in_repost_item_valuation(
260 doc, i, args, distinct_item_warehouses, affected_transactions
261 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530262
Ankush Menat494bd9e2022-03-28 18:52:46 +0530263
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530264def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530265 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530266 if not args.get(field):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530267 validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530268 frappe.throw(_(validation_msg))
269
Ankush Menat494bd9e2022-03-28 18:52:46 +0530270
Ankush Menatecdb4932022-04-17 19:06:13 +0530271def update_args_in_repost_item_valuation(
272 doc, index, args, distinct_item_warehouses, affected_transactions
273):
Ankush Menatecdb4932022-04-17 19:06:13 +0530274 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530275 {
276 "items_to_be_repost": json.dumps(args, default=str),
277 "distinct_item_and_warehouse": json.dumps(
278 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
279 ),
280 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530281 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530282 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530283 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530284
Ankush Menatecdb4932022-04-17 19:06:13 +0530285 if not frappe.flags.in_test:
286 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530287
Ankush Menat494bd9e2022-03-28 18:52:46 +0530288 frappe.publish_realtime(
289 "item_reposting_progress",
290 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
291 )
292
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530293
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530294def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
295 items_to_be_repost = []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530297 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530298
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530299 if not items_to_be_repost and voucher_type and voucher_no:
300 items_to_be_repost = frappe.db.get_all(
301 "Stock Ledger Entry",
302 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
303 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
304 order_by="creation asc",
305 group_by="item_code, warehouse",
306 )
307
Nabin Haite1fa7232022-07-20 15:19:09 +0530308 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530309
Ankush Menat494bd9e2022-03-28 18:52:46 +0530310
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530311def get_distinct_item_warehouse(args=None, doc=None):
312 distinct_item_warehouses = {}
313 if doc and doc.distinct_item_and_warehouse:
314 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530315 distinct_item_warehouses = {
316 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
317 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530318 else:
319 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530320 distinct_item_warehouses.setdefault(
321 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
322 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530323
324 return distinct_item_warehouses
325
Ankush Menat494bd9e2022-03-28 18:52:46 +0530326
Ankush Menatecdb4932022-04-17 19:06:13 +0530327def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
328 if not doc.affected_transactions:
329 return set()
330
331 transactions = frappe.parse_json(doc.affected_transactions)
332 return {tuple(transaction) for transaction in transactions}
333
334
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530335def get_current_index(doc=None):
336 if doc and doc.current_index:
337 return doc.current_index
338
Ankush Menat494bd9e2022-03-28 18:52:46 +0530339
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530340class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530341 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530342 update valution rate and qty after transaction
343 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530344
Ankush Menat494bd9e2022-03-28 18:52:46 +0530345 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530346
Ankush Menat494bd9e2022-03-28 18:52:46 +0530347 args = {
348 "item_code": "ABC",
349 "warehouse": "XYZ",
350 "posting_date": "2012-12-12",
351 "posting_time": "12:00"
352 }
Nabin Hait902e8602013-01-08 18:29:24 +0530353 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530354
355 def __init__(
356 self,
357 args,
358 allow_zero_rate=False,
359 allow_negative_stock=None,
360 via_landed_cost_voucher=False,
361 verbose=1,
362 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530363 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530364 self.verbose = verbose
365 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530366 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530367 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530368 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
369 item_code=self.item_code
370 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530371
Nabin Haita77b8c92020-12-21 14:45:50 +0530372 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530373 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530374 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530375
Nabin Haita77b8c92020-12-21 14:45:50 +0530376 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530377 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530378 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530379
380 self.new_items_found = False
381 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530382 self.affected_transactions: Set[Tuple[str, str]] = set()
Nabin Haita77b8c92020-12-21 14:45:50 +0530383
384 self.data = frappe._dict()
385 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530386 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530387
Maricad6078aa2022-06-17 15:13:13 +0530388 def set_precision(self):
389 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
390 self.currency_precision = get_field_precision(
391 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530392 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530393
394 def initialize_previous_data(self, args):
395 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530396 Get previous sl entries for current item for each related warehouse
397 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530398
Ankush Menat494bd9e2022-03-28 18:52:46 +0530399 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530400
Ankush Menat494bd9e2022-03-28 18:52:46 +0530401 self.data = {
402 warehouse1: {
403 'previus_sle': {},
404 'qty_after_transaction': 10,
405 'valuation_rate': 100,
406 'stock_value': 1000,
407 'prev_stock_value': 1000,
408 'stock_queue': '[[10, 100]]',
409 'stock_value_difference': 1000
410 }
411 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530412
413 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530414 self.data.setdefault(args.warehouse, frappe._dict())
415 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530416 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530417 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530418
Ankush Menatc1d986a2021-08-31 19:43:42 +0530419 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
420 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
421
Ankush Menat494bd9e2022-03-28 18:52:46 +0530422 warehouse_dict.update(
423 {
424 "prev_stock_value": previous_sle.stock_value or 0.0,
425 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
426 "stock_value_difference": 0.0,
427 }
428 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530429
Nabin Haita77b8c92020-12-21 14:45:50 +0530430 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530431 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530432
Nabin Haita77b8c92020-12-21 14:45:50 +0530433 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530434 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530435 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530436 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530437 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530438 entries_to_fix = self.get_future_entries_to_fix()
439
440 i = 0
441 while i < len(entries_to_fix):
442 sle = entries_to_fix[i]
443 i += 1
444
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530445 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530446
Nabin Haita77b8c92020-12-21 14:45:50 +0530447 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530448 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530449
Nabin Hait186a0452021-02-18 14:14:21 +0530450 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530451
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530452 if self.exceptions:
453 self.raise_exceptions()
454
Nabin Hait186a0452021-02-18 14:14:21 +0530455 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530456 sl_entries = self.get_sle_against_current_voucher()
457 for sle in sl_entries:
458 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530459
Nabin Haita77b8c92020-12-21 14:45:50 +0530460 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530461 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530462
Ankush Menat494bd9e2022-03-28 18:52:46 +0530463 return frappe.db.sql(
464 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530465 select
466 *, timestamp(posting_date, posting_time) as "timestamp"
467 from
468 `tabStock Ledger Entry`
469 where
470 item_code = %(item_code)s
471 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530472 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530473 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
474
Nabin Haita77b8c92020-12-21 14:45:50 +0530475 order by
476 creation ASC
477 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530478 """,
479 self.args,
480 as_dict=1,
481 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530482
Nabin Haita77b8c92020-12-21 14:45:50 +0530483 def get_future_entries_to_fix(self):
484 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530485 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
486 {"item_code": self.item_code, "warehouse": self.args.warehouse}
487 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530488
Nabin Haita77b8c92020-12-21 14:45:50 +0530489 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530490
Nabin Haita77b8c92020-12-21 14:45:50 +0530491 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530492 dependant_sle = get_sle_by_voucher_detail_no(
493 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
494 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530495
Nabin Haita77b8c92020-12-21 14:45:50 +0530496 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530497 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530498 elif (
499 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
500 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530501 return entries_to_fix
502 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530503 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530504 return entries_to_fix
505 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
506 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530507 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530508 self.initialize_previous_data(dependant_sle)
509 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530510 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530511
512 def update_distinct_item_warehouses(self, dependant_sle):
513 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530514 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530515 if key not in self.distinct_item_warehouses:
516 self.distinct_item_warehouses[key] = val
517 self.new_items_found = True
518 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530519 existing_sle_posting_date = (
520 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
521 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530522 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
523 val.sle_changed = True
524 self.distinct_item_warehouses[key] = val
525 self.new_items_found = True
526
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530527 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530528 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
529
Nabin Haita77b8c92020-12-21 14:45:50 +0530530 # previous sle data for this warehouse
531 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530532 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530533
Anand Doshi0dc79f42015-04-06 12:59:34 +0530534 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 +0530535 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530536 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530537 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530538 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530539 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530540
Nabin Haita77b8c92020-12-21 14:45:50 +0530541 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530542 if not self.args.get("sle_id"):
543 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530544
Ankush Menat66bf21f2022-01-16 20:45:59 +0530545 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530546 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530547 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530548 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530549 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530550
Ankush Menat494bd9e2022-03-28 18:52:46 +0530551 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
552 self.wh_data.valuation_rate
553 )
554 elif sle.batch_no and frappe.db.get_value(
555 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
556 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530557 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530558 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530559 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530560 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530561 self.wh_data.valuation_rate = sle.valuation_rate
562 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530563 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
564 self.wh_data.valuation_rate
565 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530566 if self.valuation_method != "Moving Average":
567 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530568 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530569 if self.valuation_method == "Moving Average":
570 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530571 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530572 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
573 self.wh_data.valuation_rate
574 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530575 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530576 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530577
Rushabh Mehta54047782013-12-26 11:07:46 +0530578 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530579 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530580 if not self.wh_data.qty_after_transaction:
581 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530582 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
583 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530584
Nabin Hait902e8602013-01-08 18:29:24 +0530585 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530586 sle.qty_after_transaction = self.wh_data.qty_after_transaction
587 sle.valuation_rate = self.wh_data.valuation_rate
588 sle.stock_value = self.wh_data.stock_value
589 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530590 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530591 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530592 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530593
Ankush Menat701878f2022-03-01 18:08:29 +0530594 if not self.args.get("sle_id"):
595 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530596
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530597 def validate_negative_stock(self, sle):
598 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530599 validate negative stock for entries current datetime onwards
600 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530601 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530602 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Maricad6078aa2022-06-17 15:13:13 +0530603 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530604
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530605 if diff < 0 and abs(diff) > 0.0001:
606 # negative stock!
607 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530608 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530609 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530610 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530611 return True
612
Nabin Haita77b8c92020-12-21 14:45:50 +0530613 def get_dynamic_incoming_outgoing_rate(self, sle):
614 # Get updated incoming/outgoing rate from transaction
615 if sle.recalculate_rate:
616 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
617
618 if flt(sle.actual_qty) >= 0:
619 sle.incoming_rate = rate
620 else:
621 sle.outgoing_rate = rate
622
623 def get_incoming_outgoing_rate_from_transaction(self, sle):
624 rate = 0
625 # Material Transfer, Repack, Manufacturing
626 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530627 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530628 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
629 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530630 elif sle.voucher_type in (
631 "Purchase Receipt",
632 "Purchase Invoice",
633 "Delivery Note",
634 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530635 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530636 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530637 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530638 from erpnext.controllers.sales_and_purchase_return import (
639 get_rate_for_return, # don't move this import to top
640 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530641
642 rate = get_rate_for_return(
643 sle.voucher_type,
644 sle.voucher_no,
645 sle.item_code,
646 voucher_detail_no=sle.voucher_detail_no,
647 sle=sle,
648 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530649
650 elif (
651 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530652 and sle.voucher_detail_no
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530653 and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
654 ):
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530655 field = (
656 "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
657 )
658 doctype = (
659 "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
660 )
661 refernce_name = frappe.get_cached_value(
662 sle.voucher_type + " Item", sle.voucher_detail_no, field
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530663 )
664
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530665 if refernce_name:
666 rate = frappe.get_cached_value(
667 doctype,
668 refernce_name,
669 "incoming_rate",
670 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530671 else:
672 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530673 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530674 elif sle.voucher_type == "Subcontracting Receipt":
675 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530676 else:
677 rate_field = "incoming_rate"
678
679 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530680 item_code, incoming_rate = frappe.db.get_value(
681 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
682 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530683
684 if item_code == sle.item_code:
685 rate = incoming_rate
686 else:
687 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
688 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530689 elif sle == "Subcontracting Receipt":
690 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530691 else:
692 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530693
Ankush Menat494bd9e2022-03-28 18:52:46 +0530694 rate = frappe.db.get_value(
695 ref_doctype,
696 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
697 rate_field,
698 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530699
700 return rate
701
702 def update_outgoing_rate_on_transaction(self, sle):
703 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530704 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
705 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530706 """
707 if sle.actual_qty and sle.voucher_detail_no:
708 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
709
710 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
711 self.update_rate_on_stock_entry(sle, outgoing_rate)
712 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
713 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
714 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
715 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530716 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
717 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530718
719 def update_rate_on_stock_entry(self, sle, outgoing_rate):
720 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
721
Ankush Menat701878f2022-03-01 18:08:29 +0530722 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
723 if not sle.dependant_sle_voucher_detail_no:
724 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530725
726 def recalculate_amounts_in_stock_entry(self, voucher_no):
727 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530728 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
729 stock_entry.db_update()
730 for d in stock_entry.items:
731 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530732
Nabin Haita77b8c92020-12-21 14:45:50 +0530733 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
734 # Update item's incoming rate on transaction
735 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
736 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530737 frappe.db.set_value(
738 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
739 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530740 else:
741 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530742 frappe.db.set_value(
743 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530744 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530745 "incoming_rate",
746 outgoing_rate,
747 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530748
749 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
750 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530751 frappe.db.set_value(
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530752 sle.voucher_type + " Item",
753 sle.voucher_detail_no,
754 {
755 "base_net_rate": outgoing_rate,
756 "valuation_rate": outgoing_rate,
757 },
Ankush Menat494bd9e2022-03-28 18:52:46 +0530758 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530759 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530760 frappe.db.set_value(
761 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
762 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530763
764 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530765 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530766 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530767 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530768 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530769 d.db_update()
770
Sagar Sharma323bdf82022-05-17 15:14:07 +0530771 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
772 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
773 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
774 else:
775 frappe.db.set_value(
776 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
777 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530778
779 def get_serialized_values(self, sle):
780 incoming_rate = flt(sle.incoming_rate)
781 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530782 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530783
784 if incoming_rate < 0:
785 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530786 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530787
Nabin Hait2620bf42016-02-29 11:30:27 +0530788 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530789 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530790 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530791 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530792 # In case of delivery/stock issue, get average purchase rate
793 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530794 if not sle.is_cancelled:
795 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
796 stock_value_change = -1 * outgoing_value
797 else:
798 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530799
Nabin Haita77b8c92020-12-21 14:45:50 +0530800 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530801
Nabin Hait2620bf42016-02-29 11:30:27 +0530802 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530803 new_stock_value = (
804 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
805 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530806 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530807 # calculate new valuation rate only if stock value is positive
808 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530809 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530810
Nabin Haita77b8c92020-12-21 14:45:50 +0530811 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530812 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
813 sle.voucher_type, sle.voucher_detail_no
814 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530815 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530816 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530817
Nabin Hait328c4f92020-01-02 19:00:32 +0530818 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
819 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530820 all_serial_nos = frappe.get_all(
821 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
822 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530823
Ankush Menat494bd9e2022-03-28 18:52:46 +0530824 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 +0530825
826 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530827 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530828 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530829 incoming_rate = frappe.db.sql(
830 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530831 select incoming_rate
832 from `tabStock Ledger Entry`
833 where
834 company = %s
835 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530836 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530837 and (serial_no = %s
838 or serial_no like %s
839 or serial_no like %s
840 or serial_no like %s
841 )
842 order by posting_date desc
843 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530844 """,
845 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
846 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530847
848 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
849
850 return incoming_values
851
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530852 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530853 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530854 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530855 if new_stock_qty >= 0:
856 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530857 if flt(self.wh_data.qty_after_transaction) <= 0:
858 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530859 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530860 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
861 actual_qty * sle.incoming_rate
862 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530863
Nabin Haita77b8c92020-12-21 14:45:50 +0530864 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530865
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530866 elif sle.outgoing_rate:
867 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530868 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
869 actual_qty * sle.outgoing_rate
870 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530871
Nabin Haita77b8c92020-12-21 14:45:50 +0530872 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530873 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530874 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530875 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530876 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
877 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530878
Nabin Haita77b8c92020-12-21 14:45:50 +0530879 if not self.wh_data.valuation_rate and actual_qty > 0:
880 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530881
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530882 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800883 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530884 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530885 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
886 sle.voucher_type, sle.voucher_detail_no
887 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800888 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530889 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530890
Ankush Menatf089d392022-02-02 12:51:21 +0530891 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530892 incoming_rate = flt(sle.incoming_rate)
893 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530894 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530895
Ankush Menat494bd9e2022-03-28 18:52:46 +0530896 self.wh_data.qty_after_transaction = round_off_if_near_zero(
897 self.wh_data.qty_after_transaction + actual_qty
898 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530899
Ankush Menat97e18a12022-01-15 17:42:25 +0530900 if self.valuation_method == "LIFO":
901 stock_queue = LIFOValuation(self.wh_data.stock_queue)
902 else:
903 stock_queue = FIFOValuation(self.wh_data.stock_queue)
904
Ankush Menatb534fee2022-02-19 20:58:36 +0530905 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
906
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530907 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530908 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530909 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530910
Ankush Menat4b29fb62021-12-18 18:40:22 +0530911 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530912 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
913 sle.voucher_type, sle.voucher_detail_no
914 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530915 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530916 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530917 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530918 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530919
Ankush Menat494bd9e2022-03-28 18:52:46 +0530920 stock_queue.remove_stock(
921 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
922 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530923
Ankush Menatb534fee2022-02-19 20:58:36 +0530924 _qty, stock_value = stock_queue.get_total_stock_and_value()
925
926 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530927
Ankush Menat97e18a12022-01-15 17:42:25 +0530928 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530929 self.wh_data.stock_value = round_off_if_near_zero(
930 self.wh_data.stock_value + stock_value_difference
931 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530932
Nabin Haita77b8c92020-12-21 14:45:50 +0530933 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530934 self.wh_data.stock_queue.append(
935 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
936 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530937
Ankush Menatb534fee2022-02-19 20:58:36 +0530938 if self.wh_data.qty_after_transaction:
939 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
940
Ankush Menatce0514c2022-02-15 11:41:41 +0530941 def update_batched_values(self, sle):
942 incoming_rate = flt(sle.incoming_rate)
943 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530944
Ankush Menat494bd9e2022-03-28 18:52:46 +0530945 self.wh_data.qty_after_transaction = round_off_if_near_zero(
946 self.wh_data.qty_after_transaction + actual_qty
947 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530948
949 if actual_qty > 0:
950 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530951 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530952 outgoing_rate = get_batch_incoming_rate(
953 item_code=sle.item_code,
954 warehouse=sle.warehouse,
955 batch_no=sle.batch_no,
956 posting_date=sle.posting_date,
957 posting_time=sle.posting_time,
958 creation=sle.creation,
959 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530960 if outgoing_rate is None:
961 # This can *only* happen if qty available for the batch is zero.
962 # in such case fall back various other rates.
963 # future entries will correct the overall accounting as each
964 # batch individually uses moving average rates.
965 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530966 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530967
Ankush Menat494bd9e2022-03-28 18:52:46 +0530968 self.wh_data.stock_value = round_off_if_near_zero(
969 self.wh_data.stock_value + stock_value_difference
970 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530971 if self.wh_data.qty_after_transaction:
972 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530973
Javier Wong9b11d9b2017-04-14 18:24:04 +0800974 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530975 ref_item_dt = ""
976
977 if voucher_type == "Stock Entry":
978 ref_item_dt = voucher_type + " Detail"
979 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
980 ref_item_dt = voucher_type + " Item"
981
982 if ref_item_dt:
983 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
984 else:
985 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530986
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530987 def get_fallback_rate(self, sle) -> float:
988 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530989 This should only get used for negative stock."""
990 return get_valuation_rate(
991 sle.item_code,
992 sle.warehouse,
993 sle.voucher_type,
994 sle.voucher_no,
995 self.allow_zero_rate,
996 currency=erpnext.get_company_currency(sle.company),
997 company=sle.company,
998 batch_no=sle.batch_no,
999 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301000
Nabin Haita77b8c92020-12-21 14:45:50 +05301001 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301002 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301003 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1004 sle = sle[0] if sle else frappe._dict()
1005 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301006
Nabin Haita77b8c92020-12-21 14:45:50 +05301007 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301008 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301009 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301010
1011 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301012 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301013 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301014 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301015
Ankush Menat494bd9e2022-03-28 18:52:46 +05301016 if (
1017 exceptions[0]["voucher_type"],
1018 exceptions[0]["voucher_no"],
1019 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301020
Nabin Haita77b8c92020-12-21 14:45:50 +05301021 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301022 abs(deficiency),
1023 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1024 frappe.get_desk_link("Warehouse", warehouse),
1025 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301026 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301027 msg = _(
1028 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1029 ).format(
1030 abs(deficiency),
1031 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1032 frappe.get_desk_link("Warehouse", warehouse),
1033 exceptions[0]["posting_date"],
1034 exceptions[0]["posting_time"],
1035 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1036 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301037
Nabin Haita77b8c92020-12-21 14:45:50 +05301038 if msg:
1039 msg_list.append(msg)
1040
1041 if msg_list:
1042 message = "\n\n".join(msg_list)
1043 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301044 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301045 else:
1046 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301047
Nabin Haita77b8c92020-12-21 14:45:50 +05301048 def update_bin(self):
1049 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301050 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301051 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301052
Ankush Menat494bd9e2022-03-28 18:52:46 +05301053 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301054 if data.valuation_rate is not None:
1055 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301056 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301057
marination8418c4b2021-06-22 21:35:25 +05301058
1059def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1060 """get stock ledger entries filtered by specific posting datetime conditions"""
1061
Ankush Menat494bd9e2022-03-28 18:52:46 +05301062 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301063 if not args.get("posting_date"):
1064 args["posting_date"] = "1900-01-01"
1065 if not args.get("posting_time"):
1066 args["posting_time"] = "00:00"
1067
1068 voucher_condition = ""
1069 if exclude_current_voucher:
1070 voucher_no = args.get("voucher_no")
1071 voucher_condition = f"and voucher_no != '{voucher_no}'"
1072
Ankush Menat494bd9e2022-03-28 18:52:46 +05301073 sle = frappe.db.sql(
1074 """
marination8418c4b2021-06-22 21:35:25 +05301075 select *, timestamp(posting_date, posting_time) as "timestamp"
1076 from `tabStock Ledger Entry`
1077 where item_code = %(item_code)s
1078 and warehouse = %(warehouse)s
1079 and is_cancelled = 0
1080 {voucher_condition}
1081 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1082 order by timestamp(posting_date, posting_time) desc, creation desc
1083 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301084 for update""".format(
1085 voucher_condition=voucher_condition
1086 ),
1087 args,
1088 as_dict=1,
1089 )
marination8418c4b2021-06-22 21:35:25 +05301090
1091 return sle[0] if sle else frappe._dict()
1092
Ankush Menat494bd9e2022-03-28 18:52:46 +05301093
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301094def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301095 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301096 get the last sle on or before the current time-bucket,
1097 to get actual qty before transaction, this function
1098 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301099
Ankush Menat494bd9e2022-03-28 18:52:46 +05301100 args = {
1101 "item_code": "ABC",
1102 "warehouse": "XYZ",
1103 "posting_date": "2012-12-12",
1104 "posting_time": "12:00",
1105 "sle": "name of reference Stock Ledger Entry"
1106 }
Anand Doshi1b531862013-01-10 19:29:51 +05301107 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301108 args["name"] = args.get("sle", None) or ""
1109 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301110 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301111
Ankush Menat494bd9e2022-03-28 18:52:46 +05301112
1113def get_stock_ledger_entries(
1114 previous_sle,
1115 operator=None,
1116 order="desc",
1117 limit=None,
1118 for_update=False,
1119 debug=False,
1120 check_serial_no=True,
1121):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301122 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301123 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1124 operator
1125 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301126 if previous_sle.get("warehouse"):
1127 conditions += " and warehouse = %(warehouse)s"
1128 elif previous_sle.get("warehouse_condition"):
1129 conditions += " and " + previous_sle.get("warehouse_condition")
1130
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301131 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301132 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1133 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301134 conditions += (
1135 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301136 (
1137 serial_no = {0}
1138 or serial_no like {1}
1139 or serial_no like {2}
1140 or serial_no like {3}
1141 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301142 """
1143 ).format(
1144 frappe.db.escape(serial_no),
1145 frappe.db.escape("{}\n%".format(serial_no)),
1146 frappe.db.escape("%\n{}".format(serial_no)),
1147 frappe.db.escape("%\n{}\n%".format(serial_no)),
1148 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301149
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301150 if not previous_sle.get("posting_date"):
1151 previous_sle["posting_date"] = "1900-01-01"
1152 if not previous_sle.get("posting_time"):
1153 previous_sle["posting_time"] = "00:00"
1154
1155 if operator in (">", "<=") and previous_sle.get("name"):
1156 conditions += " and name!=%(name)s"
1157
Ankush Menat494bd9e2022-03-28 18:52:46 +05301158 return frappe.db.sql(
1159 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301160 select *, timestamp(posting_date, posting_time) as "timestamp"
1161 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301162 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301163 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301164 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301165 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301166 %(limit)s %(for_update)s"""
1167 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301168 "conditions": conditions,
1169 "limit": limit or "",
1170 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301171 "order": order,
1172 },
1173 previous_sle,
1174 as_dict=1,
1175 debug=debug,
1176 )
1177
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301178
Nabin Haita77b8c92020-12-21 14:45:50 +05301179def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301180 return frappe.db.get_value(
1181 "Stock Ledger Entry",
1182 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1183 [
1184 "item_code",
1185 "warehouse",
1186 "posting_date",
1187 "posting_time",
1188 "timestamp(posting_date, posting_time) as timestamp",
1189 ],
1190 as_dict=1,
1191 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301192
Ankush Menatce0514c2022-02-15 11:41:41 +05301193
Ankush Menat494bd9e2022-03-28 18:52:46 +05301194def get_batch_incoming_rate(
1195 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1196):
1197
Ankush Menat102fff22022-02-19 15:51:04 +05301198 sle = frappe.qb.DocType("Stock Ledger Entry")
1199
Ankush Menate1c16872022-04-21 20:01:48 +05301200 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301201 posting_date, posting_time
1202 )
Ankush Menat102fff22022-02-19 15:51:04 +05301203 if creation:
1204 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301205 CombineDatetime(sle.posting_date, sle.posting_time)
1206 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301207 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301208
1209 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301210 frappe.qb.from_(sle)
1211 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1212 .where(
1213 (sle.item_code == item_code)
1214 & (sle.warehouse == warehouse)
1215 & (sle.batch_no == batch_no)
1216 & (sle.is_cancelled == 0)
1217 )
1218 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301219 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301220
1221 if batch_details and batch_details[0].batch_qty:
1222 return batch_details[0].batch_value / batch_details[0].batch_qty
1223
1224
Ankush Menat494bd9e2022-03-28 18:52:46 +05301225def get_valuation_rate(
1226 item_code,
1227 warehouse,
1228 voucher_type,
1229 voucher_no,
1230 allow_zero_rate=False,
1231 currency=None,
1232 company=None,
1233 raise_error_if_no_rate=True,
1234 batch_no=None,
1235):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301236
Ankush Menatf7ffe042021-11-01 13:21:14 +05301237 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301238 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301239
Ankush Menat342d09a2022-02-19 14:28:51 +05301240 last_valuation_rate = None
1241
1242 # Get moving average rate of a specific batch number
1243 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301244 last_valuation_rate = frappe.db.sql(
1245 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301246 select sum(stock_value_difference) / sum(actual_qty)
1247 from `tabStock Ledger Entry`
1248 where
1249 item_code = %s
1250 AND warehouse = %s
1251 AND batch_no = %s
1252 AND is_cancelled = 0
1253 AND NOT (voucher_no = %s AND voucher_type = %s)
1254 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301255 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1256 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301257
Ankush Menatf7ffe042021-11-01 13:21:14 +05301258 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301259 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301260 last_valuation_rate = frappe.db.sql(
1261 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301262 from `tabStock Ledger Entry` force index (item_warehouse)
1263 where
1264 item_code = %s
1265 AND warehouse = %s
1266 AND valuation_rate >= 0
1267 AND is_cancelled = 0
1268 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301269 order by posting_date desc, posting_time desc, name desc limit 1""",
1270 (item_code, warehouse, voucher_no, voucher_type),
1271 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301272
1273 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301274 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301275 last_valuation_rate = frappe.db.sql(
1276 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301277 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301278 where
1279 item_code = %s
1280 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301281 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301282 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301283 order by posting_date desc, posting_time desc, name desc limit 1""",
1284 (item_code, voucher_no, voucher_type),
1285 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301286
Nabin Haita645f362018-03-01 10:31:24 +05301287 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301288 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301289
1290 # If negative stock allowed, and item delivered without any incoming entry,
1291 # system does not found any SLE, then take valuation rate from Item
1292 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301293
1294 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301295 # try Item Standard rate
1296 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301297
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301298 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301299 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301300 valuation_rate = frappe.db.get_value(
1301 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1302 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301303
Ankush Menat494bd9e2022-03-28 18:52:46 +05301304 if (
1305 not allow_zero_rate
1306 and not valuation_rate
1307 and raise_error_if_no_rate
1308 and cint(erpnext.is_perpetual_inventory_enabled(company))
1309 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301310 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301311
Ankush Menat494bd9e2022-03-28 18:52:46 +05301312 message = _(
1313 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1314 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301315 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301316 solutions = (
1317 "<li>"
1318 + _(
1319 "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."
1320 ).format(voucher_type)
1321 + "</li>"
1322 )
1323 solutions += (
1324 "<li>"
1325 + _("If not, you can Cancel / Submit this entry")
1326 + " {0} ".format(frappe.bold("after"))
1327 + _("performing either one below:")
1328 + "</li>"
1329 )
Marica97715f22020-05-11 20:45:37 +05301330 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1331 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1332 msg = message + solutions + sub_solutions + "</li>"
1333
1334 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301335
1336 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301337
Ankush Menat494bd9e2022-03-28 18:52:46 +05301338
Ankush Menate7109c12021-08-26 16:40:45 +05301339def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301340 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301341 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301342 qty_shift = args.actual_qty
1343
Ankush Menat7c839c42022-05-06 12:09:08 +05301344 args["time_format"] = "%H:%i:%s"
1345
marination8418c4b2021-06-22 21:35:25 +05301346 # find difference/shift in qty caused by stock reconciliation
1347 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301348 qty_shift = get_stock_reco_qty_shift(args)
1349
1350 # find the next nearest stock reco so that we only recalculate SLEs till that point
1351 next_stock_reco_detail = get_next_stock_reco(args)
1352 if next_stock_reco_detail:
1353 detail = next_stock_reco_detail[0]
1354 # add condition to update SLEs before this date & time
1355 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301356
Ankush Menat494bd9e2022-03-28 18:52:46 +05301357 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301358 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301359 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301360 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301361 where
1362 item_code = %(item_code)s
1363 and warehouse = %(warehouse)s
1364 and voucher_no != %(voucher_no)s
1365 and is_cancelled = 0
Ankush Menat7c839c42022-05-06 12:09:08 +05301366 and timestamp(posting_date, time_format(posting_time, %(time_format)s))
1367 > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
marination40389772021-07-02 17:13:45 +05301368 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301369 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301370 args,
1371 )
Nabin Hait186a0452021-02-18 14:14:21 +05301372
1373 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1374
Ankush Menat494bd9e2022-03-28 18:52:46 +05301375
marination40389772021-07-02 17:13:45 +05301376def get_stock_reco_qty_shift(args):
1377 stock_reco_qty_shift = 0
1378 if args.get("is_cancelled"):
1379 if args.get("previous_qty_after_transaction"):
1380 # get qty (balance) that was set at submission
1381 last_balance = args.get("previous_qty_after_transaction")
1382 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1383 else:
1384 stock_reco_qty_shift = flt(args.actual_qty)
1385 else:
1386 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301387 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1388 "qty_after_transaction"
1389 )
marination40389772021-07-02 17:13:45 +05301390
1391 if last_balance is not None:
1392 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1393 else:
1394 stock_reco_qty_shift = args.qty_after_transaction
1395
1396 return stock_reco_qty_shift
1397
Ankush Menat494bd9e2022-03-28 18:52:46 +05301398
marination40389772021-07-02 17:13:45 +05301399def get_next_stock_reco(args):
1400 """Returns next nearest stock reconciliaton's details."""
1401
Ankush Menat494bd9e2022-03-28 18:52:46 +05301402 return frappe.db.sql(
1403 """
marination40389772021-07-02 17:13:45 +05301404 select
1405 name, posting_date, posting_time, creation, voucher_no
1406 from
marination8c441262021-07-02 17:46:05 +05301407 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301408 where
1409 item_code = %(item_code)s
1410 and warehouse = %(warehouse)s
1411 and voucher_type = 'Stock Reconciliation'
1412 and voucher_no != %(voucher_no)s
1413 and is_cancelled = 0
1414 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1415 or (
1416 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1417 and creation > %(creation)s
1418 )
1419 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301420 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301421 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301422 """,
1423 args,
1424 as_dict=1,
1425 )
1426
marination40389772021-07-02 17:13:45 +05301427
1428def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301429 return f"""
1430 and
1431 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1432 or (
1433 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1434 and creation < '{detail.creation}'
1435 )
1436 )"""
1437
Ankush Menat494bd9e2022-03-28 18:52:46 +05301438
Ankush Menate7109c12021-08-26 16:40:45 +05301439def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301440 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301441 return
1442 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1443 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301444
Ankush Menat5eba5752021-12-07 23:03:52 +05301445 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301446
1447 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301448 message = _(
1449 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1450 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301451 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301452 frappe.get_desk_link("Item", args.item_code),
1453 frappe.get_desk_link("Warehouse", args.warehouse),
1454 neg_sle[0]["posting_date"],
1455 neg_sle[0]["posting_time"],
1456 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1457 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301458
Ankush Menat494bd9e2022-03-28 18:52:46 +05301459 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301460
1461 if not args.batch_no:
1462 return
1463
1464 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301465 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301466 message = _(
1467 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1468 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301469 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301470 frappe.get_desk_link("Batch", args.batch_no),
1471 frappe.get_desk_link("Warehouse", args.warehouse),
1472 neg_batch_sle[0]["posting_date"],
1473 neg_batch_sle[0]["posting_time"],
1474 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1475 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301476 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301477
Nabin Haita77b8c92020-12-21 14:45:50 +05301478
Maricad6078aa2022-06-17 15:13:13 +05301479def is_negative_with_precision(neg_sle, is_batch=False):
1480 """
1481 Returns whether system precision rounded qty is insufficient.
1482 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1483 """
1484
1485 if not neg_sle:
1486 return False
1487
1488 field = "cumulative_total" if is_batch else "qty_after_transaction"
1489 precision = cint(frappe.db.get_default("float_precision")) or 2
1490 qty_deficit = flt(neg_sle[0][field], precision)
1491
1492 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1493
1494
Nabin Haita77b8c92020-12-21 14:45:50 +05301495def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301496 return frappe.db.sql(
1497 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301498 select
1499 qty_after_transaction, posting_date, posting_time,
1500 voucher_type, voucher_no
1501 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301502 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301503 item_code = %(item_code)s
1504 and warehouse = %(warehouse)s
1505 and voucher_no != %(voucher_no)s
1506 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1507 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301508 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301509 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301510 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301511 """,
1512 args,
1513 as_dict=1,
1514 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301515
Ankush Menat5eba5752021-12-07 23:03:52 +05301516
1517def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301518 return frappe.db.sql(
1519 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301520 with batch_ledger as (
1521 select
1522 posting_date, posting_time, voucher_type, voucher_no,
1523 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1524 from `tabStock Ledger Entry`
1525 where
1526 item_code = %(item_code)s
1527 and warehouse = %(warehouse)s
1528 and batch_no=%(batch_no)s
1529 and is_cancelled = 0
1530 order by posting_date, posting_time, creation
1531 )
1532 select * from batch_ledger
1533 where
1534 cumulative_total < 0.0
1535 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1536 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301537 """,
1538 args,
1539 as_dict=1,
1540 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301541
1542
1543def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1544 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1545 return True
1546 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1547 return True
1548 return False