blob: c2e700a86b93a7c8d4a435ad5a27a893f9869509 [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
98 if args.get("is_cancelled") and via_landed_cost_voucher:
99 return
100
101 # Reposts only current voucher SL Entries
102 # Updates valuation rate, stock value, stock queue for current transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530103 update_entries_after(
104 {
105 "item_code": args.get("item_code"),
106 "warehouse": args.get("warehouse"),
107 "posting_date": args.get("posting_date"),
108 "posting_time": args.get("posting_time"),
109 "voucher_type": args.get("voucher_type"),
110 "voucher_no": args.get("voucher_no"),
111 "sle_id": args.get("name"),
112 "creation": args.get("creation"),
113 },
114 allow_negative_stock=allow_negative_stock,
115 via_landed_cost_voucher=via_landed_cost_voucher,
116 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530117
118 # update qty in future sle and Validate negative qty
119 update_qty_in_future_sle(args, allow_negative_stock)
120
Nabin Haitadeb9762014-10-06 11:53:52 +0530121
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530122def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530123 return frappe._dict(
124 {
125 "voucher_type": row.get("voucher_type"),
126 "voucher_no": row.get("voucher_no"),
127 "posting_date": row.get("posting_date"),
128 "posting_time": row.get("posting_time"),
129 }
130 )
131
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530132
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530133def validate_serial_no(sle):
134 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530135
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530136 for sn in get_serial_nos(sle.serial_no):
137 args = copy.deepcopy(sle)
138 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530139 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530140
141 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530142 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530143 voucher_type = frappe.bold(row.voucher_type)
144 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530145 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530146
147 if vouchers:
148 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530149 msg = (
150 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
151 The list of the transactions are as below."""
152 + "<br><br><ul><li>"
153 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530154
Ankush Menat494bd9e2022-03-28 18:52:46 +0530155 msg += "</li><li>".join(vouchers)
156 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530157
Ankush Menat494bd9e2022-03-28 18:52:46 +0530158 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530159 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
160
Ankush Menat494bd9e2022-03-28 18:52:46 +0530161
Nabin Hait186a0452021-02-18 14:14:21 +0530162def validate_cancellation(args):
163 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530164 repost_entry = frappe.db.get_value(
165 "Repost Item Valuation",
166 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
167 ["name", "status"],
168 as_dict=1,
169 )
Nabin Hait186a0452021-02-18 14:14:21 +0530170
171 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530172 if repost_entry.status == "In Progress":
173 frappe.throw(
174 _(
175 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
176 )
177 )
178 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530179 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530180 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530181 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530182 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530183
Ankush Menat494bd9e2022-03-28 18:52:46 +0530184
Nabin Hait9653f602013-08-20 15:37:33 +0530185def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530186 frappe.db.sql(
187 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530188 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530189 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530190 (now(), frappe.session.user, voucher_type, voucher_no),
191 )
192
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530193
Nabin Hait54c865e2015-03-27 15:38:31 +0530194def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530195 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530196 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530197 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530198 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530199 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530200 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530201 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530202
Ankush Menat494bd9e2022-03-28 18:52:46 +0530203
204def repost_future_sle(
205 args=None,
206 voucher_type=None,
207 voucher_no=None,
208 allow_negative_stock=None,
209 via_landed_cost_voucher=False,
210 doc=None,
211):
Nabin Haita77b8c92020-12-21 14:45:50 +0530212 if not args and voucher_type and voucher_no:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530213 args = get_items_to_be_repost(voucher_type, voucher_no, doc)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530214
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530215 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Ankush Menatecdb4932022-04-17 19:06:13 +0530216 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530217
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530218 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530219 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530220 validate_item_warehouse(args[i])
221
Ankush Menat494bd9e2022-03-28 18:52:46 +0530222 obj = update_entries_after(
223 {
224 "item_code": args[i].get("item_code"),
225 "warehouse": args[i].get("warehouse"),
226 "posting_date": args[i].get("posting_date"),
227 "posting_time": args[i].get("posting_time"),
228 "creation": args[i].get("creation"),
229 "distinct_item_warehouses": distinct_item_warehouses,
230 },
231 allow_negative_stock=allow_negative_stock,
232 via_landed_cost_voucher=via_landed_cost_voucher,
233 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530234 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530235
Ankush Menat494bd9e2022-03-28 18:52:46 +0530236 distinct_item_warehouses[
237 (args[i].get("item_code"), args[i].get("warehouse"))
238 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530239
Nabin Hait97bce3a2021-07-12 13:24:43 +0530240 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530241 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530242 if ("args_idx" not in data and not data.reposting_status) or (
243 data.sle_changed and data.reposting_status
244 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530245 data.args_idx = len(args)
246 args.append(data.sle)
247 elif data.sle_changed and not data.reposting_status:
248 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530249
Nabin Hait97bce3a2021-07-12 13:24:43 +0530250 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530251 i += 1
252
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530253 if doc and i % 2 == 0:
Ankush Menatecdb4932022-04-17 19:06:13 +0530254 update_args_in_repost_item_valuation(
255 doc, i, args, distinct_item_warehouses, affected_transactions
256 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530257
258 if doc and args:
Ankush Menatecdb4932022-04-17 19:06:13 +0530259 update_args_in_repost_item_valuation(
260 doc, i, args, distinct_item_warehouses, affected_transactions
261 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530262
Ankush Menat494bd9e2022-03-28 18:52:46 +0530263
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530264def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530265 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530266 if not args.get(field):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530267 validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530268 frappe.throw(_(validation_msg))
269
Ankush Menat494bd9e2022-03-28 18:52:46 +0530270
Ankush Menatecdb4932022-04-17 19:06:13 +0530271def update_args_in_repost_item_valuation(
272 doc, index, args, distinct_item_warehouses, affected_transactions
273):
Ankush Menatecdb4932022-04-17 19:06:13 +0530274 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530275 {
276 "items_to_be_repost": json.dumps(args, default=str),
277 "distinct_item_and_warehouse": json.dumps(
278 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
279 ),
280 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530281 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530282 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530283 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530284
Ankush Menatecdb4932022-04-17 19:06:13 +0530285 if not frappe.flags.in_test:
286 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530287
Ankush Menat494bd9e2022-03-28 18:52:46 +0530288 frappe.publish_realtime(
289 "item_reposting_progress",
290 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
291 )
292
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530293
294def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
295 if doc and doc.items_to_be_repost:
296 return json.loads(doc.items_to_be_repost) or []
297
Ankush Menat494bd9e2022-03-28 18:52:46 +0530298 return frappe.db.get_all(
299 "Stock Ledger Entry",
Nabin Haita77b8c92020-12-21 14:45:50 +0530300 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
Nabin Hait186a0452021-02-18 14:14:21 +0530301 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
Nabin Haita77b8c92020-12-21 14:45:50 +0530302 order_by="creation asc",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530303 group_by="item_code, warehouse",
Nabin Haita77b8c92020-12-21 14:45:50 +0530304 )
Nabin Hait74c281c2013-08-19 16:17:18 +0530305
Ankush Menat494bd9e2022-03-28 18:52:46 +0530306
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530307def get_distinct_item_warehouse(args=None, doc=None):
308 distinct_item_warehouses = {}
309 if doc and doc.distinct_item_and_warehouse:
310 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530311 distinct_item_warehouses = {
312 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
313 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530314 else:
315 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530316 distinct_item_warehouses.setdefault(
317 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
318 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530319
320 return distinct_item_warehouses
321
Ankush Menat494bd9e2022-03-28 18:52:46 +0530322
Ankush Menatecdb4932022-04-17 19:06:13 +0530323def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
324 if not doc.affected_transactions:
325 return set()
326
327 transactions = frappe.parse_json(doc.affected_transactions)
328 return {tuple(transaction) for transaction in transactions}
329
330
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530331def get_current_index(doc=None):
332 if doc and doc.current_index:
333 return doc.current_index
334
Ankush Menat494bd9e2022-03-28 18:52:46 +0530335
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530336class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530337 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530338 update valution rate and qty after transaction
339 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530340
Ankush Menat494bd9e2022-03-28 18:52:46 +0530341 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530342
Ankush Menat494bd9e2022-03-28 18:52:46 +0530343 args = {
344 "item_code": "ABC",
345 "warehouse": "XYZ",
346 "posting_date": "2012-12-12",
347 "posting_time": "12:00"
348 }
Nabin Hait902e8602013-01-08 18:29:24 +0530349 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530350
351 def __init__(
352 self,
353 args,
354 allow_zero_rate=False,
355 allow_negative_stock=None,
356 via_landed_cost_voucher=False,
357 verbose=1,
358 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530359 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530360 self.verbose = verbose
361 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530362 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530363 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530364 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
365 item_code=self.item_code
366 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530367
Nabin Haita77b8c92020-12-21 14:45:50 +0530368 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530369 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530370 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530371
Nabin Haita77b8c92020-12-21 14:45:50 +0530372 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530373 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530374 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530375
376 self.new_items_found = False
377 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530378 self.affected_transactions: Set[Tuple[str, str]] = set()
Nabin Haita77b8c92020-12-21 14:45:50 +0530379
380 self.data = frappe._dict()
381 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530382 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530383
Maricad6078aa2022-06-17 15:13:13 +0530384 def set_precision(self):
385 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
386 self.currency_precision = get_field_precision(
387 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530388 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530389
390 def initialize_previous_data(self, args):
391 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530392 Get previous sl entries for current item for each related warehouse
393 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530394
Ankush Menat494bd9e2022-03-28 18:52:46 +0530395 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530396
Ankush Menat494bd9e2022-03-28 18:52:46 +0530397 self.data = {
398 warehouse1: {
399 'previus_sle': {},
400 'qty_after_transaction': 10,
401 'valuation_rate': 100,
402 'stock_value': 1000,
403 'prev_stock_value': 1000,
404 'stock_queue': '[[10, 100]]',
405 'stock_value_difference': 1000
406 }
407 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530408
409 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530410 self.data.setdefault(args.warehouse, frappe._dict())
411 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530412 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530413 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530414
Ankush Menatc1d986a2021-08-31 19:43:42 +0530415 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
416 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
417
Ankush Menat494bd9e2022-03-28 18:52:46 +0530418 warehouse_dict.update(
419 {
420 "prev_stock_value": previous_sle.stock_value or 0.0,
421 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
422 "stock_value_difference": 0.0,
423 }
424 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530425
Nabin Haita77b8c92020-12-21 14:45:50 +0530426 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530427 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530428
Nabin Haita77b8c92020-12-21 14:45:50 +0530429 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530430 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530431 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530432 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530433 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530434 entries_to_fix = self.get_future_entries_to_fix()
435
436 i = 0
437 while i < len(entries_to_fix):
438 sle = entries_to_fix[i]
439 i += 1
440
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530441 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530442
Nabin Haita77b8c92020-12-21 14:45:50 +0530443 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530444 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530445
Nabin Hait186a0452021-02-18 14:14:21 +0530446 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530447
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530448 if self.exceptions:
449 self.raise_exceptions()
450
Nabin Hait186a0452021-02-18 14:14:21 +0530451 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530452 sl_entries = self.get_sle_against_current_voucher()
453 for sle in sl_entries:
454 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530455
Nabin Haita77b8c92020-12-21 14:45:50 +0530456 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530457 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530458
Ankush Menat494bd9e2022-03-28 18:52:46 +0530459 return frappe.db.sql(
460 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530461 select
462 *, timestamp(posting_date, posting_time) as "timestamp"
463 from
464 `tabStock Ledger Entry`
465 where
466 item_code = %(item_code)s
467 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530468 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530469 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
470
Nabin Haita77b8c92020-12-21 14:45:50 +0530471 order by
472 creation ASC
473 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530474 """,
475 self.args,
476 as_dict=1,
477 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530478
Nabin Haita77b8c92020-12-21 14:45:50 +0530479 def get_future_entries_to_fix(self):
480 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530481 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
482 {"item_code": self.item_code, "warehouse": self.args.warehouse}
483 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530484
Nabin Haita77b8c92020-12-21 14:45:50 +0530485 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530486
Nabin Haita77b8c92020-12-21 14:45:50 +0530487 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530488 dependant_sle = get_sle_by_voucher_detail_no(
489 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
490 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530491
Nabin Haita77b8c92020-12-21 14:45:50 +0530492 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530493 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530494 elif (
495 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
496 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530497 return entries_to_fix
498 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530499 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530500 return entries_to_fix
501 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
502 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530503 else:
504 return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
505
506 def update_distinct_item_warehouses(self, dependant_sle):
507 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530508 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530509 if key not in self.distinct_item_warehouses:
510 self.distinct_item_warehouses[key] = val
511 self.new_items_found = True
512 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530513 existing_sle_posting_date = (
514 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
515 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530516 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
517 val.sle_changed = True
518 self.distinct_item_warehouses[key] = val
519 self.new_items_found = True
520
521 def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
Nabin Haita77b8c92020-12-21 14:45:50 +0530522 self.initialize_previous_data(dependant_sle)
523
Ankush Menat494bd9e2022-03-28 18:52:46 +0530524 args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict(
525 {"item_code": self.item_code, "warehouse": dependant_sle.warehouse}
526 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530527 future_sle_for_dependant = list(self.get_sle_after_datetime(args))
528
529 entries_to_fix.extend(future_sle_for_dependant)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530530 return sorted(entries_to_fix, key=lambda k: k["timestamp"])
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530531
532 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530533 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
534
Nabin Haita77b8c92020-12-21 14:45:50 +0530535 # previous sle data for this warehouse
536 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530537 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530538
Anand Doshi0dc79f42015-04-06 12:59:34 +0530539 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 +0530540 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530541 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530542 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530543 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530544 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530545
Nabin Haita77b8c92020-12-21 14:45:50 +0530546 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530547 if not self.args.get("sle_id"):
548 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530549
Ankush Menat66bf21f2022-01-16 20:45:59 +0530550 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530551 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530552 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530553 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530554 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530555
Ankush Menat494bd9e2022-03-28 18:52:46 +0530556 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
557 self.wh_data.valuation_rate
558 )
559 elif sle.batch_no and frappe.db.get_value(
560 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
561 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530562 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530563 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530564 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530565 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530566 self.wh_data.valuation_rate = sle.valuation_rate
567 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530568 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
569 self.wh_data.valuation_rate
570 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530571 if self.valuation_method != "Moving Average":
572 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530573 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530574 if self.valuation_method == "Moving Average":
575 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530576 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
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 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530580 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530581 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530582
Rushabh Mehta54047782013-12-26 11:07:46 +0530583 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530584 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530585 if not self.wh_data.qty_after_transaction:
586 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530587 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
588 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530589
Nabin Hait902e8602013-01-08 18:29:24 +0530590 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530591 sle.qty_after_transaction = self.wh_data.qty_after_transaction
592 sle.valuation_rate = self.wh_data.valuation_rate
593 sle.stock_value = self.wh_data.stock_value
594 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530595 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530596 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530597 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530598
Ankush Menat701878f2022-03-01 18:08:29 +0530599 if not self.args.get("sle_id"):
600 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530601
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530602 def validate_negative_stock(self, sle):
603 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530604 validate negative stock for entries current datetime onwards
605 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530606 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530607 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Maricad6078aa2022-06-17 15:13:13 +0530608 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530609
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530610 if diff < 0 and abs(diff) > 0.0001:
611 # negative stock!
612 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530613 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530614 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530615 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530616 return True
617
Nabin Haita77b8c92020-12-21 14:45:50 +0530618 def get_dynamic_incoming_outgoing_rate(self, sle):
619 # Get updated incoming/outgoing rate from transaction
620 if sle.recalculate_rate:
621 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
622
623 if flt(sle.actual_qty) >= 0:
624 sle.incoming_rate = rate
625 else:
626 sle.outgoing_rate = rate
627
628 def get_incoming_outgoing_rate_from_transaction(self, sle):
629 rate = 0
630 # Material Transfer, Repack, Manufacturing
631 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530632 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530633 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
634 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530635 elif sle.voucher_type in (
636 "Purchase Receipt",
637 "Purchase Invoice",
638 "Delivery Note",
639 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530640 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530641 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530642 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530643 from erpnext.controllers.sales_and_purchase_return import (
644 get_rate_for_return, # don't move this import to top
645 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530646
647 rate = get_rate_for_return(
648 sle.voucher_type,
649 sle.voucher_no,
650 sle.item_code,
651 voucher_detail_no=sle.voucher_detail_no,
652 sle=sle,
653 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530654 else:
655 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530656 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530657 elif sle.voucher_type == "Subcontracting Receipt":
658 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530659 else:
660 rate_field = "incoming_rate"
661
662 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530663 item_code, incoming_rate = frappe.db.get_value(
664 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
665 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530666
667 if item_code == sle.item_code:
668 rate = incoming_rate
669 else:
670 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
671 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530672 elif sle == "Subcontracting Receipt":
673 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530674 else:
675 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530676
Ankush Menat494bd9e2022-03-28 18:52:46 +0530677 rate = frappe.db.get_value(
678 ref_doctype,
679 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
680 rate_field,
681 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530682
683 return rate
684
685 def update_outgoing_rate_on_transaction(self, sle):
686 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530687 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
688 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530689 """
690 if sle.actual_qty and sle.voucher_detail_no:
691 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
692
693 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
694 self.update_rate_on_stock_entry(sle, outgoing_rate)
695 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
696 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
697 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
698 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530699 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
700 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530701
702 def update_rate_on_stock_entry(self, sle, outgoing_rate):
703 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
704
Ankush Menat701878f2022-03-01 18:08:29 +0530705 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
706 if not sle.dependant_sle_voucher_detail_no:
707 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530708
709 def recalculate_amounts_in_stock_entry(self, voucher_no):
710 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530711 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
712 stock_entry.db_update()
713 for d in stock_entry.items:
714 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530715
Nabin Haita77b8c92020-12-21 14:45:50 +0530716 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
717 # Update item's incoming rate on transaction
718 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
719 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530720 frappe.db.set_value(
721 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
722 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530723 else:
724 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530725 frappe.db.set_value(
726 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530727 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530728 "incoming_rate",
729 outgoing_rate,
730 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530731
732 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
733 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530734 frappe.db.set_value(
735 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
736 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530737 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530738 frappe.db.set_value(
739 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
740 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530741
s-aga-r6d89b2f2022-06-18 15:46:59 +0530742 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
743 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
744 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
745 doc.update_valuation_rate(reset_outgoing_rate=False)
746 for d in doc.items + doc.supplied_items:
747 d.db_update()
748
Sagar Sharma323bdf82022-05-17 15:14:07 +0530749 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
750 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
751 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
752 else:
753 frappe.db.set_value(
754 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
755 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530756
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530757 def get_serialized_values(self, sle):
758 incoming_rate = flt(sle.incoming_rate)
759 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530760 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530761
762 if incoming_rate < 0:
763 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530764 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530765
Nabin Hait2620bf42016-02-29 11:30:27 +0530766 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530767 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530768 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530769 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530770 # In case of delivery/stock issue, get average purchase rate
771 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530772 if not sle.is_cancelled:
773 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
774 stock_value_change = -1 * outgoing_value
775 else:
776 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530777
Nabin Haita77b8c92020-12-21 14:45:50 +0530778 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530779
Nabin Hait2620bf42016-02-29 11:30:27 +0530780 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530781 new_stock_value = (
782 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
783 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530784 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530785 # calculate new valuation rate only if stock value is positive
786 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530787 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530788
Nabin Haita77b8c92020-12-21 14:45:50 +0530789 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530790 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
791 sle.voucher_type, sle.voucher_detail_no
792 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530793 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530794 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530795
Nabin Hait328c4f92020-01-02 19:00:32 +0530796 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
797 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530798 all_serial_nos = frappe.get_all(
799 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
800 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530801
Ankush Menat494bd9e2022-03-28 18:52:46 +0530802 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 +0530803
804 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530805 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530806 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530807 incoming_rate = frappe.db.sql(
808 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530809 select incoming_rate
810 from `tabStock Ledger Entry`
811 where
812 company = %s
813 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530814 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530815 and (serial_no = %s
816 or serial_no like %s
817 or serial_no like %s
818 or serial_no like %s
819 )
820 order by posting_date desc
821 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530822 """,
823 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
824 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530825
826 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
827
828 return incoming_values
829
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530830 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530831 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530832 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530833 if new_stock_qty >= 0:
834 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530835 if flt(self.wh_data.qty_after_transaction) <= 0:
836 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530837 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530838 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
839 actual_qty * sle.incoming_rate
840 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530841
Nabin Haita77b8c92020-12-21 14:45:50 +0530842 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530843
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530844 elif sle.outgoing_rate:
845 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530846 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
847 actual_qty * sle.outgoing_rate
848 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530849
Nabin Haita77b8c92020-12-21 14:45:50 +0530850 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530851 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530852 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530853 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530854 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
855 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530856
Nabin Haita77b8c92020-12-21 14:45:50 +0530857 if not self.wh_data.valuation_rate and actual_qty > 0:
858 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530859
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530860 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800861 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530862 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530863 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
864 sle.voucher_type, sle.voucher_detail_no
865 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800866 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530867 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530868
Ankush Menatf089d392022-02-02 12:51:21 +0530869 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530870 incoming_rate = flt(sle.incoming_rate)
871 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530872 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530873
Ankush Menat494bd9e2022-03-28 18:52:46 +0530874 self.wh_data.qty_after_transaction = round_off_if_near_zero(
875 self.wh_data.qty_after_transaction + actual_qty
876 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530877
Ankush Menat97e18a12022-01-15 17:42:25 +0530878 if self.valuation_method == "LIFO":
879 stock_queue = LIFOValuation(self.wh_data.stock_queue)
880 else:
881 stock_queue = FIFOValuation(self.wh_data.stock_queue)
882
Ankush Menatb534fee2022-02-19 20:58:36 +0530883 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
884
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530885 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530886 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530887 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530888
Ankush Menat4b29fb62021-12-18 18:40:22 +0530889 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530890 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
891 sle.voucher_type, sle.voucher_detail_no
892 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530893 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530894 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530895 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530896 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530897
Ankush Menat494bd9e2022-03-28 18:52:46 +0530898 stock_queue.remove_stock(
899 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
900 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530901
Ankush Menatb534fee2022-02-19 20:58:36 +0530902 _qty, stock_value = stock_queue.get_total_stock_and_value()
903
904 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530905
Ankush Menat97e18a12022-01-15 17:42:25 +0530906 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530907 self.wh_data.stock_value = round_off_if_near_zero(
908 self.wh_data.stock_value + stock_value_difference
909 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530910
Nabin Haita77b8c92020-12-21 14:45:50 +0530911 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530912 self.wh_data.stock_queue.append(
913 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
914 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530915
Ankush Menatb534fee2022-02-19 20:58:36 +0530916 if self.wh_data.qty_after_transaction:
917 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
918
Ankush Menatce0514c2022-02-15 11:41:41 +0530919 def update_batched_values(self, sle):
920 incoming_rate = flt(sle.incoming_rate)
921 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530922
Ankush Menat494bd9e2022-03-28 18:52:46 +0530923 self.wh_data.qty_after_transaction = round_off_if_near_zero(
924 self.wh_data.qty_after_transaction + actual_qty
925 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530926
927 if actual_qty > 0:
928 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530929 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530930 outgoing_rate = get_batch_incoming_rate(
931 item_code=sle.item_code,
932 warehouse=sle.warehouse,
933 batch_no=sle.batch_no,
934 posting_date=sle.posting_date,
935 posting_time=sle.posting_time,
936 creation=sle.creation,
937 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530938 if outgoing_rate is None:
939 # This can *only* happen if qty available for the batch is zero.
940 # in such case fall back various other rates.
941 # future entries will correct the overall accounting as each
942 # batch individually uses moving average rates.
943 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530944 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530945
Ankush Menat494bd9e2022-03-28 18:52:46 +0530946 self.wh_data.stock_value = round_off_if_near_zero(
947 self.wh_data.stock_value + stock_value_difference
948 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530949 if self.wh_data.qty_after_transaction:
950 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530951
Javier Wong9b11d9b2017-04-14 18:24:04 +0800952 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530953 ref_item_dt = ""
954
955 if voucher_type == "Stock Entry":
956 ref_item_dt = voucher_type + " Detail"
957 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
958 ref_item_dt = voucher_type + " Item"
959
960 if ref_item_dt:
961 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
962 else:
963 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530964
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530965 def get_fallback_rate(self, sle) -> float:
966 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530967 This should only get used for negative stock."""
968 return get_valuation_rate(
969 sle.item_code,
970 sle.warehouse,
971 sle.voucher_type,
972 sle.voucher_no,
973 self.allow_zero_rate,
974 currency=erpnext.get_company_currency(sle.company),
975 company=sle.company,
976 batch_no=sle.batch_no,
977 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530978
Nabin Haita77b8c92020-12-21 14:45:50 +0530979 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530980 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530981 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
982 sle = sle[0] if sle else frappe._dict()
983 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530984
Nabin Haita77b8c92020-12-21 14:45:50 +0530985 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530986 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530987 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530988
989 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530990 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530991 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530992 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530993
Ankush Menat494bd9e2022-03-28 18:52:46 +0530994 if (
995 exceptions[0]["voucher_type"],
996 exceptions[0]["voucher_no"],
997 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +0530998
Nabin Haita77b8c92020-12-21 14:45:50 +0530999 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301000 abs(deficiency),
1001 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1002 frappe.get_desk_link("Warehouse", warehouse),
1003 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301004 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301005 msg = _(
1006 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1007 ).format(
1008 abs(deficiency),
1009 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1010 frappe.get_desk_link("Warehouse", warehouse),
1011 exceptions[0]["posting_date"],
1012 exceptions[0]["posting_time"],
1013 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1014 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301015
Nabin Haita77b8c92020-12-21 14:45:50 +05301016 if msg:
1017 msg_list.append(msg)
1018
1019 if msg_list:
1020 message = "\n\n".join(msg_list)
1021 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301022 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301023 else:
1024 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301025
Nabin Haita77b8c92020-12-21 14:45:50 +05301026 def update_bin(self):
1027 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301028 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301029 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301030
Ankush Menat494bd9e2022-03-28 18:52:46 +05301031 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301032 if data.valuation_rate is not None:
1033 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301034 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301035
marination8418c4b2021-06-22 21:35:25 +05301036
1037def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1038 """get stock ledger entries filtered by specific posting datetime conditions"""
1039
Ankush Menat494bd9e2022-03-28 18:52:46 +05301040 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301041 if not args.get("posting_date"):
1042 args["posting_date"] = "1900-01-01"
1043 if not args.get("posting_time"):
1044 args["posting_time"] = "00:00"
1045
1046 voucher_condition = ""
1047 if exclude_current_voucher:
1048 voucher_no = args.get("voucher_no")
1049 voucher_condition = f"and voucher_no != '{voucher_no}'"
1050
Ankush Menat494bd9e2022-03-28 18:52:46 +05301051 sle = frappe.db.sql(
1052 """
marination8418c4b2021-06-22 21:35:25 +05301053 select *, timestamp(posting_date, posting_time) as "timestamp"
1054 from `tabStock Ledger Entry`
1055 where item_code = %(item_code)s
1056 and warehouse = %(warehouse)s
1057 and is_cancelled = 0
1058 {voucher_condition}
1059 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1060 order by timestamp(posting_date, posting_time) desc, creation desc
1061 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301062 for update""".format(
1063 voucher_condition=voucher_condition
1064 ),
1065 args,
1066 as_dict=1,
1067 )
marination8418c4b2021-06-22 21:35:25 +05301068
1069 return sle[0] if sle else frappe._dict()
1070
Ankush Menat494bd9e2022-03-28 18:52:46 +05301071
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301072def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301073 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301074 get the last sle on or before the current time-bucket,
1075 to get actual qty before transaction, this function
1076 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301077
Ankush Menat494bd9e2022-03-28 18:52:46 +05301078 args = {
1079 "item_code": "ABC",
1080 "warehouse": "XYZ",
1081 "posting_date": "2012-12-12",
1082 "posting_time": "12:00",
1083 "sle": "name of reference Stock Ledger Entry"
1084 }
Anand Doshi1b531862013-01-10 19:29:51 +05301085 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301086 args["name"] = args.get("sle", None) or ""
1087 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301088 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301089
Ankush Menat494bd9e2022-03-28 18:52:46 +05301090
1091def get_stock_ledger_entries(
1092 previous_sle,
1093 operator=None,
1094 order="desc",
1095 limit=None,
1096 for_update=False,
1097 debug=False,
1098 check_serial_no=True,
1099):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301100 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301101 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1102 operator
1103 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301104 if previous_sle.get("warehouse"):
1105 conditions += " and warehouse = %(warehouse)s"
1106 elif previous_sle.get("warehouse_condition"):
1107 conditions += " and " + previous_sle.get("warehouse_condition")
1108
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301109 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301110 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1111 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301112 conditions += (
1113 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301114 (
1115 serial_no = {0}
1116 or serial_no like {1}
1117 or serial_no like {2}
1118 or serial_no like {3}
1119 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301120 """
1121 ).format(
1122 frappe.db.escape(serial_no),
1123 frappe.db.escape("{}\n%".format(serial_no)),
1124 frappe.db.escape("%\n{}".format(serial_no)),
1125 frappe.db.escape("%\n{}\n%".format(serial_no)),
1126 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301127
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301128 if not previous_sle.get("posting_date"):
1129 previous_sle["posting_date"] = "1900-01-01"
1130 if not previous_sle.get("posting_time"):
1131 previous_sle["posting_time"] = "00:00"
1132
1133 if operator in (">", "<=") and previous_sle.get("name"):
1134 conditions += " and name!=%(name)s"
1135
Ankush Menat494bd9e2022-03-28 18:52:46 +05301136 return frappe.db.sql(
1137 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301138 select *, timestamp(posting_date, posting_time) as "timestamp"
1139 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301140 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301141 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301142 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301143 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301144 %(limit)s %(for_update)s"""
1145 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301146 "conditions": conditions,
1147 "limit": limit or "",
1148 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301149 "order": order,
1150 },
1151 previous_sle,
1152 as_dict=1,
1153 debug=debug,
1154 )
1155
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301156
Nabin Haita77b8c92020-12-21 14:45:50 +05301157def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301158 return frappe.db.get_value(
1159 "Stock Ledger Entry",
1160 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1161 [
1162 "item_code",
1163 "warehouse",
1164 "posting_date",
1165 "posting_time",
1166 "timestamp(posting_date, posting_time) as timestamp",
1167 ],
1168 as_dict=1,
1169 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301170
Ankush Menatce0514c2022-02-15 11:41:41 +05301171
Ankush Menat494bd9e2022-03-28 18:52:46 +05301172def get_batch_incoming_rate(
1173 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1174):
1175
Ankush Menat102fff22022-02-19 15:51:04 +05301176 sle = frappe.qb.DocType("Stock Ledger Entry")
1177
Ankush Menate1c16872022-04-21 20:01:48 +05301178 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301179 posting_date, posting_time
1180 )
Ankush Menat102fff22022-02-19 15:51:04 +05301181 if creation:
1182 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301183 CombineDatetime(sle.posting_date, sle.posting_time)
1184 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301185 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301186
1187 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301188 frappe.qb.from_(sle)
1189 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1190 .where(
1191 (sle.item_code == item_code)
1192 & (sle.warehouse == warehouse)
1193 & (sle.batch_no == batch_no)
1194 & (sle.is_cancelled == 0)
1195 )
1196 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301197 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301198
1199 if batch_details and batch_details[0].batch_qty:
1200 return batch_details[0].batch_value / batch_details[0].batch_qty
1201
1202
Ankush Menat494bd9e2022-03-28 18:52:46 +05301203def get_valuation_rate(
1204 item_code,
1205 warehouse,
1206 voucher_type,
1207 voucher_no,
1208 allow_zero_rate=False,
1209 currency=None,
1210 company=None,
1211 raise_error_if_no_rate=True,
1212 batch_no=None,
1213):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301214
Ankush Menatf7ffe042021-11-01 13:21:14 +05301215 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301216 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301217
Ankush Menat342d09a2022-02-19 14:28:51 +05301218 last_valuation_rate = None
1219
1220 # Get moving average rate of a specific batch number
1221 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301222 last_valuation_rate = frappe.db.sql(
1223 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301224 select sum(stock_value_difference) / sum(actual_qty)
1225 from `tabStock Ledger Entry`
1226 where
1227 item_code = %s
1228 AND warehouse = %s
1229 AND batch_no = %s
1230 AND is_cancelled = 0
1231 AND NOT (voucher_no = %s AND voucher_type = %s)
1232 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301233 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1234 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301235
Ankush Menatf7ffe042021-11-01 13:21:14 +05301236 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301237 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301238 last_valuation_rate = frappe.db.sql(
1239 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301240 from `tabStock Ledger Entry` force index (item_warehouse)
1241 where
1242 item_code = %s
1243 AND warehouse = %s
1244 AND valuation_rate >= 0
1245 AND is_cancelled = 0
1246 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301247 order by posting_date desc, posting_time desc, name desc limit 1""",
1248 (item_code, warehouse, voucher_no, voucher_type),
1249 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301250
1251 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301252 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301253 last_valuation_rate = frappe.db.sql(
1254 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301255 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301256 where
1257 item_code = %s
1258 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301259 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301260 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, voucher_no, voucher_type),
1263 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301264
Nabin Haita645f362018-03-01 10:31:24 +05301265 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301266 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301267
1268 # If negative stock allowed, and item delivered without any incoming entry,
1269 # system does not found any SLE, then take valuation rate from Item
1270 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301271
1272 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301273 # try Item Standard rate
1274 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301275
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301276 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301277 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301278 valuation_rate = frappe.db.get_value(
1279 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1280 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301281
Ankush Menat494bd9e2022-03-28 18:52:46 +05301282 if (
1283 not allow_zero_rate
1284 and not valuation_rate
1285 and raise_error_if_no_rate
1286 and cint(erpnext.is_perpetual_inventory_enabled(company))
1287 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301288 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301289
Ankush Menat494bd9e2022-03-28 18:52:46 +05301290 message = _(
1291 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1292 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301293 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301294 solutions = (
1295 "<li>"
1296 + _(
1297 "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."
1298 ).format(voucher_type)
1299 + "</li>"
1300 )
1301 solutions += (
1302 "<li>"
1303 + _("If not, you can Cancel / Submit this entry")
1304 + " {0} ".format(frappe.bold("after"))
1305 + _("performing either one below:")
1306 + "</li>"
1307 )
Marica97715f22020-05-11 20:45:37 +05301308 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1309 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1310 msg = message + solutions + sub_solutions + "</li>"
1311
1312 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301313
1314 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301315
Ankush Menat494bd9e2022-03-28 18:52:46 +05301316
Ankush Menate7109c12021-08-26 16:40:45 +05301317def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301318 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301319 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301320 qty_shift = args.actual_qty
1321
Ankush Menat7c839c42022-05-06 12:09:08 +05301322 args["time_format"] = "%H:%i:%s"
1323
marination8418c4b2021-06-22 21:35:25 +05301324 # find difference/shift in qty caused by stock reconciliation
1325 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301326 qty_shift = get_stock_reco_qty_shift(args)
1327
1328 # find the next nearest stock reco so that we only recalculate SLEs till that point
1329 next_stock_reco_detail = get_next_stock_reco(args)
1330 if next_stock_reco_detail:
1331 detail = next_stock_reco_detail[0]
1332 # add condition to update SLEs before this date & time
1333 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301334
Ankush Menat494bd9e2022-03-28 18:52:46 +05301335 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301336 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301337 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301338 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301339 where
1340 item_code = %(item_code)s
1341 and warehouse = %(warehouse)s
1342 and voucher_no != %(voucher_no)s
1343 and is_cancelled = 0
Ankush Menat7c839c42022-05-06 12:09:08 +05301344 and timestamp(posting_date, time_format(posting_time, %(time_format)s))
1345 > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
marination40389772021-07-02 17:13:45 +05301346 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301347 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301348 args,
1349 )
Nabin Hait186a0452021-02-18 14:14:21 +05301350
1351 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1352
Ankush Menat494bd9e2022-03-28 18:52:46 +05301353
marination40389772021-07-02 17:13:45 +05301354def get_stock_reco_qty_shift(args):
1355 stock_reco_qty_shift = 0
1356 if args.get("is_cancelled"):
1357 if args.get("previous_qty_after_transaction"):
1358 # get qty (balance) that was set at submission
1359 last_balance = args.get("previous_qty_after_transaction")
1360 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1361 else:
1362 stock_reco_qty_shift = flt(args.actual_qty)
1363 else:
1364 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301365 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1366 "qty_after_transaction"
1367 )
marination40389772021-07-02 17:13:45 +05301368
1369 if last_balance is not None:
1370 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1371 else:
1372 stock_reco_qty_shift = args.qty_after_transaction
1373
1374 return stock_reco_qty_shift
1375
Ankush Menat494bd9e2022-03-28 18:52:46 +05301376
marination40389772021-07-02 17:13:45 +05301377def get_next_stock_reco(args):
1378 """Returns next nearest stock reconciliaton's details."""
1379
Ankush Menat494bd9e2022-03-28 18:52:46 +05301380 return frappe.db.sql(
1381 """
marination40389772021-07-02 17:13:45 +05301382 select
1383 name, posting_date, posting_time, creation, voucher_no
1384 from
marination8c441262021-07-02 17:46:05 +05301385 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301386 where
1387 item_code = %(item_code)s
1388 and warehouse = %(warehouse)s
1389 and voucher_type = 'Stock Reconciliation'
1390 and voucher_no != %(voucher_no)s
1391 and is_cancelled = 0
1392 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1393 or (
1394 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1395 and creation > %(creation)s
1396 )
1397 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301398 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301399 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301400 """,
1401 args,
1402 as_dict=1,
1403 )
1404
marination40389772021-07-02 17:13:45 +05301405
1406def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301407 return f"""
1408 and
1409 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1410 or (
1411 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1412 and creation < '{detail.creation}'
1413 )
1414 )"""
1415
Ankush Menat494bd9e2022-03-28 18:52:46 +05301416
Ankush Menate7109c12021-08-26 16:40:45 +05301417def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301418 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301419 return
1420 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1421 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301422
Ankush Menat5eba5752021-12-07 23:03:52 +05301423 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301424
1425 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301426 message = _(
1427 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1428 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301429 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301430 frappe.get_desk_link("Item", args.item_code),
1431 frappe.get_desk_link("Warehouse", args.warehouse),
1432 neg_sle[0]["posting_date"],
1433 neg_sle[0]["posting_time"],
1434 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1435 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301436
Ankush Menat494bd9e2022-03-28 18:52:46 +05301437 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301438
1439 if not args.batch_no:
1440 return
1441
1442 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301443 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301444 message = _(
1445 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1446 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301447 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301448 frappe.get_desk_link("Batch", args.batch_no),
1449 frappe.get_desk_link("Warehouse", args.warehouse),
1450 neg_batch_sle[0]["posting_date"],
1451 neg_batch_sle[0]["posting_time"],
1452 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1453 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301454 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301455
Nabin Haita77b8c92020-12-21 14:45:50 +05301456
Maricad6078aa2022-06-17 15:13:13 +05301457def is_negative_with_precision(neg_sle, is_batch=False):
1458 """
1459 Returns whether system precision rounded qty is insufficient.
1460 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1461 """
1462
1463 if not neg_sle:
1464 return False
1465
1466 field = "cumulative_total" if is_batch else "qty_after_transaction"
1467 precision = cint(frappe.db.get_default("float_precision")) or 2
1468 qty_deficit = flt(neg_sle[0][field], precision)
1469
1470 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1471
1472
Nabin Haita77b8c92020-12-21 14:45:50 +05301473def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301474 return frappe.db.sql(
1475 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301476 select
1477 qty_after_transaction, posting_date, posting_time,
1478 voucher_type, voucher_no
1479 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301480 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301481 item_code = %(item_code)s
1482 and warehouse = %(warehouse)s
1483 and voucher_no != %(voucher_no)s
1484 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1485 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301486 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301487 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301488 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301489 """,
1490 args,
1491 as_dict=1,
1492 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301493
Ankush Menat5eba5752021-12-07 23:03:52 +05301494
1495def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301496 return frappe.db.sql(
1497 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301498 with batch_ledger as (
1499 select
1500 posting_date, posting_time, voucher_type, voucher_no,
1501 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1502 from `tabStock Ledger Entry`
1503 where
1504 item_code = %(item_code)s
1505 and warehouse = %(warehouse)s
1506 and batch_no=%(batch_no)s
1507 and is_cancelled = 0
1508 order by posting_date, posting_time, creation
1509 )
1510 select * from batch_ledger
1511 where
1512 cumulative_total < 0.0
1513 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1514 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301515 """,
1516 args,
1517 as_dict=1,
1518 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301519
1520
1521def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1522 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1523 return True
1524 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1525 return True
1526 return False