blob: 463706e3886524fd1b31494cf2e19a3cc12683c9 [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
s-aga-rf0acb202023-04-12 14:13:54 +053016from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
17 get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
18)
Chillar Anand915b3432021-09-02 16:44:59 +053019from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053020 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053021 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053022 get_valuation_method,
23)
Ankush Menatb534fee2022-02-19 20:58:36 +053024from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053025
Nabin Hait97bce3a2021-07-12 13:24:43 +053026
Ankush Menat494bd9e2022-03-28 18:52:46 +053027class NegativeStockError(frappe.ValidationError):
28 pass
29
30
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053031class SerialNoExistsInFutureTransaction(frappe.ValidationError):
32 pass
Nabin Hait902e8602013-01-08 18:29:24 +053033
Anand Doshi5b004ff2013-09-25 19:55:41 +053034
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053035def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053036 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053037
Ankush Menat494bd9e2022-03-28 18:52:46 +053038 args:
39 - allow_negative_stock: disable negative stock valiations if true
40 - via_landed_cost_voucher: landed cost voucher cancels and reposts
41 entries of purchase document. This flag is used to identify if
42 cancellation and repost is happening via landed cost voucher, in
43 such cases certain validations need to be ignored (like negative
44 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053045 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053046 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053047
Nabin Haitca775742013-09-26 16:16:44 +053048 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053049 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053050 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053051 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053052 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053053
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053054 args = get_args_for_future_sle(sl_entries[0])
55 future_sle_exists(args, sl_entries)
56
Nabin Haitca775742013-09-26 16:16:44 +053057 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053058 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053059 validate_serial_no(sle)
60
Nabin Haita77b8c92020-12-21 14:45:50 +053061 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053062 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053063
Ankush Menat494bd9e2022-03-28 18:52:46 +053064 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
65 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
66 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
67 )
68 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053069
Ankush Menat494bd9e2022-03-28 18:52:46 +053070 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
71 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
72 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
73 )
74 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053075
Ankush Menat494bd9e2022-03-28 18:52:46 +053076 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053077 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053078
Nabin Haita77b8c92020-12-21 14:45:50 +053079 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053080
81 if sle.get("voucher_type") == "Stock Reconciliation":
82 # preserve previous_qty_after_transaction for qty reposting
83 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
84
Ankush Menat494bd9e2022-03-28 18:52:46 +053085 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053086 if is_stock_item:
87 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053088 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053089 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053090 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +053091 frappe.msgprint(
92 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
93 )
94
Ankush Menatcef84c22021-12-03 12:18:59 +053095
96def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
97 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
98 if not args.get("posting_date"):
99 args["posting_date"] = nowdate()
100
marination7a5fd712022-07-04 17:46:54 +0530101 if not (args.get("is_cancelled") and via_landed_cost_voucher):
102 # Reposts only current voucher SL Entries
103 # Updates valuation rate, stock value, stock queue for current transaction
104 update_entries_after(
105 {
106 "item_code": args.get("item_code"),
107 "warehouse": args.get("warehouse"),
108 "posting_date": args.get("posting_date"),
109 "posting_time": args.get("posting_time"),
110 "voucher_type": args.get("voucher_type"),
111 "voucher_no": args.get("voucher_no"),
112 "sle_id": args.get("name"),
113 "creation": args.get("creation"),
114 },
115 allow_negative_stock=allow_negative_stock,
116 via_landed_cost_voucher=via_landed_cost_voucher,
117 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530118
119 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530120 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530121 update_qty_in_future_sle(args, allow_negative_stock)
122
Nabin Haitadeb9762014-10-06 11:53:52 +0530123
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530124def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530125 return frappe._dict(
126 {
127 "voucher_type": row.get("voucher_type"),
128 "voucher_no": row.get("voucher_no"),
129 "posting_date": row.get("posting_date"),
130 "posting_time": row.get("posting_time"),
131 }
132 )
133
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530134
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530135def validate_serial_no(sle):
136 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530137
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530138 for sn in get_serial_nos(sle.serial_no):
139 args = copy.deepcopy(sle)
140 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530141 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530142
143 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530144 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530145 voucher_type = frappe.bold(row.voucher_type)
146 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530147 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530148
149 if vouchers:
150 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530151 msg = (
152 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
153 The list of the transactions are as below."""
154 + "<br><br><ul><li>"
155 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530156
Ankush Menat494bd9e2022-03-28 18:52:46 +0530157 msg += "</li><li>".join(vouchers)
158 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530159
Ankush Menat494bd9e2022-03-28 18:52:46 +0530160 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530161 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
162
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163
Nabin Hait186a0452021-02-18 14:14:21 +0530164def validate_cancellation(args):
165 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530166 repost_entry = frappe.db.get_value(
167 "Repost Item Valuation",
168 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
169 ["name", "status"],
170 as_dict=1,
171 )
Nabin Hait186a0452021-02-18 14:14:21 +0530172
173 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530174 if repost_entry.status == "In Progress":
175 frappe.throw(
176 _(
177 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
178 )
179 )
180 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530181 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530182 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530183 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530184 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530185
Ankush Menat494bd9e2022-03-28 18:52:46 +0530186
Nabin Hait9653f602013-08-20 15:37:33 +0530187def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530188 frappe.db.sql(
189 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530190 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530191 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530192 (now(), frappe.session.user, voucher_type, voucher_no),
193 )
194
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530195
Nabin Hait54c865e2015-03-27 15:38:31 +0530196def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530197 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530198 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530199 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530200 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530201 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530202 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530203 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530204
Ankush Menat494bd9e2022-03-28 18:52:46 +0530205
206def repost_future_sle(
207 args=None,
208 voucher_type=None,
209 voucher_no=None,
210 allow_negative_stock=None,
211 via_landed_cost_voucher=False,
212 doc=None,
213):
Nabin Haite1fa7232022-07-20 15:19:09 +0530214 if not args:
215 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530216
217 items_to_be_repost = get_items_to_be_repost(
218 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
219 )
220 if items_to_be_repost:
221 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530222
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530223 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Ankush Menatecdb4932022-04-17 19:06:13 +0530224 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530225
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530226 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530227 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530228 validate_item_warehouse(args[i])
229
Ankush Menat494bd9e2022-03-28 18:52:46 +0530230 obj = update_entries_after(
231 {
232 "item_code": args[i].get("item_code"),
233 "warehouse": args[i].get("warehouse"),
234 "posting_date": args[i].get("posting_date"),
235 "posting_time": args[i].get("posting_time"),
236 "creation": args[i].get("creation"),
237 "distinct_item_warehouses": distinct_item_warehouses,
238 },
239 allow_negative_stock=allow_negative_stock,
240 via_landed_cost_voucher=via_landed_cost_voucher,
241 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530242 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530243
Ankush Menat494bd9e2022-03-28 18:52:46 +0530244 distinct_item_warehouses[
245 (args[i].get("item_code"), args[i].get("warehouse"))
246 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530247
Nabin Hait97bce3a2021-07-12 13:24:43 +0530248 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530249 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530250 if ("args_idx" not in data and not data.reposting_status) or (
251 data.sle_changed and data.reposting_status
252 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530253 data.args_idx = len(args)
254 args.append(data.sle)
255 elif data.sle_changed and not data.reposting_status:
256 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530257
Nabin Hait97bce3a2021-07-12 13:24:43 +0530258 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530259 i += 1
260
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530261 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530262 update_args_in_repost_item_valuation(
263 doc, i, args, distinct_item_warehouses, affected_transactions
264 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530265
Ankush Menat494bd9e2022-03-28 18:52:46 +0530266
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530267def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530268 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530269 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530270 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530271 frappe.throw(_(validation_msg))
272
Ankush Menat494bd9e2022-03-28 18:52:46 +0530273
Ankush Menatecdb4932022-04-17 19:06:13 +0530274def update_args_in_repost_item_valuation(
275 doc, index, args, distinct_item_warehouses, affected_transactions
276):
Ankush Menatecdb4932022-04-17 19:06:13 +0530277 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530278 {
279 "items_to_be_repost": json.dumps(args, default=str),
280 "distinct_item_and_warehouse": json.dumps(
281 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
282 ),
283 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530284 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530285 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530286 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530287
Ankush Menatecdb4932022-04-17 19:06:13 +0530288 if not frappe.flags.in_test:
289 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530290
Ankush Menat494bd9e2022-03-28 18:52:46 +0530291 frappe.publish_realtime(
292 "item_reposting_progress",
293 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
294 )
295
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530297def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
298 items_to_be_repost = []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530299 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530300 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530301
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530302 if not items_to_be_repost and voucher_type and voucher_no:
303 items_to_be_repost = frappe.db.get_all(
304 "Stock Ledger Entry",
305 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
306 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
307 order_by="creation asc",
308 group_by="item_code, warehouse",
309 )
310
Nabin Haite1fa7232022-07-20 15:19:09 +0530311 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530312
Ankush Menat494bd9e2022-03-28 18:52:46 +0530313
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530314def get_distinct_item_warehouse(args=None, doc=None):
315 distinct_item_warehouses = {}
316 if doc and doc.distinct_item_and_warehouse:
317 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530318 distinct_item_warehouses = {
319 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
320 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530321 else:
322 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530323 distinct_item_warehouses.setdefault(
324 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
325 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530326
327 return distinct_item_warehouses
328
Ankush Menat494bd9e2022-03-28 18:52:46 +0530329
Ankush Menatecdb4932022-04-17 19:06:13 +0530330def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
331 if not doc.affected_transactions:
332 return set()
333
334 transactions = frappe.parse_json(doc.affected_transactions)
335 return {tuple(transaction) for transaction in transactions}
336
337
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530338def get_current_index(doc=None):
339 if doc and doc.current_index:
340 return doc.current_index
341
Ankush Menat494bd9e2022-03-28 18:52:46 +0530342
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530343class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530344 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530345 update valution rate and qty after transaction
346 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530347
Ankush Menat494bd9e2022-03-28 18:52:46 +0530348 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530349
Ankush Menat494bd9e2022-03-28 18:52:46 +0530350 args = {
351 "item_code": "ABC",
352 "warehouse": "XYZ",
353 "posting_date": "2012-12-12",
354 "posting_time": "12:00"
355 }
Nabin Hait902e8602013-01-08 18:29:24 +0530356 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530357
358 def __init__(
359 self,
360 args,
361 allow_zero_rate=False,
362 allow_negative_stock=None,
363 via_landed_cost_voucher=False,
364 verbose=1,
365 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530366 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530367 self.verbose = verbose
368 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530369 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530370 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530371 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
372 item_code=self.item_code
373 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530374
Nabin Haita77b8c92020-12-21 14:45:50 +0530375 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530376 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530377 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530378
Nabin Haita77b8c92020-12-21 14:45:50 +0530379 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530380 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530381 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530382
383 self.new_items_found = False
384 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530385 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-rf0acb202023-04-12 14:13:54 +0530386 self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
Nabin Haita77b8c92020-12-21 14:45:50 +0530387
388 self.data = frappe._dict()
389 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530390 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530391
Maricad6078aa2022-06-17 15:13:13 +0530392 def set_precision(self):
393 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
394 self.currency_precision = get_field_precision(
395 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530396 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530397
398 def initialize_previous_data(self, args):
399 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530400 Get previous sl entries for current item for each related warehouse
401 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530402
Ankush Menat494bd9e2022-03-28 18:52:46 +0530403 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530404
Ankush Menat494bd9e2022-03-28 18:52:46 +0530405 self.data = {
406 warehouse1: {
407 'previus_sle': {},
408 'qty_after_transaction': 10,
409 'valuation_rate': 100,
410 'stock_value': 1000,
411 'prev_stock_value': 1000,
412 'stock_queue': '[[10, 100]]',
413 'stock_value_difference': 1000
414 }
415 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530416
417 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530418 self.data.setdefault(args.warehouse, frappe._dict())
419 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530420 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530421 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530422
Ankush Menatc1d986a2021-08-31 19:43:42 +0530423 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
424 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
425
Ankush Menat494bd9e2022-03-28 18:52:46 +0530426 warehouse_dict.update(
427 {
428 "prev_stock_value": previous_sle.stock_value or 0.0,
429 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
430 "stock_value_difference": 0.0,
431 }
432 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530433
Nabin Haita77b8c92020-12-21 14:45:50 +0530434 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530435 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530436
Nabin Haita77b8c92020-12-21 14:45:50 +0530437 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530438 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530439 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530440 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530441 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530442 entries_to_fix = self.get_future_entries_to_fix()
443
444 i = 0
445 while i < len(entries_to_fix):
446 sle = entries_to_fix[i]
447 i += 1
448
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530449 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530450
Nabin Haita77b8c92020-12-21 14:45:50 +0530451 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530452 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530453
Nabin Hait186a0452021-02-18 14:14:21 +0530454 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530455
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530456 if self.exceptions:
457 self.raise_exceptions()
458
Nabin Hait186a0452021-02-18 14:14:21 +0530459 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530460 sl_entries = self.get_sle_against_current_voucher()
461 for sle in sl_entries:
462 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530463
Nabin Haita77b8c92020-12-21 14:45:50 +0530464 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530465 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530466
Ankush Menat494bd9e2022-03-28 18:52:46 +0530467 return frappe.db.sql(
468 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530469 select
470 *, timestamp(posting_date, posting_time) as "timestamp"
471 from
472 `tabStock Ledger Entry`
473 where
474 item_code = %(item_code)s
475 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530476 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530477 and (
478 posting_date = %(posting_date)s and
479 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
480 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530481 order by
482 creation ASC
483 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530484 """,
485 self.args,
486 as_dict=1,
487 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530488
Nabin Haita77b8c92020-12-21 14:45:50 +0530489 def get_future_entries_to_fix(self):
490 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530491 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
492 {"item_code": self.item_code, "warehouse": self.args.warehouse}
493 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530494
Nabin Haita77b8c92020-12-21 14:45:50 +0530495 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530496
Nabin Haita77b8c92020-12-21 14:45:50 +0530497 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530498 dependant_sle = get_sle_by_voucher_detail_no(
499 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
500 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530501
Nabin Haita77b8c92020-12-21 14:45:50 +0530502 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530503 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530504 elif (
505 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
506 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530507 return entries_to_fix
508 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530509 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530510 return entries_to_fix
511 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
512 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530513 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530514 self.initialize_previous_data(dependant_sle)
515 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530516 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530517
518 def update_distinct_item_warehouses(self, dependant_sle):
519 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530520 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530521 if key not in self.distinct_item_warehouses:
522 self.distinct_item_warehouses[key] = val
523 self.new_items_found = True
524 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530525 existing_sle_posting_date = (
526 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
527 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530528 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
529 val.sle_changed = True
530 self.distinct_item_warehouses[key] = val
531 self.new_items_found = True
532
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530533 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530534 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
535
Nabin Haita77b8c92020-12-21 14:45:50 +0530536 # previous sle data for this warehouse
537 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530538 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530539
Anand Doshi0dc79f42015-04-06 12:59:34 +0530540 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 +0530541 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530542 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530543 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530544 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530545 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530546
Nabin Haita77b8c92020-12-21 14:45:50 +0530547 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530548 if not self.args.get("sle_id"):
549 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530550
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530551 if (
552 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
553 and sle.voucher_detail_no
554 and sle.actual_qty < 0
555 and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
556 ):
557 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
558
Ankush Menat66bf21f2022-01-16 20:45:59 +0530559 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530560 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530561 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530562 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530563 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530564
Ankush Menat494bd9e2022-03-28 18:52:46 +0530565 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
566 self.wh_data.valuation_rate
567 )
568 elif sle.batch_no and frappe.db.get_value(
569 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
570 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530571 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530572 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530573 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530574 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530575 self.wh_data.valuation_rate = sle.valuation_rate
576 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530577 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
578 self.wh_data.valuation_rate
579 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530580 if self.valuation_method != "Moving Average":
581 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530582 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530583 if self.valuation_method == "Moving Average":
584 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530585 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530586 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
587 self.wh_data.valuation_rate
588 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530589 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530590 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530591
Rushabh Mehta54047782013-12-26 11:07:46 +0530592 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530593 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530594 if not self.wh_data.qty_after_transaction:
595 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530596 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
597 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530598
Nabin Hait902e8602013-01-08 18:29:24 +0530599 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530600 sle.qty_after_transaction = self.wh_data.qty_after_transaction
601 sle.valuation_rate = self.wh_data.valuation_rate
602 sle.stock_value = self.wh_data.stock_value
603 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530604 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530605 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530606
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530607 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530608
Ankush Menat701878f2022-03-01 18:08:29 +0530609 if not self.args.get("sle_id"):
610 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530611
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530612 def validate_negative_stock(self, sle):
613 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530614 validate negative stock for entries current datetime onwards
615 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530616 """
s-aga-rf0acb202023-04-12 14:13:54 +0530617 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530618 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530619
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530620 if diff < 0 and abs(diff) > 0.0001:
621 # negative stock!
622 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530623 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530624 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530625 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530626 return True
627
Nabin Haita77b8c92020-12-21 14:45:50 +0530628 def get_dynamic_incoming_outgoing_rate(self, sle):
629 # Get updated incoming/outgoing rate from transaction
630 if sle.recalculate_rate:
631 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
632
633 if flt(sle.actual_qty) >= 0:
634 sle.incoming_rate = rate
635 else:
636 sle.outgoing_rate = rate
637
638 def get_incoming_outgoing_rate_from_transaction(self, sle):
639 rate = 0
640 # Material Transfer, Repack, Manufacturing
641 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530642 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530643 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
644 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530645 elif sle.voucher_type in (
646 "Purchase Receipt",
647 "Purchase Invoice",
648 "Delivery Note",
649 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530650 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530651 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530652 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530653 from erpnext.controllers.sales_and_purchase_return import (
654 get_rate_for_return, # don't move this import to top
655 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530656
657 rate = get_rate_for_return(
658 sle.voucher_type,
659 sle.voucher_no,
660 sle.item_code,
661 voucher_detail_no=sle.voucher_detail_no,
662 sle=sle,
663 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530664
665 elif (
666 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530667 and sle.voucher_detail_no
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530668 and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
669 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530670 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530671 else:
672 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530673 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530674 elif sle.voucher_type == "Subcontracting Receipt":
675 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530676 else:
677 rate_field = "incoming_rate"
678
679 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530680 item_code, incoming_rate = frappe.db.get_value(
681 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
682 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530683
684 if item_code == sle.item_code:
685 rate = incoming_rate
686 else:
687 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
688 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530689 elif sle == "Subcontracting Receipt":
690 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530691 else:
692 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530693
Ankush Menat494bd9e2022-03-28 18:52:46 +0530694 rate = frappe.db.get_value(
695 ref_doctype,
696 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
697 rate_field,
698 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530699
700 return rate
701
702 def update_outgoing_rate_on_transaction(self, sle):
703 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530704 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
705 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530706 """
707 if sle.actual_qty and sle.voucher_detail_no:
708 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
709
710 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
711 self.update_rate_on_stock_entry(sle, outgoing_rate)
712 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
713 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
714 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
715 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530716 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
717 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530718
719 def update_rate_on_stock_entry(self, sle, outgoing_rate):
720 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
721
Ankush Menat701878f2022-03-01 18:08:29 +0530722 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
723 if not sle.dependant_sle_voucher_detail_no:
724 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530725
726 def recalculate_amounts_in_stock_entry(self, voucher_no):
727 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530728 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
729 stock_entry.db_update()
730 for d in stock_entry.items:
731 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530732
Nabin Haita77b8c92020-12-21 14:45:50 +0530733 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
734 # Update item's incoming rate on transaction
735 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
736 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530737 frappe.db.set_value(
738 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
739 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530740 else:
741 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530742 frappe.db.set_value(
743 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530744 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530745 "incoming_rate",
746 outgoing_rate,
747 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530748
749 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
750 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530751 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
752 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
753 ):
754 frappe.db.set_value(
755 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
756 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530757 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530758 frappe.db.set_value(
759 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
760 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530761
762 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530763 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530764 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530765 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530766 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530767 d.db_update()
768
Sagar Sharma323bdf82022-05-17 15:14:07 +0530769 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
770 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
771 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
772 else:
773 frappe.db.set_value(
774 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
775 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530776
777 def get_serialized_values(self, sle):
778 incoming_rate = flt(sle.incoming_rate)
779 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530780 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530781
782 if incoming_rate < 0:
783 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530784 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530785
Nabin Hait2620bf42016-02-29 11:30:27 +0530786 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530787 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530788 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530789 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530790 # In case of delivery/stock issue, get average purchase rate
791 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530792 if not sle.is_cancelled:
793 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
794 stock_value_change = -1 * outgoing_value
795 else:
796 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530797
Nabin Haita77b8c92020-12-21 14:45:50 +0530798 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530799
Nabin Hait2620bf42016-02-29 11:30:27 +0530800 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530801 new_stock_value = (
802 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
803 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530804 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530805 # calculate new valuation rate only if stock value is positive
806 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530807 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530808
Nabin Haita77b8c92020-12-21 14:45:50 +0530809 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530810 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
811 sle.voucher_type, sle.voucher_detail_no
812 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530813 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530814 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530815
Nabin Hait328c4f92020-01-02 19:00:32 +0530816 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
817 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530818 all_serial_nos = frappe.get_all(
819 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
820 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530821
Ankush Menat494bd9e2022-03-28 18:52:46 +0530822 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 +0530823
824 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530825 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530826 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530827 incoming_rate = frappe.db.sql(
828 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530829 select incoming_rate
830 from `tabStock Ledger Entry`
831 where
832 company = %s
833 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530834 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530835 and (serial_no = %s
836 or serial_no like %s
837 or serial_no like %s
838 or serial_no like %s
839 )
840 order by posting_date desc
841 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530842 """,
843 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
844 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530845
846 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
847
848 return incoming_values
849
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530850 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530851 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530852 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530853 if new_stock_qty >= 0:
854 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530855 if flt(self.wh_data.qty_after_transaction) <= 0:
856 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530857 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530858 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
859 actual_qty * sle.incoming_rate
860 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530861
Nabin Haita77b8c92020-12-21 14:45:50 +0530862 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530863
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530864 elif sle.outgoing_rate:
865 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530866 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
867 actual_qty * sle.outgoing_rate
868 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530869
Nabin Haita77b8c92020-12-21 14:45:50 +0530870 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530871 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530872 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530873 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530874 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
875 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530876
Nabin Haita77b8c92020-12-21 14:45:50 +0530877 if not self.wh_data.valuation_rate and actual_qty > 0:
878 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530879
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530880 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800881 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530882 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530883 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
884 sle.voucher_type, sle.voucher_detail_no
885 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800886 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530887 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530888
Ankush Menatf089d392022-02-02 12:51:21 +0530889 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530890 incoming_rate = flt(sle.incoming_rate)
891 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530892 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530893
Ankush Menat494bd9e2022-03-28 18:52:46 +0530894 self.wh_data.qty_after_transaction = round_off_if_near_zero(
895 self.wh_data.qty_after_transaction + actual_qty
896 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530897
Ankush Menat97e18a12022-01-15 17:42:25 +0530898 if self.valuation_method == "LIFO":
899 stock_queue = LIFOValuation(self.wh_data.stock_queue)
900 else:
901 stock_queue = FIFOValuation(self.wh_data.stock_queue)
902
Ankush Menatb534fee2022-02-19 20:58:36 +0530903 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
904
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530905 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530906 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530907 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530908
Ankush Menat4b29fb62021-12-18 18:40:22 +0530909 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530910 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
911 sle.voucher_type, sle.voucher_detail_no
912 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530913 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530914 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530915 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530916 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530917
Ankush Menat494bd9e2022-03-28 18:52:46 +0530918 stock_queue.remove_stock(
919 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
920 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530921
Ankush Menatb534fee2022-02-19 20:58:36 +0530922 _qty, stock_value = stock_queue.get_total_stock_and_value()
923
924 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530925
Ankush Menat97e18a12022-01-15 17:42:25 +0530926 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530927 self.wh_data.stock_value = round_off_if_near_zero(
928 self.wh_data.stock_value + stock_value_difference
929 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530930
Nabin Haita77b8c92020-12-21 14:45:50 +0530931 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530932 self.wh_data.stock_queue.append(
933 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
934 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530935
Ankush Menatb534fee2022-02-19 20:58:36 +0530936 if self.wh_data.qty_after_transaction:
937 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
938
Ankush Menatce0514c2022-02-15 11:41:41 +0530939 def update_batched_values(self, sle):
940 incoming_rate = flt(sle.incoming_rate)
941 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530942
Ankush Menat494bd9e2022-03-28 18:52:46 +0530943 self.wh_data.qty_after_transaction = round_off_if_near_zero(
944 self.wh_data.qty_after_transaction + actual_qty
945 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530946
947 if actual_qty > 0:
948 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530949 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530950 outgoing_rate = get_batch_incoming_rate(
951 item_code=sle.item_code,
952 warehouse=sle.warehouse,
953 batch_no=sle.batch_no,
954 posting_date=sle.posting_date,
955 posting_time=sle.posting_time,
956 creation=sle.creation,
957 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530958 if outgoing_rate is None:
959 # This can *only* happen if qty available for the batch is zero.
960 # in such case fall back various other rates.
961 # future entries will correct the overall accounting as each
962 # batch individually uses moving average rates.
963 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530964 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530965
Ankush Menat494bd9e2022-03-28 18:52:46 +0530966 self.wh_data.stock_value = round_off_if_near_zero(
967 self.wh_data.stock_value + stock_value_difference
968 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530969 if self.wh_data.qty_after_transaction:
970 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530971
Javier Wong9b11d9b2017-04-14 18:24:04 +0800972 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530973 ref_item_dt = ""
974
975 if voucher_type == "Stock Entry":
976 ref_item_dt = voucher_type + " Detail"
977 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
978 ref_item_dt = voucher_type + " Item"
979
980 if ref_item_dt:
981 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
982 else:
983 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530984
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530985 def get_fallback_rate(self, sle) -> float:
986 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530987 This should only get used for negative stock."""
988 return get_valuation_rate(
989 sle.item_code,
990 sle.warehouse,
991 sle.voucher_type,
992 sle.voucher_no,
993 self.allow_zero_rate,
994 currency=erpnext.get_company_currency(sle.company),
995 company=sle.company,
996 batch_no=sle.batch_no,
997 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530998
Nabin Haita77b8c92020-12-21 14:45:50 +0530999 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301000 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301001 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1002 sle = sle[0] if sle else frappe._dict()
1003 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301004
Nabin Haita77b8c92020-12-21 14:45:50 +05301005 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301006 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301007 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301008
1009 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301010 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301011 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301012 deficiency = min(e["diff"] for e in exceptions)
s-aga-r73f16752023-04-12 18:58:04 +05301013 msg_prefix = _("As {} units are reserved").format(frappe.bold(self.reserved_stock))
Rushabh Mehta538607e2016-06-12 11:03:00 +05301014
Ankush Menat494bd9e2022-03-28 18:52:46 +05301015 if (
1016 exceptions[0]["voucher_type"],
1017 exceptions[0]["voucher_no"],
1018 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301019
Nabin Haita77b8c92020-12-21 14:45:50 +05301020 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301021 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301022 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1023 frappe.get_desk_link("Warehouse", warehouse),
1024 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301025 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301026 msg = _(
1027 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1028 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301029 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301030 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1031 frappe.get_desk_link("Warehouse", warehouse),
1032 exceptions[0]["posting_date"],
1033 exceptions[0]["posting_time"],
1034 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1035 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301036
Nabin Haita77b8c92020-12-21 14:45:50 +05301037 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301038 if self.reserved_stock:
s-aga-r73f16752023-04-12 18:58:04 +05301039 msg = "{0}, {1}".format(msg_prefix, msg)
s-aga-rf0acb202023-04-12 14:13:54 +05301040
Nabin Haita77b8c92020-12-21 14:45:50 +05301041 msg_list.append(msg)
1042
1043 if msg_list:
1044 message = "\n\n".join(msg_list)
1045 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301046 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301047 else:
1048 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301049
Nabin Haita77b8c92020-12-21 14:45:50 +05301050 def update_bin(self):
1051 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301052 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301053 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301054
Ankush Menat494bd9e2022-03-28 18:52:46 +05301055 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301056 if data.valuation_rate is not None:
1057 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301058 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301059
marination8418c4b2021-06-22 21:35:25 +05301060
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301061def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301062 """get stock ledger entries filtered by specific posting datetime conditions"""
1063
Ankush Menat494bd9e2022-03-28 18:52:46 +05301064 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301065 if not args.get("posting_date"):
1066 args["posting_date"] = "1900-01-01"
1067 if not args.get("posting_time"):
1068 args["posting_time"] = "00:00"
1069
1070 voucher_condition = ""
1071 if exclude_current_voucher:
1072 voucher_no = args.get("voucher_no")
1073 voucher_condition = f"and voucher_no != '{voucher_no}'"
1074
Ankush Menat494bd9e2022-03-28 18:52:46 +05301075 sle = frappe.db.sql(
1076 """
marination8418c4b2021-06-22 21:35:25 +05301077 select *, timestamp(posting_date, posting_time) as "timestamp"
1078 from `tabStock Ledger Entry`
1079 where item_code = %(item_code)s
1080 and warehouse = %(warehouse)s
1081 and is_cancelled = 0
1082 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301083 and (
1084 posting_date < %(posting_date)s or
1085 (
1086 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301087 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301088 )
1089 )
marination8418c4b2021-06-22 21:35:25 +05301090 order by timestamp(posting_date, posting_time) desc, creation desc
1091 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301092 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301093 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301094 ),
1095 args,
1096 as_dict=1,
1097 )
marination8418c4b2021-06-22 21:35:25 +05301098
1099 return sle[0] if sle else frappe._dict()
1100
Ankush Menat494bd9e2022-03-28 18:52:46 +05301101
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301102def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301103 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301104 get the last sle on or before the current time-bucket,
1105 to get actual qty before transaction, this function
1106 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301107
Ankush Menat494bd9e2022-03-28 18:52:46 +05301108 args = {
1109 "item_code": "ABC",
1110 "warehouse": "XYZ",
1111 "posting_date": "2012-12-12",
1112 "posting_time": "12:00",
1113 "sle": "name of reference Stock Ledger Entry"
1114 }
Anand Doshi1b531862013-01-10 19:29:51 +05301115 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301116 args["name"] = args.get("sle", None) or ""
1117 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301118 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301119
Ankush Menat494bd9e2022-03-28 18:52:46 +05301120
1121def get_stock_ledger_entries(
1122 previous_sle,
1123 operator=None,
1124 order="desc",
1125 limit=None,
1126 for_update=False,
1127 debug=False,
1128 check_serial_no=True,
1129):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301130 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301131 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1132 operator
1133 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301134 if previous_sle.get("warehouse"):
1135 conditions += " and warehouse = %(warehouse)s"
1136 elif previous_sle.get("warehouse_condition"):
1137 conditions += " and " + previous_sle.get("warehouse_condition")
1138
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301139 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301140 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1141 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301142 conditions += (
1143 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301144 (
1145 serial_no = {0}
1146 or serial_no like {1}
1147 or serial_no like {2}
1148 or serial_no like {3}
1149 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301150 """
1151 ).format(
1152 frappe.db.escape(serial_no),
1153 frappe.db.escape("{}\n%".format(serial_no)),
1154 frappe.db.escape("%\n{}".format(serial_no)),
1155 frappe.db.escape("%\n{}\n%".format(serial_no)),
1156 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301157
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301158 if not previous_sle.get("posting_date"):
1159 previous_sle["posting_date"] = "1900-01-01"
1160 if not previous_sle.get("posting_time"):
1161 previous_sle["posting_time"] = "00:00"
1162
1163 if operator in (">", "<=") and previous_sle.get("name"):
1164 conditions += " and name!=%(name)s"
1165
Ankush Menat494bd9e2022-03-28 18:52:46 +05301166 return frappe.db.sql(
1167 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301168 select *, timestamp(posting_date, posting_time) as "timestamp"
1169 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301170 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301171 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301172 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301173 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301174 %(limit)s %(for_update)s"""
1175 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301176 "conditions": conditions,
1177 "limit": limit or "",
1178 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301179 "order": order,
1180 },
1181 previous_sle,
1182 as_dict=1,
1183 debug=debug,
1184 )
1185
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301186
Nabin Haita77b8c92020-12-21 14:45:50 +05301187def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301188 return frappe.db.get_value(
1189 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301190 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301191 [
1192 "item_code",
1193 "warehouse",
1194 "posting_date",
1195 "posting_time",
1196 "timestamp(posting_date, posting_time) as timestamp",
1197 ],
1198 as_dict=1,
1199 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301200
Ankush Menatce0514c2022-02-15 11:41:41 +05301201
Ankush Menat494bd9e2022-03-28 18:52:46 +05301202def get_batch_incoming_rate(
1203 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1204):
1205
Ankush Menat102fff22022-02-19 15:51:04 +05301206 sle = frappe.qb.DocType("Stock Ledger Entry")
1207
Ankush Menate1c16872022-04-21 20:01:48 +05301208 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301209 posting_date, posting_time
1210 )
Ankush Menat102fff22022-02-19 15:51:04 +05301211 if creation:
1212 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301213 CombineDatetime(sle.posting_date, sle.posting_time)
1214 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301215 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301216
1217 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301218 frappe.qb.from_(sle)
1219 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1220 .where(
1221 (sle.item_code == item_code)
1222 & (sle.warehouse == warehouse)
1223 & (sle.batch_no == batch_no)
1224 & (sle.is_cancelled == 0)
1225 )
1226 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301227 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301228
1229 if batch_details and batch_details[0].batch_qty:
1230 return batch_details[0].batch_value / batch_details[0].batch_qty
1231
1232
Ankush Menat494bd9e2022-03-28 18:52:46 +05301233def get_valuation_rate(
1234 item_code,
1235 warehouse,
1236 voucher_type,
1237 voucher_no,
1238 allow_zero_rate=False,
1239 currency=None,
1240 company=None,
1241 raise_error_if_no_rate=True,
1242 batch_no=None,
1243):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301244
Ankush Menatf7ffe042021-11-01 13:21:14 +05301245 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301246 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301247
Ankush Menat342d09a2022-02-19 14:28:51 +05301248 last_valuation_rate = None
1249
1250 # Get moving average rate of a specific batch number
1251 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301252 last_valuation_rate = frappe.db.sql(
1253 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301254 select sum(stock_value_difference) / sum(actual_qty)
1255 from `tabStock Ledger Entry`
1256 where
1257 item_code = %s
1258 AND warehouse = %s
1259 AND batch_no = %s
1260 AND is_cancelled = 0
1261 AND NOT (voucher_no = %s AND voucher_type = %s)
1262 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301263 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1264 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301265
Ankush Menatf7ffe042021-11-01 13:21:14 +05301266 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301267 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301268 last_valuation_rate = frappe.db.sql(
1269 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301270 from `tabStock Ledger Entry` force index (item_warehouse)
1271 where
1272 item_code = %s
1273 AND warehouse = %s
1274 AND valuation_rate >= 0
1275 AND is_cancelled = 0
1276 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301277 order by posting_date desc, posting_time desc, name desc limit 1""",
1278 (item_code, warehouse, voucher_no, voucher_type),
1279 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301280
Nabin Haita645f362018-03-01 10:31:24 +05301281 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301282 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301283
1284 # If negative stock allowed, and item delivered without any incoming entry,
1285 # system does not found any SLE, then take valuation rate from Item
1286 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301287
1288 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301289 # try Item Standard rate
1290 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301291
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301292 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301293 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301294 valuation_rate = frappe.db.get_value(
1295 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1296 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301297
Ankush Menat494bd9e2022-03-28 18:52:46 +05301298 if (
1299 not allow_zero_rate
1300 and not valuation_rate
1301 and raise_error_if_no_rate
1302 and cint(erpnext.is_perpetual_inventory_enabled(company))
1303 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301304 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301305
Ankush Menat494bd9e2022-03-28 18:52:46 +05301306 message = _(
1307 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1308 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301309 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301310 solutions = (
1311 "<li>"
1312 + _(
1313 "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."
1314 ).format(voucher_type)
1315 + "</li>"
1316 )
1317 solutions += (
1318 "<li>"
1319 + _("If not, you can Cancel / Submit this entry")
1320 + " {0} ".format(frappe.bold("after"))
1321 + _("performing either one below:")
1322 + "</li>"
1323 )
Marica97715f22020-05-11 20:45:37 +05301324 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1325 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1326 msg = message + solutions + sub_solutions + "</li>"
1327
1328 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301329
1330 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301331
Ankush Menat494bd9e2022-03-28 18:52:46 +05301332
Ankush Menate7109c12021-08-26 16:40:45 +05301333def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301334 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301335 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301336 qty_shift = args.actual_qty
1337
Ankush Menat7c839c42022-05-06 12:09:08 +05301338 args["time_format"] = "%H:%i:%s"
1339
marination8418c4b2021-06-22 21:35:25 +05301340 # find difference/shift in qty caused by stock reconciliation
1341 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301342 qty_shift = get_stock_reco_qty_shift(args)
1343
1344 # find the next nearest stock reco so that we only recalculate SLEs till that point
1345 next_stock_reco_detail = get_next_stock_reco(args)
1346 if next_stock_reco_detail:
1347 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301348 if detail.batch_no:
1349 regenerate_sle_for_batch_stock_reco(detail)
1350
marination40389772021-07-02 17:13:45 +05301351 # add condition to update SLEs before this date & time
1352 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301353
Ankush Menat494bd9e2022-03-28 18:52:46 +05301354 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301355 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301356 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301357 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301358 where
1359 item_code = %(item_code)s
1360 and warehouse = %(warehouse)s
1361 and voucher_no != %(voucher_no)s
1362 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301363 and (
1364 posting_date > %(posting_date)s or
1365 (
1366 posting_date = %(posting_date)s and
1367 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1368 )
1369 )
marination40389772021-07-02 17:13:45 +05301370 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301371 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301372 args,
1373 )
Nabin Hait186a0452021-02-18 14:14:21 +05301374
1375 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1376
Ankush Menat494bd9e2022-03-28 18:52:46 +05301377
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301378def regenerate_sle_for_batch_stock_reco(detail):
1379 doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
1380 doc.docstatus = 2
1381 doc.update_stock_ledger()
1382
1383 doc.recalculate_current_qty(detail.item_code, detail.batch_no)
1384 doc.docstatus = 1
1385 doc.update_stock_ledger()
1386
1387
marination40389772021-07-02 17:13:45 +05301388def get_stock_reco_qty_shift(args):
1389 stock_reco_qty_shift = 0
1390 if args.get("is_cancelled"):
1391 if args.get("previous_qty_after_transaction"):
1392 # get qty (balance) that was set at submission
1393 last_balance = args.get("previous_qty_after_transaction")
1394 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1395 else:
1396 stock_reco_qty_shift = flt(args.actual_qty)
1397 else:
1398 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301399 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301400 "qty_after_transaction"
1401 )
marination40389772021-07-02 17:13:45 +05301402
1403 if last_balance is not None:
1404 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1405 else:
1406 stock_reco_qty_shift = args.qty_after_transaction
1407
1408 return stock_reco_qty_shift
1409
Ankush Menat494bd9e2022-03-28 18:52:46 +05301410
marination40389772021-07-02 17:13:45 +05301411def get_next_stock_reco(args):
1412 """Returns next nearest stock reconciliaton's details."""
1413
Ankush Menat494bd9e2022-03-28 18:52:46 +05301414 return frappe.db.sql(
1415 """
marination40389772021-07-02 17:13:45 +05301416 select
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301417 name, posting_date, posting_time, creation, voucher_no, item_code, batch_no, actual_qty
marination40389772021-07-02 17:13:45 +05301418 from
marination8c441262021-07-02 17:46:05 +05301419 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301420 where
1421 item_code = %(item_code)s
1422 and warehouse = %(warehouse)s
1423 and voucher_type = 'Stock Reconciliation'
1424 and voucher_no != %(voucher_no)s
1425 and is_cancelled = 0
1426 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1427 or (
1428 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1429 and creation > %(creation)s
1430 )
1431 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301432 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301433 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301434 """,
1435 args,
1436 as_dict=1,
1437 )
1438
marination40389772021-07-02 17:13:45 +05301439
1440def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301441 return f"""
1442 and
1443 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1444 or (
1445 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1446 and creation < '{detail.creation}'
1447 )
1448 )"""
1449
Ankush Menat494bd9e2022-03-28 18:52:46 +05301450
Ankush Menate7109c12021-08-26 16:40:45 +05301451def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301452 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301453 return
1454 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1455 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301456
Ankush Menat5eba5752021-12-07 23:03:52 +05301457 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301458
1459 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301460 message = _(
1461 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1462 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301463 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301464 frappe.get_desk_link("Item", args.item_code),
1465 frappe.get_desk_link("Warehouse", args.warehouse),
1466 neg_sle[0]["posting_date"],
1467 neg_sle[0]["posting_time"],
1468 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1469 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301470
Ankush Menat494bd9e2022-03-28 18:52:46 +05301471 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301472
1473 if not args.batch_no:
1474 return
1475
1476 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301477 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301478 message = _(
1479 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1480 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301481 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301482 frappe.get_desk_link("Batch", args.batch_no),
1483 frappe.get_desk_link("Warehouse", args.warehouse),
1484 neg_batch_sle[0]["posting_date"],
1485 neg_batch_sle[0]["posting_time"],
1486 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1487 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301488 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301489
Nabin Haita77b8c92020-12-21 14:45:50 +05301490
Maricad6078aa2022-06-17 15:13:13 +05301491def is_negative_with_precision(neg_sle, is_batch=False):
1492 """
1493 Returns whether system precision rounded qty is insufficient.
1494 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1495 """
1496
1497 if not neg_sle:
1498 return False
1499
1500 field = "cumulative_total" if is_batch else "qty_after_transaction"
1501 precision = cint(frappe.db.get_default("float_precision")) or 2
1502 qty_deficit = flt(neg_sle[0][field], precision)
1503
1504 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1505
1506
Nabin Haita77b8c92020-12-21 14:45:50 +05301507def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301508 return frappe.db.sql(
1509 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301510 select
1511 qty_after_transaction, posting_date, posting_time,
1512 voucher_type, voucher_no
1513 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301514 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301515 item_code = %(item_code)s
1516 and warehouse = %(warehouse)s
1517 and voucher_no != %(voucher_no)s
1518 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1519 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301520 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301521 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301522 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301523 """,
1524 args,
1525 as_dict=1,
1526 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301527
Ankush Menat5eba5752021-12-07 23:03:52 +05301528
1529def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301530 return frappe.db.sql(
1531 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301532 with batch_ledger as (
1533 select
1534 posting_date, posting_time, voucher_type, voucher_no,
1535 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1536 from `tabStock Ledger Entry`
1537 where
1538 item_code = %(item_code)s
1539 and warehouse = %(warehouse)s
1540 and batch_no=%(batch_no)s
1541 and is_cancelled = 0
1542 order by posting_date, posting_time, creation
1543 )
1544 select * from batch_ledger
1545 where
1546 cumulative_total < 0.0
1547 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1548 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301549 """,
1550 args,
1551 as_dict=1,
1552 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301553
1554
1555def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1556 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1557 return True
1558 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1559 return True
1560 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301561
1562
1563def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1564 """
1565 For inter company transfer, incoming rate is the average of the outgoing rate
1566 """
1567 rate = 0.0
1568
1569 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1570
1571 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1572
1573 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1574
1575 if reference_name:
1576 rate = frappe.get_cached_value(
1577 doctype,
1578 reference_name,
1579 "incoming_rate",
1580 )
1581
1582 return rate