blob: c146e42abd88eebf9e9a07125402b57659a6d243 [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 Haita77b8c92020-12-21 14:45:50 +0530211 if not args and voucher_type and voucher_no:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530212 args = get_items_to_be_repost(voucher_type, voucher_no, doc)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530213
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530214 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Ankush Menatecdb4932022-04-17 19:06:13 +0530215 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530216
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530217 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530218 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530219 validate_item_warehouse(args[i])
220
Ankush Menat494bd9e2022-03-28 18:52:46 +0530221 obj = update_entries_after(
222 {
223 "item_code": args[i].get("item_code"),
224 "warehouse": args[i].get("warehouse"),
225 "posting_date": args[i].get("posting_date"),
226 "posting_time": args[i].get("posting_time"),
227 "creation": args[i].get("creation"),
228 "distinct_item_warehouses": distinct_item_warehouses,
229 },
230 allow_negative_stock=allow_negative_stock,
231 via_landed_cost_voucher=via_landed_cost_voucher,
232 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530233 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530234
Ankush Menat494bd9e2022-03-28 18:52:46 +0530235 distinct_item_warehouses[
236 (args[i].get("item_code"), args[i].get("warehouse"))
237 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530238
Nabin Hait97bce3a2021-07-12 13:24:43 +0530239 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530240 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530241 if ("args_idx" not in data and not data.reposting_status) or (
242 data.sle_changed and data.reposting_status
243 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530244 data.args_idx = len(args)
245 args.append(data.sle)
246 elif data.sle_changed and not data.reposting_status:
247 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530248
Nabin Hait97bce3a2021-07-12 13:24:43 +0530249 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530250 i += 1
251
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530252 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530253 update_args_in_repost_item_valuation(
254 doc, i, args, distinct_item_warehouses, affected_transactions
255 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530256
Ankush Menat494bd9e2022-03-28 18:52:46 +0530257
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530258def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530259 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530260 if not args.get(field):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530261 validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530262 frappe.throw(_(validation_msg))
263
Ankush Menat494bd9e2022-03-28 18:52:46 +0530264
Ankush Menatecdb4932022-04-17 19:06:13 +0530265def update_args_in_repost_item_valuation(
266 doc, index, args, distinct_item_warehouses, affected_transactions
267):
Ankush Menatecdb4932022-04-17 19:06:13 +0530268 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530269 {
270 "items_to_be_repost": json.dumps(args, default=str),
271 "distinct_item_and_warehouse": json.dumps(
272 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
273 ),
274 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530275 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530276 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530277 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530278
Ankush Menatecdb4932022-04-17 19:06:13 +0530279 if not frappe.flags.in_test:
280 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530281
Ankush Menat494bd9e2022-03-28 18:52:46 +0530282 frappe.publish_realtime(
283 "item_reposting_progress",
284 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
285 )
286
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530287
288def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
289 if doc and doc.items_to_be_repost:
290 return json.loads(doc.items_to_be_repost) or []
291
Ankush Menat494bd9e2022-03-28 18:52:46 +0530292 return frappe.db.get_all(
293 "Stock Ledger Entry",
Nabin Haita77b8c92020-12-21 14:45:50 +0530294 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
Nabin Hait186a0452021-02-18 14:14:21 +0530295 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
Nabin Haita77b8c92020-12-21 14:45:50 +0530296 order_by="creation asc",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530297 group_by="item_code, warehouse",
Nabin Haita77b8c92020-12-21 14:45:50 +0530298 )
Nabin Hait74c281c2013-08-19 16:17:18 +0530299
Ankush Menat494bd9e2022-03-28 18:52:46 +0530300
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530301def get_distinct_item_warehouse(args=None, doc=None):
302 distinct_item_warehouses = {}
303 if doc and doc.distinct_item_and_warehouse:
304 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530305 distinct_item_warehouses = {
306 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
307 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530308 else:
309 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530310 distinct_item_warehouses.setdefault(
311 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
312 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530313
314 return distinct_item_warehouses
315
Ankush Menat494bd9e2022-03-28 18:52:46 +0530316
Ankush Menatecdb4932022-04-17 19:06:13 +0530317def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
318 if not doc.affected_transactions:
319 return set()
320
321 transactions = frappe.parse_json(doc.affected_transactions)
322 return {tuple(transaction) for transaction in transactions}
323
324
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530325def get_current_index(doc=None):
326 if doc and doc.current_index:
327 return doc.current_index
328
Ankush Menat494bd9e2022-03-28 18:52:46 +0530329
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530330class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530331 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530332 update valution rate and qty after transaction
333 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530334
Ankush Menat494bd9e2022-03-28 18:52:46 +0530335 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530336
Ankush Menat494bd9e2022-03-28 18:52:46 +0530337 args = {
338 "item_code": "ABC",
339 "warehouse": "XYZ",
340 "posting_date": "2012-12-12",
341 "posting_time": "12:00"
342 }
Nabin Hait902e8602013-01-08 18:29:24 +0530343 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530344
345 def __init__(
346 self,
347 args,
348 allow_zero_rate=False,
349 allow_negative_stock=None,
350 via_landed_cost_voucher=False,
351 verbose=1,
352 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530353 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530354 self.verbose = verbose
355 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530356 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530357 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530358 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
359 item_code=self.item_code
360 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530361
Nabin Haita77b8c92020-12-21 14:45:50 +0530362 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530363 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530364 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530365
Nabin Haita77b8c92020-12-21 14:45:50 +0530366 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530367 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530368 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530369
370 self.new_items_found = False
371 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530372 self.affected_transactions: Set[Tuple[str, str]] = set()
Nabin Haita77b8c92020-12-21 14:45:50 +0530373
374 self.data = frappe._dict()
375 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530376 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530377
Maricad6078aa2022-06-17 15:13:13 +0530378 def set_precision(self):
379 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
380 self.currency_precision = get_field_precision(
381 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530382 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530383
384 def initialize_previous_data(self, args):
385 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530386 Get previous sl entries for current item for each related warehouse
387 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530388
Ankush Menat494bd9e2022-03-28 18:52:46 +0530389 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530390
Ankush Menat494bd9e2022-03-28 18:52:46 +0530391 self.data = {
392 warehouse1: {
393 'previus_sle': {},
394 'qty_after_transaction': 10,
395 'valuation_rate': 100,
396 'stock_value': 1000,
397 'prev_stock_value': 1000,
398 'stock_queue': '[[10, 100]]',
399 'stock_value_difference': 1000
400 }
401 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530402
403 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530404 self.data.setdefault(args.warehouse, frappe._dict())
405 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530406 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530407 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530408
Ankush Menatc1d986a2021-08-31 19:43:42 +0530409 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
410 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
411
Ankush Menat494bd9e2022-03-28 18:52:46 +0530412 warehouse_dict.update(
413 {
414 "prev_stock_value": previous_sle.stock_value or 0.0,
415 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
416 "stock_value_difference": 0.0,
417 }
418 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530419
Nabin Haita77b8c92020-12-21 14:45:50 +0530420 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530421 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530422
Nabin Haita77b8c92020-12-21 14:45:50 +0530423 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530424 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530425 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530426 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530427 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530428 entries_to_fix = self.get_future_entries_to_fix()
429
430 i = 0
431 while i < len(entries_to_fix):
432 sle = entries_to_fix[i]
433 i += 1
434
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530435 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530436
Nabin Haita77b8c92020-12-21 14:45:50 +0530437 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530438 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530439
Nabin Hait186a0452021-02-18 14:14:21 +0530440 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530441
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530442 if self.exceptions:
443 self.raise_exceptions()
444
Nabin Hait186a0452021-02-18 14:14:21 +0530445 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530446 sl_entries = self.get_sle_against_current_voucher()
447 for sle in sl_entries:
448 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530449
Nabin Haita77b8c92020-12-21 14:45:50 +0530450 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530451 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530452
Ankush Menat494bd9e2022-03-28 18:52:46 +0530453 return frappe.db.sql(
454 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530455 select
456 *, timestamp(posting_date, posting_time) as "timestamp"
457 from
458 `tabStock Ledger Entry`
459 where
460 item_code = %(item_code)s
461 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530462 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530463 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
464
Nabin Haita77b8c92020-12-21 14:45:50 +0530465 order by
466 creation ASC
467 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530468 """,
469 self.args,
470 as_dict=1,
471 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530472
Nabin Haita77b8c92020-12-21 14:45:50 +0530473 def get_future_entries_to_fix(self):
474 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530475 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
476 {"item_code": self.item_code, "warehouse": self.args.warehouse}
477 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530478
Nabin Haita77b8c92020-12-21 14:45:50 +0530479 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530480
Nabin Haita77b8c92020-12-21 14:45:50 +0530481 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530482 dependant_sle = get_sle_by_voucher_detail_no(
483 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
484 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530485
Nabin Haita77b8c92020-12-21 14:45:50 +0530486 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530487 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530488 elif (
489 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
490 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530491 return entries_to_fix
492 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530493 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530494 return entries_to_fix
495 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
496 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530497 else:
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530498 self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
499 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530500
501 def update_distinct_item_warehouses(self, dependant_sle):
502 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530503 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530504 if key not in self.distinct_item_warehouses:
505 self.distinct_item_warehouses[key] = val
506 self.new_items_found = True
507 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530508 existing_sle_posting_date = (
509 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
510 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530511 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
512 val.sle_changed = True
513 self.distinct_item_warehouses[key] = val
514 self.new_items_found = True
515
516 def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
Nabin Haita77b8c92020-12-21 14:45:50 +0530517 self.initialize_previous_data(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530518 self.distinct_item_warehouses[(self.item_code, dependant_sle.warehouse)] = frappe._dict(
519 {"sle": dependant_sle}
Ankush Menat494bd9e2022-03-28 18:52:46 +0530520 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530521
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530522 self.new_items_found = True
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530523
524 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530525 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
526
Nabin Haita77b8c92020-12-21 14:45:50 +0530527 # previous sle data for this warehouse
528 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530529 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530530
Anand Doshi0dc79f42015-04-06 12:59:34 +0530531 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 +0530532 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530533 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530534 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530535 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530536 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530537
Nabin Haita77b8c92020-12-21 14:45:50 +0530538 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530539 if not self.args.get("sle_id"):
540 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530541
Ankush Menat66bf21f2022-01-16 20:45:59 +0530542 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530543 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530544 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530545 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530546 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530547
Ankush Menat494bd9e2022-03-28 18:52:46 +0530548 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
549 self.wh_data.valuation_rate
550 )
551 elif sle.batch_no and frappe.db.get_value(
552 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
553 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530554 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530555 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530556 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530557 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530558 self.wh_data.valuation_rate = sle.valuation_rate
559 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530560 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
561 self.wh_data.valuation_rate
562 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530563 if self.valuation_method != "Moving Average":
564 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530565 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530566 if self.valuation_method == "Moving Average":
567 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530568 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530569 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
570 self.wh_data.valuation_rate
571 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530572 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530573 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530574
Rushabh Mehta54047782013-12-26 11:07:46 +0530575 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530576 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530577 if not self.wh_data.qty_after_transaction:
578 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530579 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
580 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530581
Nabin Hait902e8602013-01-08 18:29:24 +0530582 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530583 sle.qty_after_transaction = self.wh_data.qty_after_transaction
584 sle.valuation_rate = self.wh_data.valuation_rate
585 sle.stock_value = self.wh_data.stock_value
586 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530587 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530588 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530589 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530590
Ankush Menat701878f2022-03-01 18:08:29 +0530591 if not self.args.get("sle_id"):
592 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530593
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530594 def validate_negative_stock(self, sle):
595 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530596 validate negative stock for entries current datetime onwards
597 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530598 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530599 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Maricad6078aa2022-06-17 15:13:13 +0530600 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530601
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530602 if diff < 0 and abs(diff) > 0.0001:
603 # negative stock!
604 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530605 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530606 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530607 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530608 return True
609
Nabin Haita77b8c92020-12-21 14:45:50 +0530610 def get_dynamic_incoming_outgoing_rate(self, sle):
611 # Get updated incoming/outgoing rate from transaction
612 if sle.recalculate_rate:
613 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
614
615 if flt(sle.actual_qty) >= 0:
616 sle.incoming_rate = rate
617 else:
618 sle.outgoing_rate = rate
619
620 def get_incoming_outgoing_rate_from_transaction(self, sle):
621 rate = 0
622 # Material Transfer, Repack, Manufacturing
623 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530624 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530625 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
626 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530627 elif sle.voucher_type in (
628 "Purchase Receipt",
629 "Purchase Invoice",
630 "Delivery Note",
631 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530632 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530633 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530634 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530635 from erpnext.controllers.sales_and_purchase_return import (
636 get_rate_for_return, # don't move this import to top
637 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530638
639 rate = get_rate_for_return(
640 sle.voucher_type,
641 sle.voucher_no,
642 sle.item_code,
643 voucher_detail_no=sle.voucher_detail_no,
644 sle=sle,
645 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530646 else:
647 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530648 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530649 elif sle.voucher_type == "Subcontracting Receipt":
650 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530651 else:
652 rate_field = "incoming_rate"
653
654 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530655 item_code, incoming_rate = frappe.db.get_value(
656 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
657 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530658
659 if item_code == sle.item_code:
660 rate = incoming_rate
661 else:
662 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
663 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530664 elif sle == "Subcontracting Receipt":
665 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530666 else:
667 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530668
Ankush Menat494bd9e2022-03-28 18:52:46 +0530669 rate = frappe.db.get_value(
670 ref_doctype,
671 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
672 rate_field,
673 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530674
675 return rate
676
677 def update_outgoing_rate_on_transaction(self, sle):
678 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530679 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
680 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530681 """
682 if sle.actual_qty and sle.voucher_detail_no:
683 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
684
685 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
686 self.update_rate_on_stock_entry(sle, outgoing_rate)
687 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
688 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
689 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
690 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530691 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
692 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530693
694 def update_rate_on_stock_entry(self, sle, outgoing_rate):
695 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
696
Ankush Menat701878f2022-03-01 18:08:29 +0530697 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
698 if not sle.dependant_sle_voucher_detail_no:
699 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530700
701 def recalculate_amounts_in_stock_entry(self, voucher_no):
702 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530703 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
704 stock_entry.db_update()
705 for d in stock_entry.items:
706 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530707
Nabin Haita77b8c92020-12-21 14:45:50 +0530708 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
709 # Update item's incoming rate on transaction
710 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
711 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530712 frappe.db.set_value(
713 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
714 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530715 else:
716 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530717 frappe.db.set_value(
718 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530719 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530720 "incoming_rate",
721 outgoing_rate,
722 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530723
724 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
725 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530726 frappe.db.set_value(
727 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
728 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530729 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530730 frappe.db.set_value(
731 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
732 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530733
734 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530735 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530736 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530737 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530738 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530739 d.db_update()
740
Sagar Sharma323bdf82022-05-17 15:14:07 +0530741 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
742 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
743 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
744 else:
745 frappe.db.set_value(
746 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
747 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530748
749 def get_serialized_values(self, sle):
750 incoming_rate = flt(sle.incoming_rate)
751 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530752 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530753
754 if incoming_rate < 0:
755 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530756 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530757
Nabin Hait2620bf42016-02-29 11:30:27 +0530758 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530759 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530760 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530761 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530762 # In case of delivery/stock issue, get average purchase rate
763 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530764 if not sle.is_cancelled:
765 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
766 stock_value_change = -1 * outgoing_value
767 else:
768 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530769
Nabin Haita77b8c92020-12-21 14:45:50 +0530770 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530771
Nabin Hait2620bf42016-02-29 11:30:27 +0530772 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530773 new_stock_value = (
774 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
775 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530776 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530777 # calculate new valuation rate only if stock value is positive
778 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530779 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530780
Nabin Haita77b8c92020-12-21 14:45:50 +0530781 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530782 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
783 sle.voucher_type, sle.voucher_detail_no
784 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530785 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530786 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530787
Nabin Hait328c4f92020-01-02 19:00:32 +0530788 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
789 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530790 all_serial_nos = frappe.get_all(
791 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
792 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530793
Ankush Menat494bd9e2022-03-28 18:52:46 +0530794 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 +0530795
796 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530797 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530798 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530799 incoming_rate = frappe.db.sql(
800 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530801 select incoming_rate
802 from `tabStock Ledger Entry`
803 where
804 company = %s
805 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530806 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530807 and (serial_no = %s
808 or serial_no like %s
809 or serial_no like %s
810 or serial_no like %s
811 )
812 order by posting_date desc
813 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530814 """,
815 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
816 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530817
818 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
819
820 return incoming_values
821
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530822 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530823 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530824 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530825 if new_stock_qty >= 0:
826 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530827 if flt(self.wh_data.qty_after_transaction) <= 0:
828 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530829 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530830 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
831 actual_qty * sle.incoming_rate
832 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530833
Nabin Haita77b8c92020-12-21 14:45:50 +0530834 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530835
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530836 elif sle.outgoing_rate:
837 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530838 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
839 actual_qty * sle.outgoing_rate
840 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530841
Nabin Haita77b8c92020-12-21 14:45:50 +0530842 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530843 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530844 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530845 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530846 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
847 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530848
Nabin Haita77b8c92020-12-21 14:45:50 +0530849 if not self.wh_data.valuation_rate and actual_qty > 0:
850 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530851
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530852 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800853 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530854 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530855 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
856 sle.voucher_type, sle.voucher_detail_no
857 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800858 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530859 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530860
Ankush Menatf089d392022-02-02 12:51:21 +0530861 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530862 incoming_rate = flt(sle.incoming_rate)
863 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530864 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530865
Ankush Menat494bd9e2022-03-28 18:52:46 +0530866 self.wh_data.qty_after_transaction = round_off_if_near_zero(
867 self.wh_data.qty_after_transaction + actual_qty
868 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530869
Ankush Menat97e18a12022-01-15 17:42:25 +0530870 if self.valuation_method == "LIFO":
871 stock_queue = LIFOValuation(self.wh_data.stock_queue)
872 else:
873 stock_queue = FIFOValuation(self.wh_data.stock_queue)
874
Ankush Menatb534fee2022-02-19 20:58:36 +0530875 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
876
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530877 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530878 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530879 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530880
Ankush Menat4b29fb62021-12-18 18:40:22 +0530881 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530882 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
883 sle.voucher_type, sle.voucher_detail_no
884 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530885 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530886 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530887 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530888 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530889
Ankush Menat494bd9e2022-03-28 18:52:46 +0530890 stock_queue.remove_stock(
891 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
892 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530893
Ankush Menatb534fee2022-02-19 20:58:36 +0530894 _qty, stock_value = stock_queue.get_total_stock_and_value()
895
896 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530897
Ankush Menat97e18a12022-01-15 17:42:25 +0530898 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530899 self.wh_data.stock_value = round_off_if_near_zero(
900 self.wh_data.stock_value + stock_value_difference
901 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530902
Nabin Haita77b8c92020-12-21 14:45:50 +0530903 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530904 self.wh_data.stock_queue.append(
905 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
906 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530907
Ankush Menatb534fee2022-02-19 20:58:36 +0530908 if self.wh_data.qty_after_transaction:
909 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
910
Ankush Menatce0514c2022-02-15 11:41:41 +0530911 def update_batched_values(self, sle):
912 incoming_rate = flt(sle.incoming_rate)
913 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530914
Ankush Menat494bd9e2022-03-28 18:52:46 +0530915 self.wh_data.qty_after_transaction = round_off_if_near_zero(
916 self.wh_data.qty_after_transaction + actual_qty
917 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530918
919 if actual_qty > 0:
920 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530921 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530922 outgoing_rate = get_batch_incoming_rate(
923 item_code=sle.item_code,
924 warehouse=sle.warehouse,
925 batch_no=sle.batch_no,
926 posting_date=sle.posting_date,
927 posting_time=sle.posting_time,
928 creation=sle.creation,
929 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530930 if outgoing_rate is None:
931 # This can *only* happen if qty available for the batch is zero.
932 # in such case fall back various other rates.
933 # future entries will correct the overall accounting as each
934 # batch individually uses moving average rates.
935 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530936 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530937
Ankush Menat494bd9e2022-03-28 18:52:46 +0530938 self.wh_data.stock_value = round_off_if_near_zero(
939 self.wh_data.stock_value + stock_value_difference
940 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530941 if self.wh_data.qty_after_transaction:
942 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530943
Javier Wong9b11d9b2017-04-14 18:24:04 +0800944 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530945 ref_item_dt = ""
946
947 if voucher_type == "Stock Entry":
948 ref_item_dt = voucher_type + " Detail"
949 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
950 ref_item_dt = voucher_type + " Item"
951
952 if ref_item_dt:
953 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
954 else:
955 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530956
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530957 def get_fallback_rate(self, sle) -> float:
958 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530959 This should only get used for negative stock."""
960 return get_valuation_rate(
961 sle.item_code,
962 sle.warehouse,
963 sle.voucher_type,
964 sle.voucher_no,
965 self.allow_zero_rate,
966 currency=erpnext.get_company_currency(sle.company),
967 company=sle.company,
968 batch_no=sle.batch_no,
969 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530970
Nabin Haita77b8c92020-12-21 14:45:50 +0530971 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530972 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530973 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
974 sle = sle[0] if sle else frappe._dict()
975 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530976
Nabin Haita77b8c92020-12-21 14:45:50 +0530977 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530978 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530979 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530980
981 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530982 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530983 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530984 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530985
Ankush Menat494bd9e2022-03-28 18:52:46 +0530986 if (
987 exceptions[0]["voucher_type"],
988 exceptions[0]["voucher_no"],
989 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +0530990
Nabin Haita77b8c92020-12-21 14:45:50 +0530991 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530992 abs(deficiency),
993 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
994 frappe.get_desk_link("Warehouse", warehouse),
995 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530996 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530997 msg = _(
998 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
999 ).format(
1000 abs(deficiency),
1001 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1002 frappe.get_desk_link("Warehouse", warehouse),
1003 exceptions[0]["posting_date"],
1004 exceptions[0]["posting_time"],
1005 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1006 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301007
Nabin Haita77b8c92020-12-21 14:45:50 +05301008 if msg:
1009 msg_list.append(msg)
1010
1011 if msg_list:
1012 message = "\n\n".join(msg_list)
1013 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301014 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301015 else:
1016 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301017
Nabin Haita77b8c92020-12-21 14:45:50 +05301018 def update_bin(self):
1019 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301020 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301021 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301022
Ankush Menat494bd9e2022-03-28 18:52:46 +05301023 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301024 if data.valuation_rate is not None:
1025 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301026 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301027
marination8418c4b2021-06-22 21:35:25 +05301028
1029def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1030 """get stock ledger entries filtered by specific posting datetime conditions"""
1031
Ankush Menat494bd9e2022-03-28 18:52:46 +05301032 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301033 if not args.get("posting_date"):
1034 args["posting_date"] = "1900-01-01"
1035 if not args.get("posting_time"):
1036 args["posting_time"] = "00:00"
1037
1038 voucher_condition = ""
1039 if exclude_current_voucher:
1040 voucher_no = args.get("voucher_no")
1041 voucher_condition = f"and voucher_no != '{voucher_no}'"
1042
Ankush Menat494bd9e2022-03-28 18:52:46 +05301043 sle = frappe.db.sql(
1044 """
marination8418c4b2021-06-22 21:35:25 +05301045 select *, timestamp(posting_date, posting_time) as "timestamp"
1046 from `tabStock Ledger Entry`
1047 where item_code = %(item_code)s
1048 and warehouse = %(warehouse)s
1049 and is_cancelled = 0
1050 {voucher_condition}
1051 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1052 order by timestamp(posting_date, posting_time) desc, creation desc
1053 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301054 for update""".format(
1055 voucher_condition=voucher_condition
1056 ),
1057 args,
1058 as_dict=1,
1059 )
marination8418c4b2021-06-22 21:35:25 +05301060
1061 return sle[0] if sle else frappe._dict()
1062
Ankush Menat494bd9e2022-03-28 18:52:46 +05301063
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301064def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301065 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301066 get the last sle on or before the current time-bucket,
1067 to get actual qty before transaction, this function
1068 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301069
Ankush Menat494bd9e2022-03-28 18:52:46 +05301070 args = {
1071 "item_code": "ABC",
1072 "warehouse": "XYZ",
1073 "posting_date": "2012-12-12",
1074 "posting_time": "12:00",
1075 "sle": "name of reference Stock Ledger Entry"
1076 }
Anand Doshi1b531862013-01-10 19:29:51 +05301077 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301078 args["name"] = args.get("sle", None) or ""
1079 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301080 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301081
Ankush Menat494bd9e2022-03-28 18:52:46 +05301082
1083def get_stock_ledger_entries(
1084 previous_sle,
1085 operator=None,
1086 order="desc",
1087 limit=None,
1088 for_update=False,
1089 debug=False,
1090 check_serial_no=True,
1091):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301092 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301093 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1094 operator
1095 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301096 if previous_sle.get("warehouse"):
1097 conditions += " and warehouse = %(warehouse)s"
1098 elif previous_sle.get("warehouse_condition"):
1099 conditions += " and " + previous_sle.get("warehouse_condition")
1100
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301101 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301102 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1103 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301104 conditions += (
1105 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301106 (
1107 serial_no = {0}
1108 or serial_no like {1}
1109 or serial_no like {2}
1110 or serial_no like {3}
1111 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301112 """
1113 ).format(
1114 frappe.db.escape(serial_no),
1115 frappe.db.escape("{}\n%".format(serial_no)),
1116 frappe.db.escape("%\n{}".format(serial_no)),
1117 frappe.db.escape("%\n{}\n%".format(serial_no)),
1118 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301119
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301120 if not previous_sle.get("posting_date"):
1121 previous_sle["posting_date"] = "1900-01-01"
1122 if not previous_sle.get("posting_time"):
1123 previous_sle["posting_time"] = "00:00"
1124
1125 if operator in (">", "<=") and previous_sle.get("name"):
1126 conditions += " and name!=%(name)s"
1127
Ankush Menat494bd9e2022-03-28 18:52:46 +05301128 return frappe.db.sql(
1129 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301130 select *, timestamp(posting_date, posting_time) as "timestamp"
1131 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301132 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301133 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301134 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301135 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301136 %(limit)s %(for_update)s"""
1137 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301138 "conditions": conditions,
1139 "limit": limit or "",
1140 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301141 "order": order,
1142 },
1143 previous_sle,
1144 as_dict=1,
1145 debug=debug,
1146 )
1147
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301148
Nabin Haita77b8c92020-12-21 14:45:50 +05301149def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301150 return frappe.db.get_value(
1151 "Stock Ledger Entry",
1152 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1153 [
1154 "item_code",
1155 "warehouse",
1156 "posting_date",
1157 "posting_time",
1158 "timestamp(posting_date, posting_time) as timestamp",
1159 ],
1160 as_dict=1,
1161 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301162
Ankush Menatce0514c2022-02-15 11:41:41 +05301163
Ankush Menat494bd9e2022-03-28 18:52:46 +05301164def get_batch_incoming_rate(
1165 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1166):
1167
Ankush Menat102fff22022-02-19 15:51:04 +05301168 sle = frappe.qb.DocType("Stock Ledger Entry")
1169
Ankush Menate1c16872022-04-21 20:01:48 +05301170 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301171 posting_date, posting_time
1172 )
Ankush Menat102fff22022-02-19 15:51:04 +05301173 if creation:
1174 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301175 CombineDatetime(sle.posting_date, sle.posting_time)
1176 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301177 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301178
1179 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301180 frappe.qb.from_(sle)
1181 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1182 .where(
1183 (sle.item_code == item_code)
1184 & (sle.warehouse == warehouse)
1185 & (sle.batch_no == batch_no)
1186 & (sle.is_cancelled == 0)
1187 )
1188 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301189 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301190
1191 if batch_details and batch_details[0].batch_qty:
1192 return batch_details[0].batch_value / batch_details[0].batch_qty
1193
1194
Ankush Menat494bd9e2022-03-28 18:52:46 +05301195def get_valuation_rate(
1196 item_code,
1197 warehouse,
1198 voucher_type,
1199 voucher_no,
1200 allow_zero_rate=False,
1201 currency=None,
1202 company=None,
1203 raise_error_if_no_rate=True,
1204 batch_no=None,
1205):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301206
Ankush Menatf7ffe042021-11-01 13:21:14 +05301207 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301208 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301209
Ankush Menat342d09a2022-02-19 14:28:51 +05301210 last_valuation_rate = None
1211
1212 # Get moving average rate of a specific batch number
1213 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301214 last_valuation_rate = frappe.db.sql(
1215 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301216 select sum(stock_value_difference) / sum(actual_qty)
1217 from `tabStock Ledger Entry`
1218 where
1219 item_code = %s
1220 AND warehouse = %s
1221 AND batch_no = %s
1222 AND is_cancelled = 0
1223 AND NOT (voucher_no = %s AND voucher_type = %s)
1224 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301225 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1226 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301227
Ankush Menatf7ffe042021-11-01 13:21:14 +05301228 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301229 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301230 last_valuation_rate = frappe.db.sql(
1231 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301232 from `tabStock Ledger Entry` force index (item_warehouse)
1233 where
1234 item_code = %s
1235 AND warehouse = %s
1236 AND valuation_rate >= 0
1237 AND is_cancelled = 0
1238 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301239 order by posting_date desc, posting_time desc, name desc limit 1""",
1240 (item_code, warehouse, voucher_no, voucher_type),
1241 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301242
1243 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301244 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301245 last_valuation_rate = frappe.db.sql(
1246 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301247 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301248 where
1249 item_code = %s
1250 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301251 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301252 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301253 order by posting_date desc, posting_time desc, name desc limit 1""",
1254 (item_code, voucher_no, voucher_type),
1255 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301256
Nabin Haita645f362018-03-01 10:31:24 +05301257 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301258 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301259
1260 # If negative stock allowed, and item delivered without any incoming entry,
1261 # system does not found any SLE, then take valuation rate from Item
1262 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301263
1264 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301265 # try Item Standard rate
1266 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301267
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301268 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301269 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301270 valuation_rate = frappe.db.get_value(
1271 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1272 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301273
Ankush Menat494bd9e2022-03-28 18:52:46 +05301274 if (
1275 not allow_zero_rate
1276 and not valuation_rate
1277 and raise_error_if_no_rate
1278 and cint(erpnext.is_perpetual_inventory_enabled(company))
1279 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301280 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301281
Ankush Menat494bd9e2022-03-28 18:52:46 +05301282 message = _(
1283 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1284 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301285 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301286 solutions = (
1287 "<li>"
1288 + _(
1289 "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."
1290 ).format(voucher_type)
1291 + "</li>"
1292 )
1293 solutions += (
1294 "<li>"
1295 + _("If not, you can Cancel / Submit this entry")
1296 + " {0} ".format(frappe.bold("after"))
1297 + _("performing either one below:")
1298 + "</li>"
1299 )
Marica97715f22020-05-11 20:45:37 +05301300 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1301 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1302 msg = message + solutions + sub_solutions + "</li>"
1303
1304 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301305
1306 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301307
Ankush Menat494bd9e2022-03-28 18:52:46 +05301308
Ankush Menate7109c12021-08-26 16:40:45 +05301309def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301310 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301311 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301312 qty_shift = args.actual_qty
1313
Ankush Menat7c839c42022-05-06 12:09:08 +05301314 args["time_format"] = "%H:%i:%s"
1315
marination8418c4b2021-06-22 21:35:25 +05301316 # find difference/shift in qty caused by stock reconciliation
1317 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301318 qty_shift = get_stock_reco_qty_shift(args)
1319
1320 # find the next nearest stock reco so that we only recalculate SLEs till that point
1321 next_stock_reco_detail = get_next_stock_reco(args)
1322 if next_stock_reco_detail:
1323 detail = next_stock_reco_detail[0]
1324 # add condition to update SLEs before this date & time
1325 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301326
Ankush Menat494bd9e2022-03-28 18:52:46 +05301327 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301328 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301329 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301330 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301331 where
1332 item_code = %(item_code)s
1333 and warehouse = %(warehouse)s
1334 and voucher_no != %(voucher_no)s
1335 and is_cancelled = 0
Ankush Menat7c839c42022-05-06 12:09:08 +05301336 and timestamp(posting_date, time_format(posting_time, %(time_format)s))
1337 > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
marination40389772021-07-02 17:13:45 +05301338 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301339 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301340 args,
1341 )
Nabin Hait186a0452021-02-18 14:14:21 +05301342
1343 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1344
Ankush Menat494bd9e2022-03-28 18:52:46 +05301345
marination40389772021-07-02 17:13:45 +05301346def get_stock_reco_qty_shift(args):
1347 stock_reco_qty_shift = 0
1348 if args.get("is_cancelled"):
1349 if args.get("previous_qty_after_transaction"):
1350 # get qty (balance) that was set at submission
1351 last_balance = args.get("previous_qty_after_transaction")
1352 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1353 else:
1354 stock_reco_qty_shift = flt(args.actual_qty)
1355 else:
1356 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301357 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1358 "qty_after_transaction"
1359 )
marination40389772021-07-02 17:13:45 +05301360
1361 if last_balance is not None:
1362 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1363 else:
1364 stock_reco_qty_shift = args.qty_after_transaction
1365
1366 return stock_reco_qty_shift
1367
Ankush Menat494bd9e2022-03-28 18:52:46 +05301368
marination40389772021-07-02 17:13:45 +05301369def get_next_stock_reco(args):
1370 """Returns next nearest stock reconciliaton's details."""
1371
Ankush Menat494bd9e2022-03-28 18:52:46 +05301372 return frappe.db.sql(
1373 """
marination40389772021-07-02 17:13:45 +05301374 select
1375 name, posting_date, posting_time, creation, voucher_no
1376 from
marination8c441262021-07-02 17:46:05 +05301377 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301378 where
1379 item_code = %(item_code)s
1380 and warehouse = %(warehouse)s
1381 and voucher_type = 'Stock Reconciliation'
1382 and voucher_no != %(voucher_no)s
1383 and is_cancelled = 0
1384 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1385 or (
1386 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1387 and creation > %(creation)s
1388 )
1389 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301390 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301391 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301392 """,
1393 args,
1394 as_dict=1,
1395 )
1396
marination40389772021-07-02 17:13:45 +05301397
1398def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301399 return f"""
1400 and
1401 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1402 or (
1403 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1404 and creation < '{detail.creation}'
1405 )
1406 )"""
1407
Ankush Menat494bd9e2022-03-28 18:52:46 +05301408
Ankush Menate7109c12021-08-26 16:40:45 +05301409def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301410 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301411 return
1412 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1413 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301414
Ankush Menat5eba5752021-12-07 23:03:52 +05301415 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301416
1417 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301418 message = _(
1419 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1420 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301421 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301422 frappe.get_desk_link("Item", args.item_code),
1423 frappe.get_desk_link("Warehouse", args.warehouse),
1424 neg_sle[0]["posting_date"],
1425 neg_sle[0]["posting_time"],
1426 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1427 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301428
Ankush Menat494bd9e2022-03-28 18:52:46 +05301429 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301430
1431 if not args.batch_no:
1432 return
1433
1434 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301435 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301436 message = _(
1437 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1438 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301439 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301440 frappe.get_desk_link("Batch", args.batch_no),
1441 frappe.get_desk_link("Warehouse", args.warehouse),
1442 neg_batch_sle[0]["posting_date"],
1443 neg_batch_sle[0]["posting_time"],
1444 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1445 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301446 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301447
Nabin Haita77b8c92020-12-21 14:45:50 +05301448
Maricad6078aa2022-06-17 15:13:13 +05301449def is_negative_with_precision(neg_sle, is_batch=False):
1450 """
1451 Returns whether system precision rounded qty is insufficient.
1452 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1453 """
1454
1455 if not neg_sle:
1456 return False
1457
1458 field = "cumulative_total" if is_batch else "qty_after_transaction"
1459 precision = cint(frappe.db.get_default("float_precision")) or 2
1460 qty_deficit = flt(neg_sle[0][field], precision)
1461
1462 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1463
1464
Nabin Haita77b8c92020-12-21 14:45:50 +05301465def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301466 return frappe.db.sql(
1467 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301468 select
1469 qty_after_transaction, posting_date, posting_time,
1470 voucher_type, voucher_no
1471 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301472 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301473 item_code = %(item_code)s
1474 and warehouse = %(warehouse)s
1475 and voucher_no != %(voucher_no)s
1476 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1477 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301478 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301479 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301480 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301481 """,
1482 args,
1483 as_dict=1,
1484 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301485
Ankush Menat5eba5752021-12-07 23:03:52 +05301486
1487def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301488 return frappe.db.sql(
1489 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301490 with batch_ledger as (
1491 select
1492 posting_date, posting_time, voucher_type, voucher_no,
1493 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1494 from `tabStock Ledger Entry`
1495 where
1496 item_code = %(item_code)s
1497 and warehouse = %(warehouse)s
1498 and batch_no=%(batch_no)s
1499 and is_cancelled = 0
1500 order by posting_date, posting_time, creation
1501 )
1502 select * from batch_ledger
1503 where
1504 cumulative_total < 0.0
1505 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1506 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301507 """,
1508 args,
1509 as_dict=1,
1510 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301511
1512
1513def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1514 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1515 return True
1516 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1517 return True
1518 return False