blob: fb50b46003adf2198af130418d1ae1ca25dc4504 [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):
s-aga-rba77da02022-11-28 18:01:30 +0530267 validation_msg = f"The field {frappe.unscrub(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
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530545 if (
546 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
547 and sle.voucher_detail_no
548 and sle.actual_qty < 0
549 and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
550 ):
551 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
552
Ankush Menat66bf21f2022-01-16 20:45:59 +0530553 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530554 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530555 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530556 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530557 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530558
Ankush Menat494bd9e2022-03-28 18:52:46 +0530559 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
560 self.wh_data.valuation_rate
561 )
562 elif sle.batch_no and frappe.db.get_value(
563 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
564 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530565 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530566 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530567 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530568 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530569 self.wh_data.valuation_rate = sle.valuation_rate
570 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530571 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
572 self.wh_data.valuation_rate
573 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530574 if self.valuation_method != "Moving Average":
575 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530576 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530577 if self.valuation_method == "Moving Average":
578 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530579 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530580 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
581 self.wh_data.valuation_rate
582 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530583 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530584 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530585
Rushabh Mehta54047782013-12-26 11:07:46 +0530586 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530587 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530588 if not self.wh_data.qty_after_transaction:
589 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530590 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
591 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530592
Nabin Hait902e8602013-01-08 18:29:24 +0530593 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530594 sle.qty_after_transaction = self.wh_data.qty_after_transaction
595 sle.valuation_rate = self.wh_data.valuation_rate
596 sle.stock_value = self.wh_data.stock_value
597 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530598 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530599 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530600
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530601 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530602
Ankush Menat701878f2022-03-01 18:08:29 +0530603 if not self.args.get("sle_id"):
604 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530605
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530606 def validate_negative_stock(self, sle):
607 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530608 validate negative stock for entries current datetime onwards
609 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530610 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530611 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Maricad6078aa2022-06-17 15:13:13 +0530612 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530613
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530614 if diff < 0 and abs(diff) > 0.0001:
615 # negative stock!
616 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530617 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530618 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530619 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530620 return True
621
Nabin Haita77b8c92020-12-21 14:45:50 +0530622 def get_dynamic_incoming_outgoing_rate(self, sle):
623 # Get updated incoming/outgoing rate from transaction
624 if sle.recalculate_rate:
625 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
626
627 if flt(sle.actual_qty) >= 0:
628 sle.incoming_rate = rate
629 else:
630 sle.outgoing_rate = rate
631
632 def get_incoming_outgoing_rate_from_transaction(self, sle):
633 rate = 0
634 # Material Transfer, Repack, Manufacturing
635 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530636 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530637 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
638 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530639 elif sle.voucher_type in (
640 "Purchase Receipt",
641 "Purchase Invoice",
642 "Delivery Note",
643 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530644 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530645 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530646 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530647 from erpnext.controllers.sales_and_purchase_return import (
648 get_rate_for_return, # don't move this import to top
649 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530650
651 rate = get_rate_for_return(
652 sle.voucher_type,
653 sle.voucher_no,
654 sle.item_code,
655 voucher_detail_no=sle.voucher_detail_no,
656 sle=sle,
657 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530658
659 elif (
660 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530661 and sle.voucher_detail_no
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530662 and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
663 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530664 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530665 else:
666 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530667 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530668 elif sle.voucher_type == "Subcontracting Receipt":
669 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530670 else:
671 rate_field = "incoming_rate"
672
673 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530674 item_code, incoming_rate = frappe.db.get_value(
675 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
676 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530677
678 if item_code == sle.item_code:
679 rate = incoming_rate
680 else:
681 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
682 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530683 elif sle == "Subcontracting Receipt":
684 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530685 else:
686 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530687
Ankush Menat494bd9e2022-03-28 18:52:46 +0530688 rate = frappe.db.get_value(
689 ref_doctype,
690 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
691 rate_field,
692 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530693
694 return rate
695
696 def update_outgoing_rate_on_transaction(self, sle):
697 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530698 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
699 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530700 """
701 if sle.actual_qty and sle.voucher_detail_no:
702 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
703
704 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
705 self.update_rate_on_stock_entry(sle, outgoing_rate)
706 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
707 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
708 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
709 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530710 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
711 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530712
713 def update_rate_on_stock_entry(self, sle, outgoing_rate):
714 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
715
Ankush Menat701878f2022-03-01 18:08:29 +0530716 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
717 if not sle.dependant_sle_voucher_detail_no:
718 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530719
720 def recalculate_amounts_in_stock_entry(self, voucher_no):
721 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530722 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
723 stock_entry.db_update()
724 for d in stock_entry.items:
725 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530726
Nabin Haita77b8c92020-12-21 14:45:50 +0530727 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
728 # Update item's incoming rate on transaction
729 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
730 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530731 frappe.db.set_value(
732 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
733 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530734 else:
735 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530736 frappe.db.set_value(
737 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530738 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530739 "incoming_rate",
740 outgoing_rate,
741 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530742
743 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
744 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530745 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
746 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
747 ):
748 frappe.db.set_value(
749 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
750 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530751 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530752 frappe.db.set_value(
753 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
754 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530755
756 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530757 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530758 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530759 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530760 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530761 d.db_update()
762
Sagar Sharma323bdf82022-05-17 15:14:07 +0530763 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
764 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
765 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
766 else:
767 frappe.db.set_value(
768 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
769 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530770
771 def get_serialized_values(self, sle):
772 incoming_rate = flt(sle.incoming_rate)
773 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530774 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530775
776 if incoming_rate < 0:
777 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530778 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530779
Nabin Hait2620bf42016-02-29 11:30:27 +0530780 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530781 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530782 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530783 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530784 # In case of delivery/stock issue, get average purchase rate
785 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530786 if not sle.is_cancelled:
787 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
788 stock_value_change = -1 * outgoing_value
789 else:
790 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530791
Nabin Haita77b8c92020-12-21 14:45:50 +0530792 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530793
Nabin Hait2620bf42016-02-29 11:30:27 +0530794 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530795 new_stock_value = (
796 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
797 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530798 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530799 # calculate new valuation rate only if stock value is positive
800 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530801 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530802
Nabin Haita77b8c92020-12-21 14:45:50 +0530803 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530804 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
805 sle.voucher_type, sle.voucher_detail_no
806 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530807 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530808 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530809
Nabin Hait328c4f92020-01-02 19:00:32 +0530810 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
811 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530812 all_serial_nos = frappe.get_all(
813 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
814 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530815
Ankush Menat494bd9e2022-03-28 18:52:46 +0530816 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 +0530817
818 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530819 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530820 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530821 incoming_rate = frappe.db.sql(
822 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530823 select incoming_rate
824 from `tabStock Ledger Entry`
825 where
826 company = %s
827 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530828 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530829 and (serial_no = %s
830 or serial_no like %s
831 or serial_no like %s
832 or serial_no like %s
833 )
834 order by posting_date desc
835 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530836 """,
837 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
838 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530839
840 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
841
842 return incoming_values
843
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530844 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530845 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530846 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530847 if new_stock_qty >= 0:
848 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530849 if flt(self.wh_data.qty_after_transaction) <= 0:
850 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530851 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530852 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
853 actual_qty * sle.incoming_rate
854 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530855
Nabin Haita77b8c92020-12-21 14:45:50 +0530856 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530857
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530858 elif sle.outgoing_rate:
859 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530860 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
861 actual_qty * sle.outgoing_rate
862 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530863
Nabin Haita77b8c92020-12-21 14:45:50 +0530864 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530865 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530866 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530867 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530868 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
869 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530870
Nabin Haita77b8c92020-12-21 14:45:50 +0530871 if not self.wh_data.valuation_rate and actual_qty > 0:
872 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530873
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530874 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800875 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530876 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530877 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
878 sle.voucher_type, sle.voucher_detail_no
879 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800880 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530881 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530882
Ankush Menatf089d392022-02-02 12:51:21 +0530883 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530884 incoming_rate = flt(sle.incoming_rate)
885 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530886 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530887
Ankush Menat494bd9e2022-03-28 18:52:46 +0530888 self.wh_data.qty_after_transaction = round_off_if_near_zero(
889 self.wh_data.qty_after_transaction + actual_qty
890 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530891
Ankush Menat97e18a12022-01-15 17:42:25 +0530892 if self.valuation_method == "LIFO":
893 stock_queue = LIFOValuation(self.wh_data.stock_queue)
894 else:
895 stock_queue = FIFOValuation(self.wh_data.stock_queue)
896
Ankush Menatb534fee2022-02-19 20:58:36 +0530897 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
898
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530899 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530900 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530901 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530902
Ankush Menat4b29fb62021-12-18 18:40:22 +0530903 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530904 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
905 sle.voucher_type, sle.voucher_detail_no
906 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530907 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530908 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530909 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530910 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530911
Ankush Menat494bd9e2022-03-28 18:52:46 +0530912 stock_queue.remove_stock(
913 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
914 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530915
Ankush Menatb534fee2022-02-19 20:58:36 +0530916 _qty, stock_value = stock_queue.get_total_stock_and_value()
917
918 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530919
Ankush Menat97e18a12022-01-15 17:42:25 +0530920 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530921 self.wh_data.stock_value = round_off_if_near_zero(
922 self.wh_data.stock_value + stock_value_difference
923 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530924
Nabin Haita77b8c92020-12-21 14:45:50 +0530925 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530926 self.wh_data.stock_queue.append(
927 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
928 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530929
Ankush Menatb534fee2022-02-19 20:58:36 +0530930 if self.wh_data.qty_after_transaction:
931 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
932
Ankush Menatce0514c2022-02-15 11:41:41 +0530933 def update_batched_values(self, sle):
934 incoming_rate = flt(sle.incoming_rate)
935 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530936
Ankush Menat494bd9e2022-03-28 18:52:46 +0530937 self.wh_data.qty_after_transaction = round_off_if_near_zero(
938 self.wh_data.qty_after_transaction + actual_qty
939 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530940
941 if actual_qty > 0:
942 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530943 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530944 outgoing_rate = get_batch_incoming_rate(
945 item_code=sle.item_code,
946 warehouse=sle.warehouse,
947 batch_no=sle.batch_no,
948 posting_date=sle.posting_date,
949 posting_time=sle.posting_time,
950 creation=sle.creation,
951 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530952 if outgoing_rate is None:
953 # This can *only* happen if qty available for the batch is zero.
954 # in such case fall back various other rates.
955 # future entries will correct the overall accounting as each
956 # batch individually uses moving average rates.
957 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530958 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530959
Ankush Menat494bd9e2022-03-28 18:52:46 +0530960 self.wh_data.stock_value = round_off_if_near_zero(
961 self.wh_data.stock_value + stock_value_difference
962 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530963 if self.wh_data.qty_after_transaction:
964 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530965
Javier Wong9b11d9b2017-04-14 18:24:04 +0800966 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530967 ref_item_dt = ""
968
969 if voucher_type == "Stock Entry":
970 ref_item_dt = voucher_type + " Detail"
971 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
972 ref_item_dt = voucher_type + " Item"
973
974 if ref_item_dt:
975 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
976 else:
977 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530978
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530979 def get_fallback_rate(self, sle) -> float:
980 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530981 This should only get used for negative stock."""
982 return get_valuation_rate(
983 sle.item_code,
984 sle.warehouse,
985 sle.voucher_type,
986 sle.voucher_no,
987 self.allow_zero_rate,
988 currency=erpnext.get_company_currency(sle.company),
989 company=sle.company,
990 batch_no=sle.batch_no,
991 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530992
Nabin Haita77b8c92020-12-21 14:45:50 +0530993 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530994 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530995 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
996 sle = sle[0] if sle else frappe._dict()
997 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530998
Nabin Haita77b8c92020-12-21 14:45:50 +0530999 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301000 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301001 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301002
1003 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301004 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301005 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301006 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301007
Ankush Menat494bd9e2022-03-28 18:52:46 +05301008 if (
1009 exceptions[0]["voucher_type"],
1010 exceptions[0]["voucher_no"],
1011 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301012
Nabin Haita77b8c92020-12-21 14:45:50 +05301013 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301014 abs(deficiency),
1015 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1016 frappe.get_desk_link("Warehouse", warehouse),
1017 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301018 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301019 msg = _(
1020 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1021 ).format(
1022 abs(deficiency),
1023 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1024 frappe.get_desk_link("Warehouse", warehouse),
1025 exceptions[0]["posting_date"],
1026 exceptions[0]["posting_time"],
1027 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1028 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301029
Nabin Haita77b8c92020-12-21 14:45:50 +05301030 if msg:
1031 msg_list.append(msg)
1032
1033 if msg_list:
1034 message = "\n\n".join(msg_list)
1035 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301036 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301037 else:
1038 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301039
Nabin Haita77b8c92020-12-21 14:45:50 +05301040 def update_bin(self):
1041 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301042 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301043 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301044
Ankush Menat494bd9e2022-03-28 18:52:46 +05301045 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301046 if data.valuation_rate is not None:
1047 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301048 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301049
marination8418c4b2021-06-22 21:35:25 +05301050
1051def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1052 """get stock ledger entries filtered by specific posting datetime conditions"""
1053
Ankush Menat494bd9e2022-03-28 18:52:46 +05301054 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301055 if not args.get("posting_date"):
1056 args["posting_date"] = "1900-01-01"
1057 if not args.get("posting_time"):
1058 args["posting_time"] = "00:00"
1059
1060 voucher_condition = ""
1061 if exclude_current_voucher:
1062 voucher_no = args.get("voucher_no")
1063 voucher_condition = f"and voucher_no != '{voucher_no}'"
1064
Ankush Menat494bd9e2022-03-28 18:52:46 +05301065 sle = frappe.db.sql(
1066 """
marination8418c4b2021-06-22 21:35:25 +05301067 select *, timestamp(posting_date, posting_time) as "timestamp"
1068 from `tabStock Ledger Entry`
1069 where item_code = %(item_code)s
1070 and warehouse = %(warehouse)s
1071 and is_cancelled = 0
1072 {voucher_condition}
1073 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1074 order by timestamp(posting_date, posting_time) desc, creation desc
1075 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301076 for update""".format(
1077 voucher_condition=voucher_condition
1078 ),
1079 args,
1080 as_dict=1,
1081 )
marination8418c4b2021-06-22 21:35:25 +05301082
1083 return sle[0] if sle else frappe._dict()
1084
Ankush Menat494bd9e2022-03-28 18:52:46 +05301085
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301086def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301087 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301088 get the last sle on or before the current time-bucket,
1089 to get actual qty before transaction, this function
1090 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301091
Ankush Menat494bd9e2022-03-28 18:52:46 +05301092 args = {
1093 "item_code": "ABC",
1094 "warehouse": "XYZ",
1095 "posting_date": "2012-12-12",
1096 "posting_time": "12:00",
1097 "sle": "name of reference Stock Ledger Entry"
1098 }
Anand Doshi1b531862013-01-10 19:29:51 +05301099 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301100 args["name"] = args.get("sle", None) or ""
1101 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301102 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301103
Ankush Menat494bd9e2022-03-28 18:52:46 +05301104
1105def get_stock_ledger_entries(
1106 previous_sle,
1107 operator=None,
1108 order="desc",
1109 limit=None,
1110 for_update=False,
1111 debug=False,
1112 check_serial_no=True,
1113):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301114 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301115 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1116 operator
1117 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301118 if previous_sle.get("warehouse"):
1119 conditions += " and warehouse = %(warehouse)s"
1120 elif previous_sle.get("warehouse_condition"):
1121 conditions += " and " + previous_sle.get("warehouse_condition")
1122
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301123 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301124 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1125 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301126 conditions += (
1127 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301128 (
1129 serial_no = {0}
1130 or serial_no like {1}
1131 or serial_no like {2}
1132 or serial_no like {3}
1133 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301134 """
1135 ).format(
1136 frappe.db.escape(serial_no),
1137 frappe.db.escape("{}\n%".format(serial_no)),
1138 frappe.db.escape("%\n{}".format(serial_no)),
1139 frappe.db.escape("%\n{}\n%".format(serial_no)),
1140 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301141
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301142 if not previous_sle.get("posting_date"):
1143 previous_sle["posting_date"] = "1900-01-01"
1144 if not previous_sle.get("posting_time"):
1145 previous_sle["posting_time"] = "00:00"
1146
1147 if operator in (">", "<=") and previous_sle.get("name"):
1148 conditions += " and name!=%(name)s"
1149
Ankush Menat494bd9e2022-03-28 18:52:46 +05301150 return frappe.db.sql(
1151 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301152 select *, timestamp(posting_date, posting_time) as "timestamp"
1153 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301154 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301155 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301156 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301157 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301158 %(limit)s %(for_update)s"""
1159 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301160 "conditions": conditions,
1161 "limit": limit or "",
1162 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301163 "order": order,
1164 },
1165 previous_sle,
1166 as_dict=1,
1167 debug=debug,
1168 )
1169
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301170
Nabin Haita77b8c92020-12-21 14:45:50 +05301171def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301172 return frappe.db.get_value(
1173 "Stock Ledger Entry",
1174 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1175 [
1176 "item_code",
1177 "warehouse",
1178 "posting_date",
1179 "posting_time",
1180 "timestamp(posting_date, posting_time) as timestamp",
1181 ],
1182 as_dict=1,
1183 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301184
Ankush Menatce0514c2022-02-15 11:41:41 +05301185
Ankush Menat494bd9e2022-03-28 18:52:46 +05301186def get_batch_incoming_rate(
1187 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1188):
1189
Ankush Menat102fff22022-02-19 15:51:04 +05301190 sle = frappe.qb.DocType("Stock Ledger Entry")
1191
Ankush Menate1c16872022-04-21 20:01:48 +05301192 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301193 posting_date, posting_time
1194 )
Ankush Menat102fff22022-02-19 15:51:04 +05301195 if creation:
1196 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301197 CombineDatetime(sle.posting_date, sle.posting_time)
1198 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301199 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301200
1201 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301202 frappe.qb.from_(sle)
1203 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1204 .where(
1205 (sle.item_code == item_code)
1206 & (sle.warehouse == warehouse)
1207 & (sle.batch_no == batch_no)
1208 & (sle.is_cancelled == 0)
1209 )
1210 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301211 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301212
1213 if batch_details and batch_details[0].batch_qty:
1214 return batch_details[0].batch_value / batch_details[0].batch_qty
1215
1216
Ankush Menat494bd9e2022-03-28 18:52:46 +05301217def get_valuation_rate(
1218 item_code,
1219 warehouse,
1220 voucher_type,
1221 voucher_no,
1222 allow_zero_rate=False,
1223 currency=None,
1224 company=None,
1225 raise_error_if_no_rate=True,
1226 batch_no=None,
1227):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301228
Ankush Menatf7ffe042021-11-01 13:21:14 +05301229 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301230 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301231
Ankush Menat342d09a2022-02-19 14:28:51 +05301232 last_valuation_rate = None
1233
1234 # Get moving average rate of a specific batch number
1235 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301236 last_valuation_rate = frappe.db.sql(
1237 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301238 select sum(stock_value_difference) / sum(actual_qty)
1239 from `tabStock Ledger Entry`
1240 where
1241 item_code = %s
1242 AND warehouse = %s
1243 AND batch_no = %s
1244 AND is_cancelled = 0
1245 AND NOT (voucher_no = %s AND voucher_type = %s)
1246 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301247 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1248 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301249
Ankush Menatf7ffe042021-11-01 13:21:14 +05301250 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301251 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301252 last_valuation_rate = frappe.db.sql(
1253 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301254 from `tabStock Ledger Entry` force index (item_warehouse)
1255 where
1256 item_code = %s
1257 AND warehouse = %s
1258 AND valuation_rate >= 0
1259 AND is_cancelled = 0
1260 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301261 order by posting_date desc, posting_time desc, name desc limit 1""",
1262 (item_code, warehouse, voucher_no, voucher_type),
1263 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301264
1265 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301266 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301267 last_valuation_rate = frappe.db.sql(
1268 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301269 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301270 where
1271 item_code = %s
1272 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301273 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301274 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301275 order by posting_date desc, posting_time desc, name desc limit 1""",
1276 (item_code, voucher_no, voucher_type),
1277 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301278
Nabin Haita645f362018-03-01 10:31:24 +05301279 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301280 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301281
1282 # If negative stock allowed, and item delivered without any incoming entry,
1283 # system does not found any SLE, then take valuation rate from Item
1284 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301285
1286 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301287 # try Item Standard rate
1288 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301289
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301290 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301291 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301292 valuation_rate = frappe.db.get_value(
1293 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1294 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301295
Ankush Menat494bd9e2022-03-28 18:52:46 +05301296 if (
1297 not allow_zero_rate
1298 and not valuation_rate
1299 and raise_error_if_no_rate
1300 and cint(erpnext.is_perpetual_inventory_enabled(company))
1301 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301302 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301303
Ankush Menat494bd9e2022-03-28 18:52:46 +05301304 message = _(
1305 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1306 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301307 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301308 solutions = (
1309 "<li>"
1310 + _(
1311 "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."
1312 ).format(voucher_type)
1313 + "</li>"
1314 )
1315 solutions += (
1316 "<li>"
1317 + _("If not, you can Cancel / Submit this entry")
1318 + " {0} ".format(frappe.bold("after"))
1319 + _("performing either one below:")
1320 + "</li>"
1321 )
Marica97715f22020-05-11 20:45:37 +05301322 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1323 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1324 msg = message + solutions + sub_solutions + "</li>"
1325
1326 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301327
1328 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301329
Ankush Menat494bd9e2022-03-28 18:52:46 +05301330
Ankush Menate7109c12021-08-26 16:40:45 +05301331def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301332 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301333 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301334 qty_shift = args.actual_qty
1335
Ankush Menat7c839c42022-05-06 12:09:08 +05301336 args["time_format"] = "%H:%i:%s"
1337
marination8418c4b2021-06-22 21:35:25 +05301338 # find difference/shift in qty caused by stock reconciliation
1339 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301340 qty_shift = get_stock_reco_qty_shift(args)
1341
1342 # find the next nearest stock reco so that we only recalculate SLEs till that point
1343 next_stock_reco_detail = get_next_stock_reco(args)
1344 if next_stock_reco_detail:
1345 detail = next_stock_reco_detail[0]
1346 # add condition to update SLEs before this date & time
1347 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301348
Ankush Menat494bd9e2022-03-28 18:52:46 +05301349 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301350 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301351 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301352 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301353 where
1354 item_code = %(item_code)s
1355 and warehouse = %(warehouse)s
1356 and voucher_no != %(voucher_no)s
1357 and is_cancelled = 0
Ankush Menat7c839c42022-05-06 12:09:08 +05301358 and timestamp(posting_date, time_format(posting_time, %(time_format)s))
1359 > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
marination40389772021-07-02 17:13:45 +05301360 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301361 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301362 args,
1363 )
Nabin Hait186a0452021-02-18 14:14:21 +05301364
1365 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1366
Ankush Menat494bd9e2022-03-28 18:52:46 +05301367
marination40389772021-07-02 17:13:45 +05301368def get_stock_reco_qty_shift(args):
1369 stock_reco_qty_shift = 0
1370 if args.get("is_cancelled"):
1371 if args.get("previous_qty_after_transaction"):
1372 # get qty (balance) that was set at submission
1373 last_balance = args.get("previous_qty_after_transaction")
1374 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1375 else:
1376 stock_reco_qty_shift = flt(args.actual_qty)
1377 else:
1378 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301379 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1380 "qty_after_transaction"
1381 )
marination40389772021-07-02 17:13:45 +05301382
1383 if last_balance is not None:
1384 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1385 else:
1386 stock_reco_qty_shift = args.qty_after_transaction
1387
1388 return stock_reco_qty_shift
1389
Ankush Menat494bd9e2022-03-28 18:52:46 +05301390
marination40389772021-07-02 17:13:45 +05301391def get_next_stock_reco(args):
1392 """Returns next nearest stock reconciliaton's details."""
1393
Ankush Menat494bd9e2022-03-28 18:52:46 +05301394 return frappe.db.sql(
1395 """
marination40389772021-07-02 17:13:45 +05301396 select
1397 name, posting_date, posting_time, creation, voucher_no
1398 from
marination8c441262021-07-02 17:46:05 +05301399 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301400 where
1401 item_code = %(item_code)s
1402 and warehouse = %(warehouse)s
1403 and voucher_type = 'Stock Reconciliation'
1404 and voucher_no != %(voucher_no)s
1405 and is_cancelled = 0
1406 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1407 or (
1408 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1409 and creation > %(creation)s
1410 )
1411 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301412 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301413 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301414 """,
1415 args,
1416 as_dict=1,
1417 )
1418
marination40389772021-07-02 17:13:45 +05301419
1420def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301421 return f"""
1422 and
1423 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1424 or (
1425 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1426 and creation < '{detail.creation}'
1427 )
1428 )"""
1429
Ankush Menat494bd9e2022-03-28 18:52:46 +05301430
Ankush Menate7109c12021-08-26 16:40:45 +05301431def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301432 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301433 return
1434 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1435 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301436
Ankush Menat5eba5752021-12-07 23:03:52 +05301437 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301438
1439 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301440 message = _(
1441 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1442 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301443 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301444 frappe.get_desk_link("Item", args.item_code),
1445 frappe.get_desk_link("Warehouse", args.warehouse),
1446 neg_sle[0]["posting_date"],
1447 neg_sle[0]["posting_time"],
1448 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1449 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301450
Ankush Menat494bd9e2022-03-28 18:52:46 +05301451 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301452
1453 if not args.batch_no:
1454 return
1455
1456 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301457 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301458 message = _(
1459 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1460 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301461 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301462 frappe.get_desk_link("Batch", args.batch_no),
1463 frappe.get_desk_link("Warehouse", args.warehouse),
1464 neg_batch_sle[0]["posting_date"],
1465 neg_batch_sle[0]["posting_time"],
1466 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1467 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301468 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301469
Nabin Haita77b8c92020-12-21 14:45:50 +05301470
Maricad6078aa2022-06-17 15:13:13 +05301471def is_negative_with_precision(neg_sle, is_batch=False):
1472 """
1473 Returns whether system precision rounded qty is insufficient.
1474 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1475 """
1476
1477 if not neg_sle:
1478 return False
1479
1480 field = "cumulative_total" if is_batch else "qty_after_transaction"
1481 precision = cint(frappe.db.get_default("float_precision")) or 2
1482 qty_deficit = flt(neg_sle[0][field], precision)
1483
1484 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1485
1486
Nabin Haita77b8c92020-12-21 14:45:50 +05301487def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301488 return frappe.db.sql(
1489 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301490 select
1491 qty_after_transaction, posting_date, posting_time,
1492 voucher_type, voucher_no
1493 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301494 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301495 item_code = %(item_code)s
1496 and warehouse = %(warehouse)s
1497 and voucher_no != %(voucher_no)s
1498 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1499 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301500 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301501 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301502 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301503 """,
1504 args,
1505 as_dict=1,
1506 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301507
Ankush Menat5eba5752021-12-07 23:03:52 +05301508
1509def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301510 return frappe.db.sql(
1511 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301512 with batch_ledger as (
1513 select
1514 posting_date, posting_time, voucher_type, voucher_no,
1515 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1516 from `tabStock Ledger Entry`
1517 where
1518 item_code = %(item_code)s
1519 and warehouse = %(warehouse)s
1520 and batch_no=%(batch_no)s
1521 and is_cancelled = 0
1522 order by posting_date, posting_time, creation
1523 )
1524 select * from batch_ledger
1525 where
1526 cumulative_total < 0.0
1527 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1528 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301529 """,
1530 args,
1531 as_dict=1,
1532 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301533
1534
1535def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1536 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1537 return True
1538 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1539 return True
1540 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301541
1542
1543def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1544 """
1545 For inter company transfer, incoming rate is the average of the outgoing rate
1546 """
1547 rate = 0.0
1548
1549 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1550
1551 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1552
1553 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1554
1555 if reference_name:
1556 rate = frappe.get_cached_value(
1557 doctype,
1558 reference_name,
1559 "incoming_rate",
1560 )
1561
1562 return rate