blob: fd1aece7b153375ac652cb75ea3a2a5bbcb8e8a8 [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 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530649 else:
650 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530651 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530652 elif sle.voucher_type == "Subcontracting Receipt":
653 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530654 else:
655 rate_field = "incoming_rate"
656
657 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530658 item_code, incoming_rate = frappe.db.get_value(
659 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
660 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530661
662 if item_code == sle.item_code:
663 rate = incoming_rate
664 else:
665 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
666 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530667 elif sle == "Subcontracting Receipt":
668 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530669 else:
670 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530671
Ankush Menat494bd9e2022-03-28 18:52:46 +0530672 rate = frappe.db.get_value(
673 ref_doctype,
674 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
675 rate_field,
676 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530677
678 return rate
679
680 def update_outgoing_rate_on_transaction(self, sle):
681 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530682 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
683 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530684 """
685 if sle.actual_qty and sle.voucher_detail_no:
686 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
687
688 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
689 self.update_rate_on_stock_entry(sle, outgoing_rate)
690 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
691 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
692 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
693 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530694 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
695 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530696
697 def update_rate_on_stock_entry(self, sle, outgoing_rate):
698 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
699
Ankush Menat701878f2022-03-01 18:08:29 +0530700 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
701 if not sle.dependant_sle_voucher_detail_no:
702 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530703
704 def recalculate_amounts_in_stock_entry(self, voucher_no):
705 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530706 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
707 stock_entry.db_update()
708 for d in stock_entry.items:
709 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530710
Nabin Haita77b8c92020-12-21 14:45:50 +0530711 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
712 # Update item's incoming rate on transaction
713 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
714 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530715 frappe.db.set_value(
716 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
717 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530718 else:
719 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530720 frappe.db.set_value(
721 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530722 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530723 "incoming_rate",
724 outgoing_rate,
725 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530726
727 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
728 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530729 frappe.db.set_value(
730 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
731 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530732 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530733 frappe.db.set_value(
734 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
735 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530736
737 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530738 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530739 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530740 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530741 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530742 d.db_update()
743
Sagar Sharma323bdf82022-05-17 15:14:07 +0530744 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
745 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
746 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
747 else:
748 frappe.db.set_value(
749 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
750 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530751
752 def get_serialized_values(self, sle):
753 incoming_rate = flt(sle.incoming_rate)
754 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530755 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530756
757 if incoming_rate < 0:
758 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530759 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530760
Nabin Hait2620bf42016-02-29 11:30:27 +0530761 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530762 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530763 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530764 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530765 # In case of delivery/stock issue, get average purchase rate
766 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530767 if not sle.is_cancelled:
768 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
769 stock_value_change = -1 * outgoing_value
770 else:
771 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530772
Nabin Haita77b8c92020-12-21 14:45:50 +0530773 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530774
Nabin Hait2620bf42016-02-29 11:30:27 +0530775 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530776 new_stock_value = (
777 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
778 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530779 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530780 # calculate new valuation rate only if stock value is positive
781 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530782 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530783
Nabin Haita77b8c92020-12-21 14:45:50 +0530784 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530785 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
786 sle.voucher_type, sle.voucher_detail_no
787 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530788 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530789 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530790
Nabin Hait328c4f92020-01-02 19:00:32 +0530791 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
792 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530793 all_serial_nos = frappe.get_all(
794 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
795 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530796
Ankush Menat494bd9e2022-03-28 18:52:46 +0530797 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 +0530798
799 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530800 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530801 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530802 incoming_rate = frappe.db.sql(
803 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530804 select incoming_rate
805 from `tabStock Ledger Entry`
806 where
807 company = %s
808 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530809 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530810 and (serial_no = %s
811 or serial_no like %s
812 or serial_no like %s
813 or serial_no like %s
814 )
815 order by posting_date desc
816 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530817 """,
818 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
819 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530820
821 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
822
823 return incoming_values
824
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530825 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530826 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530827 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530828 if new_stock_qty >= 0:
829 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530830 if flt(self.wh_data.qty_after_transaction) <= 0:
831 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530832 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530833 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
834 actual_qty * sle.incoming_rate
835 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530836
Nabin Haita77b8c92020-12-21 14:45:50 +0530837 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530838
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530839 elif sle.outgoing_rate:
840 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530841 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
842 actual_qty * sle.outgoing_rate
843 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530844
Nabin Haita77b8c92020-12-21 14:45:50 +0530845 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530846 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530847 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530848 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530849 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
850 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530851
Nabin Haita77b8c92020-12-21 14:45:50 +0530852 if not self.wh_data.valuation_rate and actual_qty > 0:
853 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530854
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530855 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800856 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530857 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530858 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
859 sle.voucher_type, sle.voucher_detail_no
860 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800861 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530862 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530863
Ankush Menatf089d392022-02-02 12:51:21 +0530864 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530865 incoming_rate = flt(sle.incoming_rate)
866 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530867 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530868
Ankush Menat494bd9e2022-03-28 18:52:46 +0530869 self.wh_data.qty_after_transaction = round_off_if_near_zero(
870 self.wh_data.qty_after_transaction + actual_qty
871 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530872
Ankush Menat97e18a12022-01-15 17:42:25 +0530873 if self.valuation_method == "LIFO":
874 stock_queue = LIFOValuation(self.wh_data.stock_queue)
875 else:
876 stock_queue = FIFOValuation(self.wh_data.stock_queue)
877
Ankush Menatb534fee2022-02-19 20:58:36 +0530878 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
879
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530880 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530881 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530882 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530883
Ankush Menat4b29fb62021-12-18 18:40:22 +0530884 def rate_generator() -> float:
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 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530888 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530889 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530890 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530891 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530892
Ankush Menat494bd9e2022-03-28 18:52:46 +0530893 stock_queue.remove_stock(
894 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
895 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530896
Ankush Menatb534fee2022-02-19 20:58:36 +0530897 _qty, stock_value = stock_queue.get_total_stock_and_value()
898
899 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530900
Ankush Menat97e18a12022-01-15 17:42:25 +0530901 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530902 self.wh_data.stock_value = round_off_if_near_zero(
903 self.wh_data.stock_value + stock_value_difference
904 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530905
Nabin Haita77b8c92020-12-21 14:45:50 +0530906 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530907 self.wh_data.stock_queue.append(
908 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
909 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530910
Ankush Menatb534fee2022-02-19 20:58:36 +0530911 if self.wh_data.qty_after_transaction:
912 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
913
Ankush Menatce0514c2022-02-15 11:41:41 +0530914 def update_batched_values(self, sle):
915 incoming_rate = flt(sle.incoming_rate)
916 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530917
Ankush Menat494bd9e2022-03-28 18:52:46 +0530918 self.wh_data.qty_after_transaction = round_off_if_near_zero(
919 self.wh_data.qty_after_transaction + actual_qty
920 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530921
922 if actual_qty > 0:
923 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530924 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530925 outgoing_rate = get_batch_incoming_rate(
926 item_code=sle.item_code,
927 warehouse=sle.warehouse,
928 batch_no=sle.batch_no,
929 posting_date=sle.posting_date,
930 posting_time=sle.posting_time,
931 creation=sle.creation,
932 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530933 if outgoing_rate is None:
934 # This can *only* happen if qty available for the batch is zero.
935 # in such case fall back various other rates.
936 # future entries will correct the overall accounting as each
937 # batch individually uses moving average rates.
938 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530939 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530940
Ankush Menat494bd9e2022-03-28 18:52:46 +0530941 self.wh_data.stock_value = round_off_if_near_zero(
942 self.wh_data.stock_value + stock_value_difference
943 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530944 if self.wh_data.qty_after_transaction:
945 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530946
Javier Wong9b11d9b2017-04-14 18:24:04 +0800947 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530948 ref_item_dt = ""
949
950 if voucher_type == "Stock Entry":
951 ref_item_dt = voucher_type + " Detail"
952 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
953 ref_item_dt = voucher_type + " Item"
954
955 if ref_item_dt:
956 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
957 else:
958 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530959
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530960 def get_fallback_rate(self, sle) -> float:
961 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530962 This should only get used for negative stock."""
963 return get_valuation_rate(
964 sle.item_code,
965 sle.warehouse,
966 sle.voucher_type,
967 sle.voucher_no,
968 self.allow_zero_rate,
969 currency=erpnext.get_company_currency(sle.company),
970 company=sle.company,
971 batch_no=sle.batch_no,
972 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530973
Nabin Haita77b8c92020-12-21 14:45:50 +0530974 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530975 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530976 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
977 sle = sle[0] if sle else frappe._dict()
978 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530979
Nabin Haita77b8c92020-12-21 14:45:50 +0530980 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530981 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530982 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530983
984 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530985 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530986 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530987 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530988
Ankush Menat494bd9e2022-03-28 18:52:46 +0530989 if (
990 exceptions[0]["voucher_type"],
991 exceptions[0]["voucher_no"],
992 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +0530993
Nabin Haita77b8c92020-12-21 14:45:50 +0530994 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530995 abs(deficiency),
996 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
997 frappe.get_desk_link("Warehouse", warehouse),
998 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530999 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301000 msg = _(
1001 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1002 ).format(
1003 abs(deficiency),
1004 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1005 frappe.get_desk_link("Warehouse", warehouse),
1006 exceptions[0]["posting_date"],
1007 exceptions[0]["posting_time"],
1008 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1009 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301010
Nabin Haita77b8c92020-12-21 14:45:50 +05301011 if msg:
1012 msg_list.append(msg)
1013
1014 if msg_list:
1015 message = "\n\n".join(msg_list)
1016 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301017 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301018 else:
1019 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301020
Nabin Haita77b8c92020-12-21 14:45:50 +05301021 def update_bin(self):
1022 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301023 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301024 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301025
Ankush Menat494bd9e2022-03-28 18:52:46 +05301026 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301027 if data.valuation_rate is not None:
1028 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301029 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301030
marination8418c4b2021-06-22 21:35:25 +05301031
1032def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1033 """get stock ledger entries filtered by specific posting datetime conditions"""
1034
Ankush Menat494bd9e2022-03-28 18:52:46 +05301035 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301036 if not args.get("posting_date"):
1037 args["posting_date"] = "1900-01-01"
1038 if not args.get("posting_time"):
1039 args["posting_time"] = "00:00"
1040
1041 voucher_condition = ""
1042 if exclude_current_voucher:
1043 voucher_no = args.get("voucher_no")
1044 voucher_condition = f"and voucher_no != '{voucher_no}'"
1045
Ankush Menat494bd9e2022-03-28 18:52:46 +05301046 sle = frappe.db.sql(
1047 """
marination8418c4b2021-06-22 21:35:25 +05301048 select *, timestamp(posting_date, posting_time) as "timestamp"
1049 from `tabStock Ledger Entry`
1050 where item_code = %(item_code)s
1051 and warehouse = %(warehouse)s
1052 and is_cancelled = 0
1053 {voucher_condition}
1054 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1055 order by timestamp(posting_date, posting_time) desc, creation desc
1056 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301057 for update""".format(
1058 voucher_condition=voucher_condition
1059 ),
1060 args,
1061 as_dict=1,
1062 )
marination8418c4b2021-06-22 21:35:25 +05301063
1064 return sle[0] if sle else frappe._dict()
1065
Ankush Menat494bd9e2022-03-28 18:52:46 +05301066
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301067def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301068 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301069 get the last sle on or before the current time-bucket,
1070 to get actual qty before transaction, this function
1071 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301072
Ankush Menat494bd9e2022-03-28 18:52:46 +05301073 args = {
1074 "item_code": "ABC",
1075 "warehouse": "XYZ",
1076 "posting_date": "2012-12-12",
1077 "posting_time": "12:00",
1078 "sle": "name of reference Stock Ledger Entry"
1079 }
Anand Doshi1b531862013-01-10 19:29:51 +05301080 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301081 args["name"] = args.get("sle", None) or ""
1082 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301083 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301084
Ankush Menat494bd9e2022-03-28 18:52:46 +05301085
1086def get_stock_ledger_entries(
1087 previous_sle,
1088 operator=None,
1089 order="desc",
1090 limit=None,
1091 for_update=False,
1092 debug=False,
1093 check_serial_no=True,
1094):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301095 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301096 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1097 operator
1098 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301099 if previous_sle.get("warehouse"):
1100 conditions += " and warehouse = %(warehouse)s"
1101 elif previous_sle.get("warehouse_condition"):
1102 conditions += " and " + previous_sle.get("warehouse_condition")
1103
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301104 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301105 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1106 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301107 conditions += (
1108 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301109 (
1110 serial_no = {0}
1111 or serial_no like {1}
1112 or serial_no like {2}
1113 or serial_no like {3}
1114 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301115 """
1116 ).format(
1117 frappe.db.escape(serial_no),
1118 frappe.db.escape("{}\n%".format(serial_no)),
1119 frappe.db.escape("%\n{}".format(serial_no)),
1120 frappe.db.escape("%\n{}\n%".format(serial_no)),
1121 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301122
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301123 if not previous_sle.get("posting_date"):
1124 previous_sle["posting_date"] = "1900-01-01"
1125 if not previous_sle.get("posting_time"):
1126 previous_sle["posting_time"] = "00:00"
1127
1128 if operator in (">", "<=") and previous_sle.get("name"):
1129 conditions += " and name!=%(name)s"
1130
Ankush Menat494bd9e2022-03-28 18:52:46 +05301131 return frappe.db.sql(
1132 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301133 select *, timestamp(posting_date, posting_time) as "timestamp"
1134 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301135 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301136 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301137 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301138 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301139 %(limit)s %(for_update)s"""
1140 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301141 "conditions": conditions,
1142 "limit": limit or "",
1143 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301144 "order": order,
1145 },
1146 previous_sle,
1147 as_dict=1,
1148 debug=debug,
1149 )
1150
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301151
Nabin Haita77b8c92020-12-21 14:45:50 +05301152def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301153 return frappe.db.get_value(
1154 "Stock Ledger Entry",
1155 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1156 [
1157 "item_code",
1158 "warehouse",
1159 "posting_date",
1160 "posting_time",
1161 "timestamp(posting_date, posting_time) as timestamp",
1162 ],
1163 as_dict=1,
1164 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301165
Ankush Menatce0514c2022-02-15 11:41:41 +05301166
Ankush Menat494bd9e2022-03-28 18:52:46 +05301167def get_batch_incoming_rate(
1168 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1169):
1170
Ankush Menat102fff22022-02-19 15:51:04 +05301171 sle = frappe.qb.DocType("Stock Ledger Entry")
1172
Ankush Menate1c16872022-04-21 20:01:48 +05301173 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301174 posting_date, posting_time
1175 )
Ankush Menat102fff22022-02-19 15:51:04 +05301176 if creation:
1177 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301178 CombineDatetime(sle.posting_date, sle.posting_time)
1179 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301180 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301181
1182 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301183 frappe.qb.from_(sle)
1184 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1185 .where(
1186 (sle.item_code == item_code)
1187 & (sle.warehouse == warehouse)
1188 & (sle.batch_no == batch_no)
1189 & (sle.is_cancelled == 0)
1190 )
1191 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301192 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301193
1194 if batch_details and batch_details[0].batch_qty:
1195 return batch_details[0].batch_value / batch_details[0].batch_qty
1196
1197
Ankush Menat494bd9e2022-03-28 18:52:46 +05301198def get_valuation_rate(
1199 item_code,
1200 warehouse,
1201 voucher_type,
1202 voucher_no,
1203 allow_zero_rate=False,
1204 currency=None,
1205 company=None,
1206 raise_error_if_no_rate=True,
1207 batch_no=None,
1208):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301209
Ankush Menatf7ffe042021-11-01 13:21:14 +05301210 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301211 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301212
Ankush Menat342d09a2022-02-19 14:28:51 +05301213 last_valuation_rate = None
1214
1215 # Get moving average rate of a specific batch number
1216 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301217 last_valuation_rate = frappe.db.sql(
1218 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301219 select sum(stock_value_difference) / sum(actual_qty)
1220 from `tabStock Ledger Entry`
1221 where
1222 item_code = %s
1223 AND warehouse = %s
1224 AND batch_no = %s
1225 AND is_cancelled = 0
1226 AND NOT (voucher_no = %s AND voucher_type = %s)
1227 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301228 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1229 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301230
Ankush Menatf7ffe042021-11-01 13:21:14 +05301231 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301232 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301233 last_valuation_rate = frappe.db.sql(
1234 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301235 from `tabStock Ledger Entry` force index (item_warehouse)
1236 where
1237 item_code = %s
1238 AND warehouse = %s
1239 AND valuation_rate >= 0
1240 AND is_cancelled = 0
1241 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301242 order by posting_date desc, posting_time desc, name desc limit 1""",
1243 (item_code, warehouse, voucher_no, voucher_type),
1244 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301245
1246 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301247 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301248 last_valuation_rate = frappe.db.sql(
1249 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301250 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301251 where
1252 item_code = %s
1253 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301254 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301255 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301256 order by posting_date desc, posting_time desc, name desc limit 1""",
1257 (item_code, voucher_no, voucher_type),
1258 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301259
Nabin Haita645f362018-03-01 10:31:24 +05301260 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301261 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301262
1263 # If negative stock allowed, and item delivered without any incoming entry,
1264 # system does not found any SLE, then take valuation rate from Item
1265 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301266
1267 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301268 # try Item Standard rate
1269 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301270
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301271 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301272 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301273 valuation_rate = frappe.db.get_value(
1274 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1275 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301276
Ankush Menat494bd9e2022-03-28 18:52:46 +05301277 if (
1278 not allow_zero_rate
1279 and not valuation_rate
1280 and raise_error_if_no_rate
1281 and cint(erpnext.is_perpetual_inventory_enabled(company))
1282 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301283 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301284
Ankush Menat494bd9e2022-03-28 18:52:46 +05301285 message = _(
1286 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1287 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301288 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301289 solutions = (
1290 "<li>"
1291 + _(
1292 "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."
1293 ).format(voucher_type)
1294 + "</li>"
1295 )
1296 solutions += (
1297 "<li>"
1298 + _("If not, you can Cancel / Submit this entry")
1299 + " {0} ".format(frappe.bold("after"))
1300 + _("performing either one below:")
1301 + "</li>"
1302 )
Marica97715f22020-05-11 20:45:37 +05301303 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1304 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1305 msg = message + solutions + sub_solutions + "</li>"
1306
1307 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301308
1309 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301310
Ankush Menat494bd9e2022-03-28 18:52:46 +05301311
Ankush Menate7109c12021-08-26 16:40:45 +05301312def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301313 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301314 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301315 qty_shift = args.actual_qty
1316
Ankush Menat7c839c42022-05-06 12:09:08 +05301317 args["time_format"] = "%H:%i:%s"
1318
marination8418c4b2021-06-22 21:35:25 +05301319 # find difference/shift in qty caused by stock reconciliation
1320 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301321 qty_shift = get_stock_reco_qty_shift(args)
1322
1323 # find the next nearest stock reco so that we only recalculate SLEs till that point
1324 next_stock_reco_detail = get_next_stock_reco(args)
1325 if next_stock_reco_detail:
1326 detail = next_stock_reco_detail[0]
1327 # add condition to update SLEs before this date & time
1328 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301329
Ankush Menat494bd9e2022-03-28 18:52:46 +05301330 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301331 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301332 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301333 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301334 where
1335 item_code = %(item_code)s
1336 and warehouse = %(warehouse)s
1337 and voucher_no != %(voucher_no)s
1338 and is_cancelled = 0
Ankush Menat7c839c42022-05-06 12:09:08 +05301339 and timestamp(posting_date, time_format(posting_time, %(time_format)s))
1340 > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
marination40389772021-07-02 17:13:45 +05301341 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301342 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301343 args,
1344 )
Nabin Hait186a0452021-02-18 14:14:21 +05301345
1346 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1347
Ankush Menat494bd9e2022-03-28 18:52:46 +05301348
marination40389772021-07-02 17:13:45 +05301349def get_stock_reco_qty_shift(args):
1350 stock_reco_qty_shift = 0
1351 if args.get("is_cancelled"):
1352 if args.get("previous_qty_after_transaction"):
1353 # get qty (balance) that was set at submission
1354 last_balance = args.get("previous_qty_after_transaction")
1355 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1356 else:
1357 stock_reco_qty_shift = flt(args.actual_qty)
1358 else:
1359 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301360 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1361 "qty_after_transaction"
1362 )
marination40389772021-07-02 17:13:45 +05301363
1364 if last_balance is not None:
1365 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1366 else:
1367 stock_reco_qty_shift = args.qty_after_transaction
1368
1369 return stock_reco_qty_shift
1370
Ankush Menat494bd9e2022-03-28 18:52:46 +05301371
marination40389772021-07-02 17:13:45 +05301372def get_next_stock_reco(args):
1373 """Returns next nearest stock reconciliaton's details."""
1374
Ankush Menat494bd9e2022-03-28 18:52:46 +05301375 return frappe.db.sql(
1376 """
marination40389772021-07-02 17:13:45 +05301377 select
1378 name, posting_date, posting_time, creation, voucher_no
1379 from
marination8c441262021-07-02 17:46:05 +05301380 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301381 where
1382 item_code = %(item_code)s
1383 and warehouse = %(warehouse)s
1384 and voucher_type = 'Stock Reconciliation'
1385 and voucher_no != %(voucher_no)s
1386 and is_cancelled = 0
1387 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1388 or (
1389 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1390 and creation > %(creation)s
1391 )
1392 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301393 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301394 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301395 """,
1396 args,
1397 as_dict=1,
1398 )
1399
marination40389772021-07-02 17:13:45 +05301400
1401def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301402 return f"""
1403 and
1404 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1405 or (
1406 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1407 and creation < '{detail.creation}'
1408 )
1409 )"""
1410
Ankush Menat494bd9e2022-03-28 18:52:46 +05301411
Ankush Menate7109c12021-08-26 16:40:45 +05301412def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301413 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301414 return
1415 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1416 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301417
Ankush Menat5eba5752021-12-07 23:03:52 +05301418 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301419
1420 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301421 message = _(
1422 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1423 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301424 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301425 frappe.get_desk_link("Item", args.item_code),
1426 frappe.get_desk_link("Warehouse", args.warehouse),
1427 neg_sle[0]["posting_date"],
1428 neg_sle[0]["posting_time"],
1429 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1430 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301431
Ankush Menat494bd9e2022-03-28 18:52:46 +05301432 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301433
1434 if not args.batch_no:
1435 return
1436
1437 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301438 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301439 message = _(
1440 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1441 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301442 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301443 frappe.get_desk_link("Batch", args.batch_no),
1444 frappe.get_desk_link("Warehouse", args.warehouse),
1445 neg_batch_sle[0]["posting_date"],
1446 neg_batch_sle[0]["posting_time"],
1447 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1448 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301449 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301450
Nabin Haita77b8c92020-12-21 14:45:50 +05301451
Maricad6078aa2022-06-17 15:13:13 +05301452def is_negative_with_precision(neg_sle, is_batch=False):
1453 """
1454 Returns whether system precision rounded qty is insufficient.
1455 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1456 """
1457
1458 if not neg_sle:
1459 return False
1460
1461 field = "cumulative_total" if is_batch else "qty_after_transaction"
1462 precision = cint(frappe.db.get_default("float_precision")) or 2
1463 qty_deficit = flt(neg_sle[0][field], precision)
1464
1465 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1466
1467
Nabin Haita77b8c92020-12-21 14:45:50 +05301468def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301469 return frappe.db.sql(
1470 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301471 select
1472 qty_after_transaction, posting_date, posting_time,
1473 voucher_type, voucher_no
1474 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301475 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301476 item_code = %(item_code)s
1477 and warehouse = %(warehouse)s
1478 and voucher_no != %(voucher_no)s
1479 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1480 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301481 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301482 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301483 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301484 """,
1485 args,
1486 as_dict=1,
1487 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301488
Ankush Menat5eba5752021-12-07 23:03:52 +05301489
1490def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301491 return frappe.db.sql(
1492 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301493 with batch_ledger as (
1494 select
1495 posting_date, posting_time, voucher_type, voucher_no,
1496 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1497 from `tabStock Ledger Entry`
1498 where
1499 item_code = %(item_code)s
1500 and warehouse = %(warehouse)s
1501 and batch_no=%(batch_no)s
1502 and is_cancelled = 0
1503 order by posting_date, posting_time, creation
1504 )
1505 select * from batch_ledger
1506 where
1507 cumulative_total < 0.0
1508 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1509 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301510 """,
1511 args,
1512 as_dict=1,
1513 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301514
1515
1516def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1517 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1518 return True
1519 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1520 return True
1521 return False