blob: 3524a47c713eaaaa64eeb0eec9d5d83c0a8b6b82 [file] [log] [blame]
Maricad6078aa2022-06-17 15:13:13 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait902e8602013-01-08 18:29:24 +05303
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05304import copy
Nabin Hait26d46552013-01-09 15:23:05 +05305import json
Ankush Menatecdb4932022-04-17 19:06:13 +05306from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
9from frappe import _
10from frappe.model.meta import get_field_precision
Ankush Menate1c16872022-04-21 20:01:48 +053011from frappe.query_builder.functions import CombineDatetime, Sum
Ankush Menatcef84c22021-12-03 12:18:59 +053012from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
Achilles Rasquinha361366e2018-02-14 17:08:59 +053013
Chillar Anand915b3432021-09-02 16:44:59 +053014import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053015from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
Chillar Anand915b3432021-09-02 16:44:59 +053016from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053017 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053018 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053019 get_valuation_method,
20)
Ankush Menatb534fee2022-02-19 20:58:36 +053021from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053022
Nabin Hait97bce3a2021-07-12 13:24:43 +053023
Ankush Menat494bd9e2022-03-28 18:52:46 +053024class NegativeStockError(frappe.ValidationError):
25 pass
26
27
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053028class SerialNoExistsInFutureTransaction(frappe.ValidationError):
29 pass
Nabin Hait902e8602013-01-08 18:29:24 +053030
Anand Doshi5b004ff2013-09-25 19:55:41 +053031
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053032def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053033 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053034
Ankush Menat494bd9e2022-03-28 18:52:46 +053035 args:
36 - allow_negative_stock: disable negative stock valiations if true
37 - via_landed_cost_voucher: landed cost voucher cancels and reposts
38 entries of purchase document. This flag is used to identify if
39 cancellation and repost is happening via landed cost voucher, in
40 such cases certain validations need to be ignored (like negative
41 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053042 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053043 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053044
Nabin Haitca775742013-09-26 16:16:44 +053045 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053046 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053047 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053048 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053049 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053050
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053051 args = get_args_for_future_sle(sl_entries[0])
52 future_sle_exists(args, sl_entries)
53
Nabin Haitca775742013-09-26 16:16:44 +053054 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053055 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053056 validate_serial_no(sle)
57
Nabin Haita77b8c92020-12-21 14:45:50 +053058 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053059 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053060
Ankush Menat494bd9e2022-03-28 18:52:46 +053061 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
62 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
63 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
64 )
65 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053066
Ankush Menat494bd9e2022-03-28 18:52:46 +053067 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
68 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
69 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
70 )
71 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053072
Ankush Menat494bd9e2022-03-28 18:52:46 +053073 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053074 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053075
Nabin Haita77b8c92020-12-21 14:45:50 +053076 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053077
78 if sle.get("voucher_type") == "Stock Reconciliation":
79 # preserve previous_qty_after_transaction for qty reposting
80 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
81
Ankush Menat494bd9e2022-03-28 18:52:46 +053082 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053083 if is_stock_item:
84 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053085 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053086 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053087 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +053088 frappe.msgprint(
89 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
90 )
91
Ankush Menatcef84c22021-12-03 12:18:59 +053092
93def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
94 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
95 if not args.get("posting_date"):
96 args["posting_date"] = nowdate()
97
marination7a5fd712022-07-04 17:46:54 +053098 if not (args.get("is_cancelled") and via_landed_cost_voucher):
99 # Reposts only current voucher SL Entries
100 # Updates valuation rate, stock value, stock queue for current transaction
101 update_entries_after(
102 {
103 "item_code": args.get("item_code"),
104 "warehouse": args.get("warehouse"),
105 "posting_date": args.get("posting_date"),
106 "posting_time": args.get("posting_time"),
107 "voucher_type": args.get("voucher_type"),
108 "voucher_no": args.get("voucher_no"),
109 "sle_id": args.get("name"),
110 "creation": args.get("creation"),
111 },
112 allow_negative_stock=allow_negative_stock,
113 via_landed_cost_voucher=via_landed_cost_voucher,
114 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530115
116 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530117 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530118 update_qty_in_future_sle(args, allow_negative_stock)
119
Nabin Haitadeb9762014-10-06 11:53:52 +0530120
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530121def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530122 return frappe._dict(
123 {
124 "voucher_type": row.get("voucher_type"),
125 "voucher_no": row.get("voucher_no"),
126 "posting_date": row.get("posting_date"),
127 "posting_time": row.get("posting_time"),
128 }
129 )
130
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530131
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530132def validate_serial_no(sle):
133 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530134
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530135 for sn in get_serial_nos(sle.serial_no):
136 args = copy.deepcopy(sle)
137 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530138 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530139
140 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530141 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530142 voucher_type = frappe.bold(row.voucher_type)
143 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530144 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530145
146 if vouchers:
147 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530148 msg = (
149 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
150 The list of the transactions are as below."""
151 + "<br><br><ul><li>"
152 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530153
Ankush Menat494bd9e2022-03-28 18:52:46 +0530154 msg += "</li><li>".join(vouchers)
155 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530156
Ankush Menat494bd9e2022-03-28 18:52:46 +0530157 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530158 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
159
Ankush Menat494bd9e2022-03-28 18:52:46 +0530160
Nabin Hait186a0452021-02-18 14:14:21 +0530161def validate_cancellation(args):
162 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163 repost_entry = frappe.db.get_value(
164 "Repost Item Valuation",
165 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
166 ["name", "status"],
167 as_dict=1,
168 )
Nabin Hait186a0452021-02-18 14:14:21 +0530169
170 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530171 if repost_entry.status == "In Progress":
172 frappe.throw(
173 _(
174 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
175 )
176 )
177 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530178 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530179 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530180 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530181 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530182
Ankush Menat494bd9e2022-03-28 18:52:46 +0530183
Nabin Hait9653f602013-08-20 15:37:33 +0530184def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530185 frappe.db.sql(
186 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530187 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530188 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530189 (now(), frappe.session.user, voucher_type, voucher_no),
190 )
191
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530192
Nabin Hait54c865e2015-03-27 15:38:31 +0530193def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530194 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530195 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530196 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530197 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530198 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530199 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530200 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530201
Ankush Menat494bd9e2022-03-28 18:52:46 +0530202
203def repost_future_sle(
204 args=None,
205 voucher_type=None,
206 voucher_no=None,
207 allow_negative_stock=None,
208 via_landed_cost_voucher=False,
209 doc=None,
210):
Nabin Haite1fa7232022-07-20 15:19:09 +0530211 if not args:
212 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530213
214 items_to_be_repost = get_items_to_be_repost(
215 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
216 )
217 if items_to_be_repost:
218 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530219
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530220 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Ankush Menatecdb4932022-04-17 19:06:13 +0530221 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530222
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530223 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530224 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530225 validate_item_warehouse(args[i])
226
Ankush Menat494bd9e2022-03-28 18:52:46 +0530227 obj = update_entries_after(
228 {
229 "item_code": args[i].get("item_code"),
230 "warehouse": args[i].get("warehouse"),
231 "posting_date": args[i].get("posting_date"),
232 "posting_time": args[i].get("posting_time"),
233 "creation": args[i].get("creation"),
234 "distinct_item_warehouses": distinct_item_warehouses,
235 },
236 allow_negative_stock=allow_negative_stock,
237 via_landed_cost_voucher=via_landed_cost_voucher,
238 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530239 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530240
Ankush Menat494bd9e2022-03-28 18:52:46 +0530241 distinct_item_warehouses[
242 (args[i].get("item_code"), args[i].get("warehouse"))
243 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530244
Nabin Hait97bce3a2021-07-12 13:24:43 +0530245 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530246 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530247 if ("args_idx" not in data and not data.reposting_status) or (
248 data.sle_changed and data.reposting_status
249 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530250 data.args_idx = len(args)
251 args.append(data.sle)
252 elif data.sle_changed and not data.reposting_status:
253 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530254
Nabin Hait97bce3a2021-07-12 13:24:43 +0530255 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530256 i += 1
257
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530258 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530259 update_args_in_repost_item_valuation(
260 doc, i, args, distinct_item_warehouses, affected_transactions
261 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530262
Ankush Menat494bd9e2022-03-28 18:52:46 +0530263
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530264def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530265 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530266 if not args.get(field):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530267 validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530268 frappe.throw(_(validation_msg))
269
Ankush Menat494bd9e2022-03-28 18:52:46 +0530270
Ankush Menatecdb4932022-04-17 19:06:13 +0530271def update_args_in_repost_item_valuation(
272 doc, index, args, distinct_item_warehouses, affected_transactions
273):
Ankush Menatecdb4932022-04-17 19:06:13 +0530274 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530275 {
276 "items_to_be_repost": json.dumps(args, default=str),
277 "distinct_item_and_warehouse": json.dumps(
278 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
279 ),
280 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530281 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530282 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530283 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530284
Ankush Menatecdb4932022-04-17 19:06:13 +0530285 if not frappe.flags.in_test:
286 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530287
Ankush Menat494bd9e2022-03-28 18:52:46 +0530288 frappe.publish_realtime(
289 "item_reposting_progress",
290 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
291 )
292
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530293
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530294def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
295 items_to_be_repost = []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530297 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530298
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530299 if not items_to_be_repost and voucher_type and voucher_no:
300 items_to_be_repost = frappe.db.get_all(
301 "Stock Ledger Entry",
302 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
303 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
304 order_by="creation asc",
305 group_by="item_code, warehouse",
306 )
307
Nabin Haite1fa7232022-07-20 15:19:09 +0530308 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530309
Ankush Menat494bd9e2022-03-28 18:52:46 +0530310
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530311def get_distinct_item_warehouse(args=None, doc=None):
312 distinct_item_warehouses = {}
313 if doc and doc.distinct_item_and_warehouse:
314 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530315 distinct_item_warehouses = {
316 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
317 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530318 else:
319 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530320 distinct_item_warehouses.setdefault(
321 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
322 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530323
324 return distinct_item_warehouses
325
Ankush Menat494bd9e2022-03-28 18:52:46 +0530326
Ankush Menatecdb4932022-04-17 19:06:13 +0530327def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
328 if not doc.affected_transactions:
329 return set()
330
331 transactions = frappe.parse_json(doc.affected_transactions)
332 return {tuple(transaction) for transaction in transactions}
333
334
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530335def get_current_index(doc=None):
336 if doc and doc.current_index:
337 return doc.current_index
338
Ankush Menat494bd9e2022-03-28 18:52:46 +0530339
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530340class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530341 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530342 update valution rate and qty after transaction
343 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530344
Ankush Menat494bd9e2022-03-28 18:52:46 +0530345 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530346
Ankush Menat494bd9e2022-03-28 18:52:46 +0530347 args = {
348 "item_code": "ABC",
349 "warehouse": "XYZ",
350 "posting_date": "2012-12-12",
351 "posting_time": "12:00"
352 }
Nabin Hait902e8602013-01-08 18:29:24 +0530353 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530354
355 def __init__(
356 self,
357 args,
358 allow_zero_rate=False,
359 allow_negative_stock=None,
360 via_landed_cost_voucher=False,
361 verbose=1,
362 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530363 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530364 self.verbose = verbose
365 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530366 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530367 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530368 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
369 item_code=self.item_code
370 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530371
Nabin Haita77b8c92020-12-21 14:45:50 +0530372 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530373 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530374 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530375
Nabin Haita77b8c92020-12-21 14:45:50 +0530376 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530377 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530378 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530379
380 self.new_items_found = False
381 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530382 self.affected_transactions: Set[Tuple[str, str]] = set()
Nabin Haita77b8c92020-12-21 14:45:50 +0530383
384 self.data = frappe._dict()
385 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530386 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530387
Maricad6078aa2022-06-17 15:13:13 +0530388 def set_precision(self):
389 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
390 self.currency_precision = get_field_precision(
391 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530392 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530393
394 def initialize_previous_data(self, args):
395 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530396 Get previous sl entries for current item for each related warehouse
397 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530398
Ankush Menat494bd9e2022-03-28 18:52:46 +0530399 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530400
Ankush Menat494bd9e2022-03-28 18:52:46 +0530401 self.data = {
402 warehouse1: {
403 'previus_sle': {},
404 'qty_after_transaction': 10,
405 'valuation_rate': 100,
406 'stock_value': 1000,
407 'prev_stock_value': 1000,
408 'stock_queue': '[[10, 100]]',
409 'stock_value_difference': 1000
410 }
411 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530412
413 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530414 self.data.setdefault(args.warehouse, frappe._dict())
415 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530416 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530417 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530418
Ankush Menatc1d986a2021-08-31 19:43:42 +0530419 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
420 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
421
Ankush Menat494bd9e2022-03-28 18:52:46 +0530422 warehouse_dict.update(
423 {
424 "prev_stock_value": previous_sle.stock_value or 0.0,
425 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
426 "stock_value_difference": 0.0,
427 }
428 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530429
Nabin Haita77b8c92020-12-21 14:45:50 +0530430 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530431 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530432
Nabin Haita77b8c92020-12-21 14:45:50 +0530433 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530434 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530435 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530436 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530437 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530438 entries_to_fix = self.get_future_entries_to_fix()
439
440 i = 0
441 while i < len(entries_to_fix):
442 sle = entries_to_fix[i]
443 i += 1
444
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530445 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530446
Nabin Haita77b8c92020-12-21 14:45:50 +0530447 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530448 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530449
Nabin Hait186a0452021-02-18 14:14:21 +0530450 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530451
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530452 if self.exceptions:
453 self.raise_exceptions()
454
Nabin Hait186a0452021-02-18 14:14:21 +0530455 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530456 sl_entries = self.get_sle_against_current_voucher()
457 for sle in sl_entries:
458 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530459
Nabin Haita77b8c92020-12-21 14:45:50 +0530460 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530461 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530462
Ankush Menat494bd9e2022-03-28 18:52:46 +0530463 return frappe.db.sql(
464 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530465 select
466 *, timestamp(posting_date, posting_time) as "timestamp"
467 from
468 `tabStock Ledger Entry`
469 where
470 item_code = %(item_code)s
471 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530472 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530473 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
474
Nabin Haita77b8c92020-12-21 14:45:50 +0530475 order by
476 creation ASC
477 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530478 """,
479 self.args,
480 as_dict=1,
481 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530482
Nabin Haita77b8c92020-12-21 14:45:50 +0530483 def get_future_entries_to_fix(self):
484 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530485 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
486 {"item_code": self.item_code, "warehouse": self.args.warehouse}
487 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530488
Nabin Haita77b8c92020-12-21 14:45:50 +0530489 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530490
Nabin Haita77b8c92020-12-21 14:45:50 +0530491 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530492 dependant_sle = get_sle_by_voucher_detail_no(
493 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
494 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530495
Nabin Haita77b8c92020-12-21 14:45:50 +0530496 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530497 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530498 elif (
499 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
500 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530501 return entries_to_fix
502 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530503 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530504 return entries_to_fix
505 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
506 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530507 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530508 self.initialize_previous_data(dependant_sle)
509 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530510 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530511
512 def update_distinct_item_warehouses(self, dependant_sle):
513 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530514 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530515 if key not in self.distinct_item_warehouses:
516 self.distinct_item_warehouses[key] = val
517 self.new_items_found = True
518 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530519 existing_sle_posting_date = (
520 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
521 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530522 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
523 val.sle_changed = True
524 self.distinct_item_warehouses[key] = val
525 self.new_items_found = True
526
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530527 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530528 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
529
Nabin Haita77b8c92020-12-21 14:45:50 +0530530 # previous sle data for this warehouse
531 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530532 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530533
Anand Doshi0dc79f42015-04-06 12:59:34 +0530534 if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530535 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530536 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530537 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530538 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530539 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530540
Nabin Haita77b8c92020-12-21 14:45:50 +0530541 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530542 if not self.args.get("sle_id"):
543 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530544
Ankush Menat66bf21f2022-01-16 20:45:59 +0530545 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530546 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530547 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530548 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530549 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530550
Ankush Menat494bd9e2022-03-28 18:52:46 +0530551 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
552 self.wh_data.valuation_rate
553 )
554 elif sle.batch_no and frappe.db.get_value(
555 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
556 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530557 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530558 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530559 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530560 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530561 self.wh_data.valuation_rate = sle.valuation_rate
562 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530563 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
564 self.wh_data.valuation_rate
565 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530566 if self.valuation_method != "Moving Average":
567 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530568 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530569 if self.valuation_method == "Moving Average":
570 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530571 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530572 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
573 self.wh_data.valuation_rate
574 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530575 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530576 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530577
Rushabh Mehta54047782013-12-26 11:07:46 +0530578 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530579 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530580 if not self.wh_data.qty_after_transaction:
581 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530582 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
583 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530584
Nabin Hait902e8602013-01-08 18:29:24 +0530585 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530586 sle.qty_after_transaction = self.wh_data.qty_after_transaction
587 sle.valuation_rate = self.wh_data.valuation_rate
588 sle.stock_value = self.wh_data.stock_value
589 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530590 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530591 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530592 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530593
Ankush Menat701878f2022-03-01 18:08:29 +0530594 if not self.args.get("sle_id"):
595 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530596
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530597 def validate_negative_stock(self, sle):
598 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530599 validate negative stock for entries current datetime onwards
600 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530601 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530602 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Maricad6078aa2022-06-17 15:13:13 +0530603 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530604
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530605 if diff < 0 and abs(diff) > 0.0001:
606 # negative stock!
607 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530608 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530609 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530610 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530611 return True
612
Nabin Haita77b8c92020-12-21 14:45:50 +0530613 def get_dynamic_incoming_outgoing_rate(self, sle):
614 # Get updated incoming/outgoing rate from transaction
615 if sle.recalculate_rate:
616 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
617
618 if flt(sle.actual_qty) >= 0:
619 sle.incoming_rate = rate
620 else:
621 sle.outgoing_rate = rate
622
623 def get_incoming_outgoing_rate_from_transaction(self, sle):
624 rate = 0
625 # Material Transfer, Repack, Manufacturing
626 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530627 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530628 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
629 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530630 elif sle.voucher_type in (
631 "Purchase Receipt",
632 "Purchase Invoice",
633 "Delivery Note",
634 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530635 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530636 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530637 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530638 from erpnext.controllers.sales_and_purchase_return import (
639 get_rate_for_return, # don't move this import to top
640 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530641
642 rate = get_rate_for_return(
643 sle.voucher_type,
644 sle.voucher_no,
645 sle.item_code,
646 voucher_detail_no=sle.voucher_detail_no,
647 sle=sle,
648 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530649
650 elif (
651 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
652 and sle.actual_qty > 0
653 and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
654 ):
655 sle_details = frappe.db.get_value(
656 "Stock Ledger Entry",
657 {
658 "voucher_type": sle.voucher_type,
659 "voucher_no": sle.voucher_no,
660 "dependant_sle_voucher_detail_no": sle.voucher_detail_no,
661 },
662 ["stock_value_difference", "actual_qty"],
663 as_dict=1,
664 )
665
666 rate = abs(sle_details.stock_value_difference / sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530667 else:
668 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530669 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530670 elif sle.voucher_type == "Subcontracting Receipt":
671 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530672 else:
673 rate_field = "incoming_rate"
674
675 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530676 item_code, incoming_rate = frappe.db.get_value(
677 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
678 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530679
680 if item_code == sle.item_code:
681 rate = incoming_rate
682 else:
683 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
684 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530685 elif sle == "Subcontracting Receipt":
686 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530687 else:
688 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530689
Ankush Menat494bd9e2022-03-28 18:52:46 +0530690 rate = frappe.db.get_value(
691 ref_doctype,
692 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
693 rate_field,
694 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530695
696 return rate
697
698 def update_outgoing_rate_on_transaction(self, sle):
699 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530700 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
701 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530702 """
703 if sle.actual_qty and sle.voucher_detail_no:
704 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
705
706 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
707 self.update_rate_on_stock_entry(sle, outgoing_rate)
708 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
709 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
710 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
711 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530712 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
713 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530714
715 def update_rate_on_stock_entry(self, sle, outgoing_rate):
716 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
717
Ankush Menat701878f2022-03-01 18:08:29 +0530718 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
719 if not sle.dependant_sle_voucher_detail_no:
720 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530721
722 def recalculate_amounts_in_stock_entry(self, voucher_no):
723 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530724 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
725 stock_entry.db_update()
726 for d in stock_entry.items:
727 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530728
Nabin Haita77b8c92020-12-21 14:45:50 +0530729 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
730 # Update item's incoming rate on transaction
731 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
732 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530733 frappe.db.set_value(
734 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
735 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530736 else:
737 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530738 frappe.db.set_value(
739 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530740 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530741 "incoming_rate",
742 outgoing_rate,
743 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530744
745 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
746 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530747 frappe.db.set_value(
748 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
749 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530750 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530751 frappe.db.set_value(
752 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
753 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530754
755 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530756 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530757 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530758 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530759 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530760 d.db_update()
761
Sagar Sharma323bdf82022-05-17 15:14:07 +0530762 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
763 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
764 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
765 else:
766 frappe.db.set_value(
767 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
768 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530769
770 def get_serialized_values(self, sle):
771 incoming_rate = flt(sle.incoming_rate)
772 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530773 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530774
775 if incoming_rate < 0:
776 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530777 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530778
Nabin Hait2620bf42016-02-29 11:30:27 +0530779 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530780 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530781 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530782 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530783 # In case of delivery/stock issue, get average purchase rate
784 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530785 if not sle.is_cancelled:
786 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
787 stock_value_change = -1 * outgoing_value
788 else:
789 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530790
Nabin Haita77b8c92020-12-21 14:45:50 +0530791 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530792
Nabin Hait2620bf42016-02-29 11:30:27 +0530793 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530794 new_stock_value = (
795 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
796 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530797 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530798 # calculate new valuation rate only if stock value is positive
799 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530800 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530801
Nabin Haita77b8c92020-12-21 14:45:50 +0530802 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530803 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
804 sle.voucher_type, sle.voucher_detail_no
805 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530806 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530807 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530808
Nabin Hait328c4f92020-01-02 19:00:32 +0530809 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
810 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530811 all_serial_nos = frappe.get_all(
812 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
813 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530814
Ankush Menat494bd9e2022-03-28 18:52:46 +0530815 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 +0530816
817 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530818 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530819 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530820 incoming_rate = frappe.db.sql(
821 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530822 select incoming_rate
823 from `tabStock Ledger Entry`
824 where
825 company = %s
826 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530827 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530828 and (serial_no = %s
829 or serial_no like %s
830 or serial_no like %s
831 or serial_no like %s
832 )
833 order by posting_date desc
834 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530835 """,
836 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
837 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530838
839 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
840
841 return incoming_values
842
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530843 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530844 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530845 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530846 if new_stock_qty >= 0:
847 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530848 if flt(self.wh_data.qty_after_transaction) <= 0:
849 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530850 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530851 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
852 actual_qty * sle.incoming_rate
853 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530854
Nabin Haita77b8c92020-12-21 14:45:50 +0530855 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530856
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530857 elif sle.outgoing_rate:
858 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530859 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
860 actual_qty * sle.outgoing_rate
861 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530862
Nabin Haita77b8c92020-12-21 14:45:50 +0530863 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530864 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530865 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530866 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530867 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
868 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530869
Nabin Haita77b8c92020-12-21 14:45:50 +0530870 if not self.wh_data.valuation_rate and actual_qty > 0:
871 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530872
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530873 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800874 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530875 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530876 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
877 sle.voucher_type, sle.voucher_detail_no
878 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800879 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530880 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530881
Ankush Menatf089d392022-02-02 12:51:21 +0530882 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530883 incoming_rate = flt(sle.incoming_rate)
884 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530885 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530886
Ankush Menat494bd9e2022-03-28 18:52:46 +0530887 self.wh_data.qty_after_transaction = round_off_if_near_zero(
888 self.wh_data.qty_after_transaction + actual_qty
889 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530890
Ankush Menat97e18a12022-01-15 17:42:25 +0530891 if self.valuation_method == "LIFO":
892 stock_queue = LIFOValuation(self.wh_data.stock_queue)
893 else:
894 stock_queue = FIFOValuation(self.wh_data.stock_queue)
895
Ankush Menatb534fee2022-02-19 20:58:36 +0530896 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
897
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530898 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530899 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530900 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530901
Ankush Menat4b29fb62021-12-18 18:40:22 +0530902 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530903 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
904 sle.voucher_type, sle.voucher_detail_no
905 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530906 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530907 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530908 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530909 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530910
Ankush Menat494bd9e2022-03-28 18:52:46 +0530911 stock_queue.remove_stock(
912 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
913 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530914
Ankush Menatb534fee2022-02-19 20:58:36 +0530915 _qty, stock_value = stock_queue.get_total_stock_and_value()
916
917 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530918
Ankush Menat97e18a12022-01-15 17:42:25 +0530919 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530920 self.wh_data.stock_value = round_off_if_near_zero(
921 self.wh_data.stock_value + stock_value_difference
922 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530923
Nabin Haita77b8c92020-12-21 14:45:50 +0530924 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530925 self.wh_data.stock_queue.append(
926 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
927 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530928
Ankush Menatb534fee2022-02-19 20:58:36 +0530929 if self.wh_data.qty_after_transaction:
930 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
931
Ankush Menatce0514c2022-02-15 11:41:41 +0530932 def update_batched_values(self, sle):
933 incoming_rate = flt(sle.incoming_rate)
934 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530935
Ankush Menat494bd9e2022-03-28 18:52:46 +0530936 self.wh_data.qty_after_transaction = round_off_if_near_zero(
937 self.wh_data.qty_after_transaction + actual_qty
938 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530939
940 if actual_qty > 0:
941 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530942 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530943 outgoing_rate = get_batch_incoming_rate(
944 item_code=sle.item_code,
945 warehouse=sle.warehouse,
946 batch_no=sle.batch_no,
947 posting_date=sle.posting_date,
948 posting_time=sle.posting_time,
949 creation=sle.creation,
950 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530951 if outgoing_rate is None:
952 # This can *only* happen if qty available for the batch is zero.
953 # in such case fall back various other rates.
954 # future entries will correct the overall accounting as each
955 # batch individually uses moving average rates.
956 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530957 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530958
Ankush Menat494bd9e2022-03-28 18:52:46 +0530959 self.wh_data.stock_value = round_off_if_near_zero(
960 self.wh_data.stock_value + stock_value_difference
961 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530962 if self.wh_data.qty_after_transaction:
963 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530964
Javier Wong9b11d9b2017-04-14 18:24:04 +0800965 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530966 ref_item_dt = ""
967
968 if voucher_type == "Stock Entry":
969 ref_item_dt = voucher_type + " Detail"
970 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
971 ref_item_dt = voucher_type + " Item"
972
973 if ref_item_dt:
974 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
975 else:
976 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530977
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530978 def get_fallback_rate(self, sle) -> float:
979 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530980 This should only get used for negative stock."""
981 return get_valuation_rate(
982 sle.item_code,
983 sle.warehouse,
984 sle.voucher_type,
985 sle.voucher_no,
986 self.allow_zero_rate,
987 currency=erpnext.get_company_currency(sle.company),
988 company=sle.company,
989 batch_no=sle.batch_no,
990 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530991
Nabin Haita77b8c92020-12-21 14:45:50 +0530992 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530993 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530994 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
995 sle = sle[0] if sle else frappe._dict()
996 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530997
Nabin Haita77b8c92020-12-21 14:45:50 +0530998 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530999 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301000 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301001
1002 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301003 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301004 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301005 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301006
Ankush Menat494bd9e2022-03-28 18:52:46 +05301007 if (
1008 exceptions[0]["voucher_type"],
1009 exceptions[0]["voucher_no"],
1010 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301011
Nabin Haita77b8c92020-12-21 14:45:50 +05301012 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301013 abs(deficiency),
1014 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1015 frappe.get_desk_link("Warehouse", warehouse),
1016 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301017 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301018 msg = _(
1019 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1020 ).format(
1021 abs(deficiency),
1022 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1023 frappe.get_desk_link("Warehouse", warehouse),
1024 exceptions[0]["posting_date"],
1025 exceptions[0]["posting_time"],
1026 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1027 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301028
Nabin Haita77b8c92020-12-21 14:45:50 +05301029 if msg:
1030 msg_list.append(msg)
1031
1032 if msg_list:
1033 message = "\n\n".join(msg_list)
1034 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301035 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301036 else:
1037 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301038
Nabin Haita77b8c92020-12-21 14:45:50 +05301039 def update_bin(self):
1040 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301041 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301042 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301043
Ankush Menat494bd9e2022-03-28 18:52:46 +05301044 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301045 if data.valuation_rate is not None:
1046 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301047 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301048
marination8418c4b2021-06-22 21:35:25 +05301049
1050def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1051 """get stock ledger entries filtered by specific posting datetime conditions"""
1052
Ankush Menat494bd9e2022-03-28 18:52:46 +05301053 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301054 if not args.get("posting_date"):
1055 args["posting_date"] = "1900-01-01"
1056 if not args.get("posting_time"):
1057 args["posting_time"] = "00:00"
1058
1059 voucher_condition = ""
1060 if exclude_current_voucher:
1061 voucher_no = args.get("voucher_no")
1062 voucher_condition = f"and voucher_no != '{voucher_no}'"
1063
Ankush Menat494bd9e2022-03-28 18:52:46 +05301064 sle = frappe.db.sql(
1065 """
marination8418c4b2021-06-22 21:35:25 +05301066 select *, timestamp(posting_date, posting_time) as "timestamp"
1067 from `tabStock Ledger Entry`
1068 where item_code = %(item_code)s
1069 and warehouse = %(warehouse)s
1070 and is_cancelled = 0
1071 {voucher_condition}
1072 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1073 order by timestamp(posting_date, posting_time) desc, creation desc
1074 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301075 for update""".format(
1076 voucher_condition=voucher_condition
1077 ),
1078 args,
1079 as_dict=1,
1080 )
marination8418c4b2021-06-22 21:35:25 +05301081
1082 return sle[0] if sle else frappe._dict()
1083
Ankush Menat494bd9e2022-03-28 18:52:46 +05301084
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301085def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301086 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301087 get the last sle on or before the current time-bucket,
1088 to get actual qty before transaction, this function
1089 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301090
Ankush Menat494bd9e2022-03-28 18:52:46 +05301091 args = {
1092 "item_code": "ABC",
1093 "warehouse": "XYZ",
1094 "posting_date": "2012-12-12",
1095 "posting_time": "12:00",
1096 "sle": "name of reference Stock Ledger Entry"
1097 }
Anand Doshi1b531862013-01-10 19:29:51 +05301098 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301099 args["name"] = args.get("sle", None) or ""
1100 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301101 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301102
Ankush Menat494bd9e2022-03-28 18:52:46 +05301103
1104def get_stock_ledger_entries(
1105 previous_sle,
1106 operator=None,
1107 order="desc",
1108 limit=None,
1109 for_update=False,
1110 debug=False,
1111 check_serial_no=True,
1112):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301113 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301114 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1115 operator
1116 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301117 if previous_sle.get("warehouse"):
1118 conditions += " and warehouse = %(warehouse)s"
1119 elif previous_sle.get("warehouse_condition"):
1120 conditions += " and " + previous_sle.get("warehouse_condition")
1121
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301122 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301123 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1124 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301125 conditions += (
1126 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301127 (
1128 serial_no = {0}
1129 or serial_no like {1}
1130 or serial_no like {2}
1131 or serial_no like {3}
1132 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301133 """
1134 ).format(
1135 frappe.db.escape(serial_no),
1136 frappe.db.escape("{}\n%".format(serial_no)),
1137 frappe.db.escape("%\n{}".format(serial_no)),
1138 frappe.db.escape("%\n{}\n%".format(serial_no)),
1139 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301140
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301141 if not previous_sle.get("posting_date"):
1142 previous_sle["posting_date"] = "1900-01-01"
1143 if not previous_sle.get("posting_time"):
1144 previous_sle["posting_time"] = "00:00"
1145
1146 if operator in (">", "<=") and previous_sle.get("name"):
1147 conditions += " and name!=%(name)s"
1148
Ankush Menat494bd9e2022-03-28 18:52:46 +05301149 return frappe.db.sql(
1150 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301151 select *, timestamp(posting_date, posting_time) as "timestamp"
1152 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301153 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301154 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301155 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301156 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301157 %(limit)s %(for_update)s"""
1158 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301159 "conditions": conditions,
1160 "limit": limit or "",
1161 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301162 "order": order,
1163 },
1164 previous_sle,
1165 as_dict=1,
1166 debug=debug,
1167 )
1168
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301169
Nabin Haita77b8c92020-12-21 14:45:50 +05301170def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301171 return frappe.db.get_value(
1172 "Stock Ledger Entry",
1173 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1174 [
1175 "item_code",
1176 "warehouse",
1177 "posting_date",
1178 "posting_time",
1179 "timestamp(posting_date, posting_time) as timestamp",
1180 ],
1181 as_dict=1,
1182 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301183
Ankush Menatce0514c2022-02-15 11:41:41 +05301184
Ankush Menat494bd9e2022-03-28 18:52:46 +05301185def get_batch_incoming_rate(
1186 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1187):
1188
Ankush Menat102fff22022-02-19 15:51:04 +05301189 sle = frappe.qb.DocType("Stock Ledger Entry")
1190
Ankush Menate1c16872022-04-21 20:01:48 +05301191 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301192 posting_date, posting_time
1193 )
Ankush Menat102fff22022-02-19 15:51:04 +05301194 if creation:
1195 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301196 CombineDatetime(sle.posting_date, sle.posting_time)
1197 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301198 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301199
1200 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301201 frappe.qb.from_(sle)
1202 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1203 .where(
1204 (sle.item_code == item_code)
1205 & (sle.warehouse == warehouse)
1206 & (sle.batch_no == batch_no)
1207 & (sle.is_cancelled == 0)
1208 )
1209 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301210 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301211
1212 if batch_details and batch_details[0].batch_qty:
1213 return batch_details[0].batch_value / batch_details[0].batch_qty
1214
1215
Ankush Menat494bd9e2022-03-28 18:52:46 +05301216def get_valuation_rate(
1217 item_code,
1218 warehouse,
1219 voucher_type,
1220 voucher_no,
1221 allow_zero_rate=False,
1222 currency=None,
1223 company=None,
1224 raise_error_if_no_rate=True,
1225 batch_no=None,
1226):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301227
Ankush Menatf7ffe042021-11-01 13:21:14 +05301228 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301229 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301230
Ankush Menat342d09a2022-02-19 14:28:51 +05301231 last_valuation_rate = None
1232
1233 # Get moving average rate of a specific batch number
1234 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301235 last_valuation_rate = frappe.db.sql(
1236 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301237 select sum(stock_value_difference) / sum(actual_qty)
1238 from `tabStock Ledger Entry`
1239 where
1240 item_code = %s
1241 AND warehouse = %s
1242 AND batch_no = %s
1243 AND is_cancelled = 0
1244 AND NOT (voucher_no = %s AND voucher_type = %s)
1245 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301246 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1247 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301248
Ankush Menatf7ffe042021-11-01 13:21:14 +05301249 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301250 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301251 last_valuation_rate = frappe.db.sql(
1252 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301253 from `tabStock Ledger Entry` force index (item_warehouse)
1254 where
1255 item_code = %s
1256 AND warehouse = %s
1257 AND valuation_rate >= 0
1258 AND is_cancelled = 0
1259 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301260 order by posting_date desc, posting_time desc, name desc limit 1""",
1261 (item_code, warehouse, voucher_no, voucher_type),
1262 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301263
1264 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301265 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301266 last_valuation_rate = frappe.db.sql(
1267 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301268 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301269 where
1270 item_code = %s
1271 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301272 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301273 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301274 order by posting_date desc, posting_time desc, name desc limit 1""",
1275 (item_code, voucher_no, voucher_type),
1276 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301277
Nabin Haita645f362018-03-01 10:31:24 +05301278 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301279 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301280
1281 # If negative stock allowed, and item delivered without any incoming entry,
1282 # system does not found any SLE, then take valuation rate from Item
1283 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301284
1285 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301286 # try Item Standard rate
1287 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301288
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301289 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301290 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301291 valuation_rate = frappe.db.get_value(
1292 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1293 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301294
Ankush Menat494bd9e2022-03-28 18:52:46 +05301295 if (
1296 not allow_zero_rate
1297 and not valuation_rate
1298 and raise_error_if_no_rate
1299 and cint(erpnext.is_perpetual_inventory_enabled(company))
1300 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301301 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301302
Ankush Menat494bd9e2022-03-28 18:52:46 +05301303 message = _(
1304 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1305 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301306 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301307 solutions = (
1308 "<li>"
1309 + _(
1310 "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."
1311 ).format(voucher_type)
1312 + "</li>"
1313 )
1314 solutions += (
1315 "<li>"
1316 + _("If not, you can Cancel / Submit this entry")
1317 + " {0} ".format(frappe.bold("after"))
1318 + _("performing either one below:")
1319 + "</li>"
1320 )
Marica97715f22020-05-11 20:45:37 +05301321 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1322 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1323 msg = message + solutions + sub_solutions + "</li>"
1324
1325 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301326
1327 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301328
Ankush Menat494bd9e2022-03-28 18:52:46 +05301329
Ankush Menate7109c12021-08-26 16:40:45 +05301330def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301331 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301332 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301333 qty_shift = args.actual_qty
1334
Ankush Menat7c839c42022-05-06 12:09:08 +05301335 args["time_format"] = "%H:%i:%s"
1336
marination8418c4b2021-06-22 21:35:25 +05301337 # find difference/shift in qty caused by stock reconciliation
1338 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301339 qty_shift = get_stock_reco_qty_shift(args)
1340
1341 # find the next nearest stock reco so that we only recalculate SLEs till that point
1342 next_stock_reco_detail = get_next_stock_reco(args)
1343 if next_stock_reco_detail:
1344 detail = next_stock_reco_detail[0]
1345 # add condition to update SLEs before this date & time
1346 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301347
Ankush Menat494bd9e2022-03-28 18:52:46 +05301348 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301349 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301350 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301351 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301352 where
1353 item_code = %(item_code)s
1354 and warehouse = %(warehouse)s
1355 and voucher_no != %(voucher_no)s
1356 and is_cancelled = 0
Ankush Menat7c839c42022-05-06 12:09:08 +05301357 and timestamp(posting_date, time_format(posting_time, %(time_format)s))
1358 > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
marination40389772021-07-02 17:13:45 +05301359 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301360 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301361 args,
1362 )
Nabin Hait186a0452021-02-18 14:14:21 +05301363
1364 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1365
Ankush Menat494bd9e2022-03-28 18:52:46 +05301366
marination40389772021-07-02 17:13:45 +05301367def get_stock_reco_qty_shift(args):
1368 stock_reco_qty_shift = 0
1369 if args.get("is_cancelled"):
1370 if args.get("previous_qty_after_transaction"):
1371 # get qty (balance) that was set at submission
1372 last_balance = args.get("previous_qty_after_transaction")
1373 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1374 else:
1375 stock_reco_qty_shift = flt(args.actual_qty)
1376 else:
1377 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301378 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1379 "qty_after_transaction"
1380 )
marination40389772021-07-02 17:13:45 +05301381
1382 if last_balance is not None:
1383 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1384 else:
1385 stock_reco_qty_shift = args.qty_after_transaction
1386
1387 return stock_reco_qty_shift
1388
Ankush Menat494bd9e2022-03-28 18:52:46 +05301389
marination40389772021-07-02 17:13:45 +05301390def get_next_stock_reco(args):
1391 """Returns next nearest stock reconciliaton's details."""
1392
Ankush Menat494bd9e2022-03-28 18:52:46 +05301393 return frappe.db.sql(
1394 """
marination40389772021-07-02 17:13:45 +05301395 select
1396 name, posting_date, posting_time, creation, voucher_no
1397 from
marination8c441262021-07-02 17:46:05 +05301398 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301399 where
1400 item_code = %(item_code)s
1401 and warehouse = %(warehouse)s
1402 and voucher_type = 'Stock Reconciliation'
1403 and voucher_no != %(voucher_no)s
1404 and is_cancelled = 0
1405 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1406 or (
1407 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1408 and creation > %(creation)s
1409 )
1410 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301411 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301412 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301413 """,
1414 args,
1415 as_dict=1,
1416 )
1417
marination40389772021-07-02 17:13:45 +05301418
1419def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301420 return f"""
1421 and
1422 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1423 or (
1424 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1425 and creation < '{detail.creation}'
1426 )
1427 )"""
1428
Ankush Menat494bd9e2022-03-28 18:52:46 +05301429
Ankush Menate7109c12021-08-26 16:40:45 +05301430def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301431 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301432 return
1433 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1434 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301435
Ankush Menat5eba5752021-12-07 23:03:52 +05301436 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301437
1438 if is_negative_with_precision(neg_sle):
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_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301443 frappe.get_desk_link("Item", args.item_code),
1444 frappe.get_desk_link("Warehouse", args.warehouse),
1445 neg_sle[0]["posting_date"],
1446 neg_sle[0]["posting_time"],
1447 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1448 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301449
Ankush Menat494bd9e2022-03-28 18:52:46 +05301450 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301451
1452 if not args.batch_no:
1453 return
1454
1455 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301456 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301457 message = _(
1458 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1459 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301460 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301461 frappe.get_desk_link("Batch", args.batch_no),
1462 frappe.get_desk_link("Warehouse", args.warehouse),
1463 neg_batch_sle[0]["posting_date"],
1464 neg_batch_sle[0]["posting_time"],
1465 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1466 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301467 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301468
Nabin Haita77b8c92020-12-21 14:45:50 +05301469
Maricad6078aa2022-06-17 15:13:13 +05301470def is_negative_with_precision(neg_sle, is_batch=False):
1471 """
1472 Returns whether system precision rounded qty is insufficient.
1473 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1474 """
1475
1476 if not neg_sle:
1477 return False
1478
1479 field = "cumulative_total" if is_batch else "qty_after_transaction"
1480 precision = cint(frappe.db.get_default("float_precision")) or 2
1481 qty_deficit = flt(neg_sle[0][field], precision)
1482
1483 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1484
1485
Nabin Haita77b8c92020-12-21 14:45:50 +05301486def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301487 return frappe.db.sql(
1488 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301489 select
1490 qty_after_transaction, posting_date, posting_time,
1491 voucher_type, voucher_no
1492 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301493 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301494 item_code = %(item_code)s
1495 and warehouse = %(warehouse)s
1496 and voucher_no != %(voucher_no)s
1497 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1498 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301499 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301500 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301501 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301502 """,
1503 args,
1504 as_dict=1,
1505 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301506
Ankush Menat5eba5752021-12-07 23:03:52 +05301507
1508def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301509 return frappe.db.sql(
1510 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301511 with batch_ledger as (
1512 select
1513 posting_date, posting_time, voucher_type, voucher_no,
1514 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1515 from `tabStock Ledger Entry`
1516 where
1517 item_code = %(item_code)s
1518 and warehouse = %(warehouse)s
1519 and batch_no=%(batch_no)s
1520 and is_cancelled = 0
1521 order by posting_date, posting_time, creation
1522 )
1523 select * from batch_ledger
1524 where
1525 cumulative_total < 0.0
1526 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1527 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301528 """,
1529 args,
1530 as_dict=1,
1531 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301532
1533
1534def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1535 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1536 return True
1537 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1538 return True
1539 return False