blob: 01c5aa9e31568b1bf9f63d5cfad3101d92a61b48 [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
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530253 if doc:
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
Ankush Menat494bd9e2022-03-28 18:52:46 +0530258
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530259def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530260 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530261 if not args.get(field):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530262 validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530263 frappe.throw(_(validation_msg))
264
Ankush Menat494bd9e2022-03-28 18:52:46 +0530265
Ankush Menatecdb4932022-04-17 19:06:13 +0530266def update_args_in_repost_item_valuation(
267 doc, index, args, distinct_item_warehouses, affected_transactions
268):
Ankush Menatecdb4932022-04-17 19:06:13 +0530269 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530270 {
271 "items_to_be_repost": json.dumps(args, default=str),
272 "distinct_item_and_warehouse": json.dumps(
273 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
274 ),
275 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530276 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530277 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530278 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530279
Ankush Menatecdb4932022-04-17 19:06:13 +0530280 if not frappe.flags.in_test:
281 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530282
Ankush Menat494bd9e2022-03-28 18:52:46 +0530283 frappe.publish_realtime(
284 "item_reposting_progress",
285 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
286 )
287
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530288
289def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
290 if doc and doc.items_to_be_repost:
291 return json.loads(doc.items_to_be_repost) or []
292
Ankush Menat494bd9e2022-03-28 18:52:46 +0530293 return frappe.db.get_all(
294 "Stock Ledger Entry",
Nabin Haita77b8c92020-12-21 14:45:50 +0530295 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
Nabin Hait186a0452021-02-18 14:14:21 +0530296 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
Nabin Haita77b8c92020-12-21 14:45:50 +0530297 order_by="creation asc",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530298 group_by="item_code, warehouse",
Nabin Haita77b8c92020-12-21 14:45:50 +0530299 )
Nabin Hait74c281c2013-08-19 16:17:18 +0530300
Ankush Menat494bd9e2022-03-28 18:52:46 +0530301
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530302def get_distinct_item_warehouse(args=None, doc=None):
303 distinct_item_warehouses = {}
304 if doc and doc.distinct_item_and_warehouse:
305 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530306 distinct_item_warehouses = {
307 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
308 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530309 else:
310 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530311 distinct_item_warehouses.setdefault(
312 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
313 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530314
315 return distinct_item_warehouses
316
Ankush Menat494bd9e2022-03-28 18:52:46 +0530317
Ankush Menatecdb4932022-04-17 19:06:13 +0530318def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
319 if not doc.affected_transactions:
320 return set()
321
322 transactions = frappe.parse_json(doc.affected_transactions)
323 return {tuple(transaction) for transaction in transactions}
324
325
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530326def get_current_index(doc=None):
327 if doc and doc.current_index:
328 return doc.current_index
329
Ankush Menat494bd9e2022-03-28 18:52:46 +0530330
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530331class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530332 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530333 update valution rate and qty after transaction
334 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530335
Ankush Menat494bd9e2022-03-28 18:52:46 +0530336 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530337
Ankush Menat494bd9e2022-03-28 18:52:46 +0530338 args = {
339 "item_code": "ABC",
340 "warehouse": "XYZ",
341 "posting_date": "2012-12-12",
342 "posting_time": "12:00"
343 }
Nabin Hait902e8602013-01-08 18:29:24 +0530344 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530345
346 def __init__(
347 self,
348 args,
349 allow_zero_rate=False,
350 allow_negative_stock=None,
351 via_landed_cost_voucher=False,
352 verbose=1,
353 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530354 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530355 self.verbose = verbose
356 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530357 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530358 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530359 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
360 item_code=self.item_code
361 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530362
Nabin Haita77b8c92020-12-21 14:45:50 +0530363 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530364 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530365 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530366
Nabin Haita77b8c92020-12-21 14:45:50 +0530367 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530368 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530369 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530370
371 self.new_items_found = False
372 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530373 self.affected_transactions: Set[Tuple[str, str]] = set()
Nabin Haita77b8c92020-12-21 14:45:50 +0530374
375 self.data = frappe._dict()
376 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530377 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530378
Maricad6078aa2022-06-17 15:13:13 +0530379 def set_precision(self):
380 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
381 self.currency_precision = get_field_precision(
382 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530383 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530384
385 def initialize_previous_data(self, args):
386 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530387 Get previous sl entries for current item for each related warehouse
388 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530389
Ankush Menat494bd9e2022-03-28 18:52:46 +0530390 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530391
Ankush Menat494bd9e2022-03-28 18:52:46 +0530392 self.data = {
393 warehouse1: {
394 'previus_sle': {},
395 'qty_after_transaction': 10,
396 'valuation_rate': 100,
397 'stock_value': 1000,
398 'prev_stock_value': 1000,
399 'stock_queue': '[[10, 100]]',
400 'stock_value_difference': 1000
401 }
402 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530403
404 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530405 self.data.setdefault(args.warehouse, frappe._dict())
406 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530407 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530408 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530409
Ankush Menatc1d986a2021-08-31 19:43:42 +0530410 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
411 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
412
Ankush Menat494bd9e2022-03-28 18:52:46 +0530413 warehouse_dict.update(
414 {
415 "prev_stock_value": previous_sle.stock_value or 0.0,
416 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
417 "stock_value_difference": 0.0,
418 }
419 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530420
Nabin Haita77b8c92020-12-21 14:45:50 +0530421 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530422 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530423
Nabin Haita77b8c92020-12-21 14:45:50 +0530424 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530425 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530426 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530427 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530428 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530429 entries_to_fix = self.get_future_entries_to_fix()
430
431 i = 0
432 while i < len(entries_to_fix):
433 sle = entries_to_fix[i]
434 i += 1
435
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530436 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530437
Nabin Haita77b8c92020-12-21 14:45:50 +0530438 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530439 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530440
Nabin Hait186a0452021-02-18 14:14:21 +0530441 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530442
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530443 if self.exceptions:
444 self.raise_exceptions()
445
Nabin Hait186a0452021-02-18 14:14:21 +0530446 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530447 sl_entries = self.get_sle_against_current_voucher()
448 for sle in sl_entries:
449 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530450
Nabin Haita77b8c92020-12-21 14:45:50 +0530451 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530452 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530453
Ankush Menat494bd9e2022-03-28 18:52:46 +0530454 return frappe.db.sql(
455 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530456 select
457 *, timestamp(posting_date, posting_time) as "timestamp"
458 from
459 `tabStock Ledger Entry`
460 where
461 item_code = %(item_code)s
462 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530463 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530464 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
465
Nabin Haita77b8c92020-12-21 14:45:50 +0530466 order by
467 creation ASC
468 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530469 """,
470 self.args,
471 as_dict=1,
472 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530473
Nabin Haita77b8c92020-12-21 14:45:50 +0530474 def get_future_entries_to_fix(self):
475 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530476 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
477 {"item_code": self.item_code, "warehouse": self.args.warehouse}
478 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530479
Nabin Haita77b8c92020-12-21 14:45:50 +0530480 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530481
Nabin Haita77b8c92020-12-21 14:45:50 +0530482 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530483 dependant_sle = get_sle_by_voucher_detail_no(
484 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
485 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530486
Nabin Haita77b8c92020-12-21 14:45:50 +0530487 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530488 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530489 elif (
490 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
491 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530492 return entries_to_fix
493 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530494 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530495 return entries_to_fix
496 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
497 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530498 else:
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530499 self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
500 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530501
502 def update_distinct_item_warehouses(self, dependant_sle):
503 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530504 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530505 if key not in self.distinct_item_warehouses:
506 self.distinct_item_warehouses[key] = val
507 self.new_items_found = True
508 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530509 existing_sle_posting_date = (
510 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
511 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530512 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
513 val.sle_changed = True
514 self.distinct_item_warehouses[key] = val
515 self.new_items_found = True
516
517 def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
Nabin Haita77b8c92020-12-21 14:45:50 +0530518 self.initialize_previous_data(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530519 self.distinct_item_warehouses[(self.item_code, dependant_sle.warehouse)] = frappe._dict(
520 {"sle": dependant_sle}
Ankush Menat494bd9e2022-03-28 18:52:46 +0530521 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530522
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530523 self.new_items_found = True
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530524
525 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530526 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
527
Nabin Haita77b8c92020-12-21 14:45:50 +0530528 # previous sle data for this warehouse
529 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530530 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530531
Anand Doshi0dc79f42015-04-06 12:59:34 +0530532 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 +0530533 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530534 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530535 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530536 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530537 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530538
Nabin Haita77b8c92020-12-21 14:45:50 +0530539 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530540 if not self.args.get("sle_id"):
541 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530542
Ankush Menat66bf21f2022-01-16 20:45:59 +0530543 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530544 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530545 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530546 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530547 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530548
Ankush Menat494bd9e2022-03-28 18:52:46 +0530549 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
550 self.wh_data.valuation_rate
551 )
552 elif sle.batch_no and frappe.db.get_value(
553 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
554 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530555 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530556 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530557 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530558 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530559 self.wh_data.valuation_rate = sle.valuation_rate
560 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530561 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
562 self.wh_data.valuation_rate
563 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530564 if self.valuation_method != "Moving Average":
565 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530566 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530567 if self.valuation_method == "Moving Average":
568 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530569 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530570 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
571 self.wh_data.valuation_rate
572 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530573 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530574 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530575
Rushabh Mehta54047782013-12-26 11:07:46 +0530576 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530577 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530578 if not self.wh_data.qty_after_transaction:
579 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530580 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
581 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530582
Nabin Hait902e8602013-01-08 18:29:24 +0530583 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530584 sle.qty_after_transaction = self.wh_data.qty_after_transaction
585 sle.valuation_rate = self.wh_data.valuation_rate
586 sle.stock_value = self.wh_data.stock_value
587 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530588 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530589 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530590 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530591
Ankush Menat701878f2022-03-01 18:08:29 +0530592 if not self.args.get("sle_id"):
593 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530594
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530595 def validate_negative_stock(self, sle):
596 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530597 validate negative stock for entries current datetime onwards
598 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530599 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530600 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Maricad6078aa2022-06-17 15:13:13 +0530601 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530602
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530603 if diff < 0 and abs(diff) > 0.0001:
604 # negative stock!
605 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530606 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530607 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530608 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530609 return True
610
Nabin Haita77b8c92020-12-21 14:45:50 +0530611 def get_dynamic_incoming_outgoing_rate(self, sle):
612 # Get updated incoming/outgoing rate from transaction
613 if sle.recalculate_rate:
614 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
615
616 if flt(sle.actual_qty) >= 0:
617 sle.incoming_rate = rate
618 else:
619 sle.outgoing_rate = rate
620
621 def get_incoming_outgoing_rate_from_transaction(self, sle):
622 rate = 0
623 # Material Transfer, Repack, Manufacturing
624 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530625 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530626 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
627 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530628 elif sle.voucher_type in (
629 "Purchase Receipt",
630 "Purchase Invoice",
631 "Delivery Note",
632 "Sales Invoice",
633 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530634 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530635 from erpnext.controllers.sales_and_purchase_return import (
636 get_rate_for_return, # don't move this import to top
637 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530638
639 rate = get_rate_for_return(
640 sle.voucher_type,
641 sle.voucher_no,
642 sle.item_code,
643 voucher_detail_no=sle.voucher_detail_no,
644 sle=sle,
645 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530646 else:
647 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530648 rate_field = "valuation_rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530649 else:
650 rate_field = "incoming_rate"
651
652 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530653 item_code, incoming_rate = frappe.db.get_value(
654 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
655 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530656
657 if item_code == sle.item_code:
658 rate = incoming_rate
659 else:
660 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
661 ref_doctype = "Packed Item"
662 else:
663 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530664
Ankush Menat494bd9e2022-03-28 18:52:46 +0530665 rate = frappe.db.get_value(
666 ref_doctype,
667 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
668 rate_field,
669 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530670
671 return rate
672
673 def update_outgoing_rate_on_transaction(self, sle):
674 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530675 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
676 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530677 """
678 if sle.actual_qty and sle.voucher_detail_no:
679 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
680
681 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
682 self.update_rate_on_stock_entry(sle, outgoing_rate)
683 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
684 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
685 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
686 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
687
688 def update_rate_on_stock_entry(self, sle, outgoing_rate):
689 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
690
Ankush Menat701878f2022-03-01 18:08:29 +0530691 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
692 if not sle.dependant_sle_voucher_detail_no:
693 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530694
695 def recalculate_amounts_in_stock_entry(self, voucher_no):
696 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530697 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
698 stock_entry.db_update()
699 for d in stock_entry.items:
700 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530701
Nabin Haita77b8c92020-12-21 14:45:50 +0530702 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
703 # Update item's incoming rate on transaction
704 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
705 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530706 frappe.db.set_value(
707 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
708 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530709 else:
710 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530711 frappe.db.set_value(
712 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530713 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530714 "incoming_rate",
715 outgoing_rate,
716 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530717
718 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
719 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530720 frappe.db.set_value(
721 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
722 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530723 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530724 frappe.db.set_value(
725 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
726 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530727
728 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530729 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530730 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530731 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530732 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530733 d.db_update()
734
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530735 def get_serialized_values(self, sle):
736 incoming_rate = flt(sle.incoming_rate)
737 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530738 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530739
740 if incoming_rate < 0:
741 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530742 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530743
Nabin Hait2620bf42016-02-29 11:30:27 +0530744 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530745 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530746 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530747 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530748 # In case of delivery/stock issue, get average purchase rate
749 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530750 if not sle.is_cancelled:
751 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
752 stock_value_change = -1 * outgoing_value
753 else:
754 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530755
Nabin Haita77b8c92020-12-21 14:45:50 +0530756 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530757
Nabin Hait2620bf42016-02-29 11:30:27 +0530758 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530759 new_stock_value = (
760 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
761 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530762 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530763 # calculate new valuation rate only if stock value is positive
764 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530765 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530766
Nabin Haita77b8c92020-12-21 14:45:50 +0530767 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530768 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
769 sle.voucher_type, sle.voucher_detail_no
770 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530771 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530772 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530773
Nabin Hait328c4f92020-01-02 19:00:32 +0530774 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
775 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530776 all_serial_nos = frappe.get_all(
777 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
778 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530779
Ankush Menat494bd9e2022-03-28 18:52:46 +0530780 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 +0530781
782 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530783 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530784 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530785 incoming_rate = frappe.db.sql(
786 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530787 select incoming_rate
788 from `tabStock Ledger Entry`
789 where
790 company = %s
791 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530792 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530793 and (serial_no = %s
794 or serial_no like %s
795 or serial_no like %s
796 or serial_no like %s
797 )
798 order by posting_date desc
799 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530800 """,
801 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
802 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530803
804 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
805
806 return incoming_values
807
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530808 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530809 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530810 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530811 if new_stock_qty >= 0:
812 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530813 if flt(self.wh_data.qty_after_transaction) <= 0:
814 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530815 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530816 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
817 actual_qty * sle.incoming_rate
818 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530819
Nabin Haita77b8c92020-12-21 14:45:50 +0530820 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530821
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530822 elif sle.outgoing_rate:
823 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530824 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
825 actual_qty * sle.outgoing_rate
826 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530827
Nabin Haita77b8c92020-12-21 14:45:50 +0530828 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530829 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530830 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530831 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530832 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
833 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530834
Nabin Haita77b8c92020-12-21 14:45:50 +0530835 if not self.wh_data.valuation_rate and actual_qty > 0:
836 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530837
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530838 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800839 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530840 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530841 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
842 sle.voucher_type, sle.voucher_detail_no
843 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800844 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530845 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530846
Ankush Menatf089d392022-02-02 12:51:21 +0530847 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530848 incoming_rate = flt(sle.incoming_rate)
849 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530850 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530851
Ankush Menat494bd9e2022-03-28 18:52:46 +0530852 self.wh_data.qty_after_transaction = round_off_if_near_zero(
853 self.wh_data.qty_after_transaction + actual_qty
854 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530855
Ankush Menat97e18a12022-01-15 17:42:25 +0530856 if self.valuation_method == "LIFO":
857 stock_queue = LIFOValuation(self.wh_data.stock_queue)
858 else:
859 stock_queue = FIFOValuation(self.wh_data.stock_queue)
860
Ankush Menatb534fee2022-02-19 20:58:36 +0530861 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
862
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530863 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530864 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530865 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530866
Ankush Menat4b29fb62021-12-18 18:40:22 +0530867 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530868 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
869 sle.voucher_type, sle.voucher_detail_no
870 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530871 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530872 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530873 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530874 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530875
Ankush Menat494bd9e2022-03-28 18:52:46 +0530876 stock_queue.remove_stock(
877 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
878 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530879
Ankush Menatb534fee2022-02-19 20:58:36 +0530880 _qty, stock_value = stock_queue.get_total_stock_and_value()
881
882 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530883
Ankush Menat97e18a12022-01-15 17:42:25 +0530884 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530885 self.wh_data.stock_value = round_off_if_near_zero(
886 self.wh_data.stock_value + stock_value_difference
887 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530888
Nabin Haita77b8c92020-12-21 14:45:50 +0530889 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530890 self.wh_data.stock_queue.append(
891 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
892 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530893
Ankush Menatb534fee2022-02-19 20:58:36 +0530894 if self.wh_data.qty_after_transaction:
895 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
896
Ankush Menatce0514c2022-02-15 11:41:41 +0530897 def update_batched_values(self, sle):
898 incoming_rate = flt(sle.incoming_rate)
899 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530900
Ankush Menat494bd9e2022-03-28 18:52:46 +0530901 self.wh_data.qty_after_transaction = round_off_if_near_zero(
902 self.wh_data.qty_after_transaction + actual_qty
903 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530904
905 if actual_qty > 0:
906 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530907 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530908 outgoing_rate = get_batch_incoming_rate(
909 item_code=sle.item_code,
910 warehouse=sle.warehouse,
911 batch_no=sle.batch_no,
912 posting_date=sle.posting_date,
913 posting_time=sle.posting_time,
914 creation=sle.creation,
915 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530916 if outgoing_rate is None:
917 # This can *only* happen if qty available for the batch is zero.
918 # in such case fall back various other rates.
919 # future entries will correct the overall accounting as each
920 # batch individually uses moving average rates.
921 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530922 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530923
Ankush Menat494bd9e2022-03-28 18:52:46 +0530924 self.wh_data.stock_value = round_off_if_near_zero(
925 self.wh_data.stock_value + stock_value_difference
926 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530927 if self.wh_data.qty_after_transaction:
928 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530929
Javier Wong9b11d9b2017-04-14 18:24:04 +0800930 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530931 ref_item_dt = ""
932
933 if voucher_type == "Stock Entry":
934 ref_item_dt = voucher_type + " Detail"
935 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
936 ref_item_dt = voucher_type + " Item"
937
938 if ref_item_dt:
939 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
940 else:
941 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530942
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530943 def get_fallback_rate(self, sle) -> float:
944 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530945 This should only get used for negative stock."""
946 return get_valuation_rate(
947 sle.item_code,
948 sle.warehouse,
949 sle.voucher_type,
950 sle.voucher_no,
951 self.allow_zero_rate,
952 currency=erpnext.get_company_currency(sle.company),
953 company=sle.company,
954 batch_no=sle.batch_no,
955 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530956
Nabin Haita77b8c92020-12-21 14:45:50 +0530957 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530958 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530959 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
960 sle = sle[0] if sle else frappe._dict()
961 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530962
Nabin Haita77b8c92020-12-21 14:45:50 +0530963 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530964 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530965 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530966
967 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530968 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530969 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530970 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530971
Ankush Menat494bd9e2022-03-28 18:52:46 +0530972 if (
973 exceptions[0]["voucher_type"],
974 exceptions[0]["voucher_no"],
975 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +0530976
Nabin Haita77b8c92020-12-21 14:45:50 +0530977 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530978 abs(deficiency),
979 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
980 frappe.get_desk_link("Warehouse", warehouse),
981 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530982 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530983 msg = _(
984 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
985 ).format(
986 abs(deficiency),
987 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
988 frappe.get_desk_link("Warehouse", warehouse),
989 exceptions[0]["posting_date"],
990 exceptions[0]["posting_time"],
991 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
992 )
Rushabh Mehta538607e2016-06-12 11:03:00 +0530993
Nabin Haita77b8c92020-12-21 14:45:50 +0530994 if msg:
995 msg_list.append(msg)
996
997 if msg_list:
998 message = "\n\n".join(msg_list)
999 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301000 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301001 else:
1002 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301003
Nabin Haita77b8c92020-12-21 14:45:50 +05301004 def update_bin(self):
1005 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301006 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301007 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301008
Ankush Menat494bd9e2022-03-28 18:52:46 +05301009 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301010 if data.valuation_rate is not None:
1011 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301012 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301013
marination8418c4b2021-06-22 21:35:25 +05301014
1015def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1016 """get stock ledger entries filtered by specific posting datetime conditions"""
1017
Ankush Menat494bd9e2022-03-28 18:52:46 +05301018 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301019 if not args.get("posting_date"):
1020 args["posting_date"] = "1900-01-01"
1021 if not args.get("posting_time"):
1022 args["posting_time"] = "00:00"
1023
1024 voucher_condition = ""
1025 if exclude_current_voucher:
1026 voucher_no = args.get("voucher_no")
1027 voucher_condition = f"and voucher_no != '{voucher_no}'"
1028
Ankush Menat494bd9e2022-03-28 18:52:46 +05301029 sle = frappe.db.sql(
1030 """
marination8418c4b2021-06-22 21:35:25 +05301031 select *, timestamp(posting_date, posting_time) as "timestamp"
1032 from `tabStock Ledger Entry`
1033 where item_code = %(item_code)s
1034 and warehouse = %(warehouse)s
1035 and is_cancelled = 0
1036 {voucher_condition}
1037 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1038 order by timestamp(posting_date, posting_time) desc, creation desc
1039 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301040 for update""".format(
1041 voucher_condition=voucher_condition
1042 ),
1043 args,
1044 as_dict=1,
1045 )
marination8418c4b2021-06-22 21:35:25 +05301046
1047 return sle[0] if sle else frappe._dict()
1048
Ankush Menat494bd9e2022-03-28 18:52:46 +05301049
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301050def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301051 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301052 get the last sle on or before the current time-bucket,
1053 to get actual qty before transaction, this function
1054 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301055
Ankush Menat494bd9e2022-03-28 18:52:46 +05301056 args = {
1057 "item_code": "ABC",
1058 "warehouse": "XYZ",
1059 "posting_date": "2012-12-12",
1060 "posting_time": "12:00",
1061 "sle": "name of reference Stock Ledger Entry"
1062 }
Anand Doshi1b531862013-01-10 19:29:51 +05301063 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301064 args["name"] = args.get("sle", None) or ""
1065 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301066 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301067
Ankush Menat494bd9e2022-03-28 18:52:46 +05301068
1069def get_stock_ledger_entries(
1070 previous_sle,
1071 operator=None,
1072 order="desc",
1073 limit=None,
1074 for_update=False,
1075 debug=False,
1076 check_serial_no=True,
1077):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301078 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301079 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1080 operator
1081 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301082 if previous_sle.get("warehouse"):
1083 conditions += " and warehouse = %(warehouse)s"
1084 elif previous_sle.get("warehouse_condition"):
1085 conditions += " and " + previous_sle.get("warehouse_condition")
1086
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301087 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301088 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1089 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301090 conditions += (
1091 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301092 (
1093 serial_no = {0}
1094 or serial_no like {1}
1095 or serial_no like {2}
1096 or serial_no like {3}
1097 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301098 """
1099 ).format(
1100 frappe.db.escape(serial_no),
1101 frappe.db.escape("{}\n%".format(serial_no)),
1102 frappe.db.escape("%\n{}".format(serial_no)),
1103 frappe.db.escape("%\n{}\n%".format(serial_no)),
1104 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301105
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301106 if not previous_sle.get("posting_date"):
1107 previous_sle["posting_date"] = "1900-01-01"
1108 if not previous_sle.get("posting_time"):
1109 previous_sle["posting_time"] = "00:00"
1110
1111 if operator in (">", "<=") and previous_sle.get("name"):
1112 conditions += " and name!=%(name)s"
1113
Ankush Menat494bd9e2022-03-28 18:52:46 +05301114 return frappe.db.sql(
1115 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301116 select *, timestamp(posting_date, posting_time) as "timestamp"
1117 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301118 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301119 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301120 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301121 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301122 %(limit)s %(for_update)s"""
1123 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301124 "conditions": conditions,
1125 "limit": limit or "",
1126 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301127 "order": order,
1128 },
1129 previous_sle,
1130 as_dict=1,
1131 debug=debug,
1132 )
1133
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301134
Nabin Haita77b8c92020-12-21 14:45:50 +05301135def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301136 return frappe.db.get_value(
1137 "Stock Ledger Entry",
1138 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1139 [
1140 "item_code",
1141 "warehouse",
1142 "posting_date",
1143 "posting_time",
1144 "timestamp(posting_date, posting_time) as timestamp",
1145 ],
1146 as_dict=1,
1147 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301148
Ankush Menatce0514c2022-02-15 11:41:41 +05301149
Ankush Menat494bd9e2022-03-28 18:52:46 +05301150def get_batch_incoming_rate(
1151 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1152):
1153
Ankush Menat102fff22022-02-19 15:51:04 +05301154 sle = frappe.qb.DocType("Stock Ledger Entry")
1155
Ankush Menate1c16872022-04-21 20:01:48 +05301156 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301157 posting_date, posting_time
1158 )
Ankush Menat102fff22022-02-19 15:51:04 +05301159 if creation:
1160 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301161 CombineDatetime(sle.posting_date, sle.posting_time)
1162 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301163 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301164
1165 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301166 frappe.qb.from_(sle)
1167 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1168 .where(
1169 (sle.item_code == item_code)
1170 & (sle.warehouse == warehouse)
1171 & (sle.batch_no == batch_no)
1172 & (sle.is_cancelled == 0)
1173 )
1174 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301175 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301176
1177 if batch_details and batch_details[0].batch_qty:
1178 return batch_details[0].batch_value / batch_details[0].batch_qty
1179
1180
Ankush Menat494bd9e2022-03-28 18:52:46 +05301181def get_valuation_rate(
1182 item_code,
1183 warehouse,
1184 voucher_type,
1185 voucher_no,
1186 allow_zero_rate=False,
1187 currency=None,
1188 company=None,
1189 raise_error_if_no_rate=True,
1190 batch_no=None,
1191):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301192
Ankush Menatf7ffe042021-11-01 13:21:14 +05301193 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301194 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301195
Ankush Menat342d09a2022-02-19 14:28:51 +05301196 last_valuation_rate = None
1197
1198 # Get moving average rate of a specific batch number
1199 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301200 last_valuation_rate = frappe.db.sql(
1201 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301202 select sum(stock_value_difference) / sum(actual_qty)
1203 from `tabStock Ledger Entry`
1204 where
1205 item_code = %s
1206 AND warehouse = %s
1207 AND batch_no = %s
1208 AND is_cancelled = 0
1209 AND NOT (voucher_no = %s AND voucher_type = %s)
1210 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301211 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1212 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301213
Ankush Menatf7ffe042021-11-01 13:21:14 +05301214 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301215 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301216 last_valuation_rate = frappe.db.sql(
1217 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301218 from `tabStock Ledger Entry` force index (item_warehouse)
1219 where
1220 item_code = %s
1221 AND warehouse = %s
1222 AND valuation_rate >= 0
1223 AND is_cancelled = 0
1224 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301225 order by posting_date desc, posting_time desc, name desc limit 1""",
1226 (item_code, warehouse, voucher_no, voucher_type),
1227 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301228
1229 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301230 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301231 last_valuation_rate = frappe.db.sql(
1232 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301233 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301234 where
1235 item_code = %s
1236 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301237 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301238 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301239 order by posting_date desc, posting_time desc, name desc limit 1""",
1240 (item_code, voucher_no, voucher_type),
1241 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301242
Nabin Haita645f362018-03-01 10:31:24 +05301243 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301244 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301245
1246 # If negative stock allowed, and item delivered without any incoming entry,
1247 # system does not found any SLE, then take valuation rate from Item
1248 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301249
1250 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301251 # try Item Standard rate
1252 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301253
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301254 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301255 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301256 valuation_rate = frappe.db.get_value(
1257 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1258 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301259
Ankush Menat494bd9e2022-03-28 18:52:46 +05301260 if (
1261 not allow_zero_rate
1262 and not valuation_rate
1263 and raise_error_if_no_rate
1264 and cint(erpnext.is_perpetual_inventory_enabled(company))
1265 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301266 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301267
Ankush Menat494bd9e2022-03-28 18:52:46 +05301268 message = _(
1269 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1270 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301271 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301272 solutions = (
1273 "<li>"
1274 + _(
1275 "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."
1276 ).format(voucher_type)
1277 + "</li>"
1278 )
1279 solutions += (
1280 "<li>"
1281 + _("If not, you can Cancel / Submit this entry")
1282 + " {0} ".format(frappe.bold("after"))
1283 + _("performing either one below:")
1284 + "</li>"
1285 )
Marica97715f22020-05-11 20:45:37 +05301286 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1287 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1288 msg = message + solutions + sub_solutions + "</li>"
1289
1290 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301291
1292 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301293
Ankush Menat494bd9e2022-03-28 18:52:46 +05301294
Ankush Menate7109c12021-08-26 16:40:45 +05301295def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301296 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301297 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301298 qty_shift = args.actual_qty
1299
Ankush Menat7c839c42022-05-06 12:09:08 +05301300 args["time_format"] = "%H:%i:%s"
1301
marination8418c4b2021-06-22 21:35:25 +05301302 # find difference/shift in qty caused by stock reconciliation
1303 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301304 qty_shift = get_stock_reco_qty_shift(args)
1305
1306 # find the next nearest stock reco so that we only recalculate SLEs till that point
1307 next_stock_reco_detail = get_next_stock_reco(args)
1308 if next_stock_reco_detail:
1309 detail = next_stock_reco_detail[0]
1310 # add condition to update SLEs before this date & time
1311 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301312
Ankush Menat494bd9e2022-03-28 18:52:46 +05301313 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301314 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301315 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301316 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301317 where
1318 item_code = %(item_code)s
1319 and warehouse = %(warehouse)s
1320 and voucher_no != %(voucher_no)s
1321 and is_cancelled = 0
Ankush Menat7c839c42022-05-06 12:09:08 +05301322 and timestamp(posting_date, time_format(posting_time, %(time_format)s))
1323 > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
marination40389772021-07-02 17:13:45 +05301324 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301325 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301326 args,
1327 )
Nabin Hait186a0452021-02-18 14:14:21 +05301328
1329 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1330
Ankush Menat494bd9e2022-03-28 18:52:46 +05301331
marination40389772021-07-02 17:13:45 +05301332def get_stock_reco_qty_shift(args):
1333 stock_reco_qty_shift = 0
1334 if args.get("is_cancelled"):
1335 if args.get("previous_qty_after_transaction"):
1336 # get qty (balance) that was set at submission
1337 last_balance = args.get("previous_qty_after_transaction")
1338 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1339 else:
1340 stock_reco_qty_shift = flt(args.actual_qty)
1341 else:
1342 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301343 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1344 "qty_after_transaction"
1345 )
marination40389772021-07-02 17:13:45 +05301346
1347 if last_balance is not None:
1348 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1349 else:
1350 stock_reco_qty_shift = args.qty_after_transaction
1351
1352 return stock_reco_qty_shift
1353
Ankush Menat494bd9e2022-03-28 18:52:46 +05301354
marination40389772021-07-02 17:13:45 +05301355def get_next_stock_reco(args):
1356 """Returns next nearest stock reconciliaton's details."""
1357
Ankush Menat494bd9e2022-03-28 18:52:46 +05301358 return frappe.db.sql(
1359 """
marination40389772021-07-02 17:13:45 +05301360 select
1361 name, posting_date, posting_time, creation, voucher_no
1362 from
marination8c441262021-07-02 17:46:05 +05301363 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301364 where
1365 item_code = %(item_code)s
1366 and warehouse = %(warehouse)s
1367 and voucher_type = 'Stock Reconciliation'
1368 and voucher_no != %(voucher_no)s
1369 and is_cancelled = 0
1370 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1371 or (
1372 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1373 and creation > %(creation)s
1374 )
1375 )
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301376 order by timestamp(posting_date, posting_time) asc, creation asc
marination40389772021-07-02 17:13:45 +05301377 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301378 """,
1379 args,
1380 as_dict=1,
1381 )
1382
marination40389772021-07-02 17:13:45 +05301383
1384def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301385 return f"""
1386 and
1387 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1388 or (
1389 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1390 and creation < '{detail.creation}'
1391 )
1392 )"""
1393
Ankush Menat494bd9e2022-03-28 18:52:46 +05301394
Ankush Menate7109c12021-08-26 16:40:45 +05301395def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301396 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301397 return
1398 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1399 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301400
Ankush Menat5eba5752021-12-07 23:03:52 +05301401 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301402
1403 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301404 message = _(
1405 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1406 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301407 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301408 frappe.get_desk_link("Item", args.item_code),
1409 frappe.get_desk_link("Warehouse", args.warehouse),
1410 neg_sle[0]["posting_date"],
1411 neg_sle[0]["posting_time"],
1412 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1413 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301414
Ankush Menat494bd9e2022-03-28 18:52:46 +05301415 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301416
1417 if not args.batch_no:
1418 return
1419
1420 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301421 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301422 message = _(
1423 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1424 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301425 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301426 frappe.get_desk_link("Batch", args.batch_no),
1427 frappe.get_desk_link("Warehouse", args.warehouse),
1428 neg_batch_sle[0]["posting_date"],
1429 neg_batch_sle[0]["posting_time"],
1430 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1431 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301432 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301433
Nabin Haita77b8c92020-12-21 14:45:50 +05301434
Maricad6078aa2022-06-17 15:13:13 +05301435def is_negative_with_precision(neg_sle, is_batch=False):
1436 """
1437 Returns whether system precision rounded qty is insufficient.
1438 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1439 """
1440
1441 if not neg_sle:
1442 return False
1443
1444 field = "cumulative_total" if is_batch else "qty_after_transaction"
1445 precision = cint(frappe.db.get_default("float_precision")) or 2
1446 qty_deficit = flt(neg_sle[0][field], precision)
1447
1448 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1449
1450
Nabin Haita77b8c92020-12-21 14:45:50 +05301451def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301452 return frappe.db.sql(
1453 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301454 select
1455 qty_after_transaction, posting_date, posting_time,
1456 voucher_type, voucher_no
1457 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301458 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301459 item_code = %(item_code)s
1460 and warehouse = %(warehouse)s
1461 and voucher_no != %(voucher_no)s
1462 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1463 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301464 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301465 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301466 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301467 """,
1468 args,
1469 as_dict=1,
1470 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301471
Ankush Menat5eba5752021-12-07 23:03:52 +05301472
1473def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301474 return frappe.db.sql(
1475 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301476 with batch_ledger as (
1477 select
1478 posting_date, posting_time, voucher_type, voucher_no,
1479 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1480 from `tabStock Ledger Entry`
1481 where
1482 item_code = %(item_code)s
1483 and warehouse = %(warehouse)s
1484 and batch_no=%(batch_no)s
1485 and is_cancelled = 0
1486 order by posting_date, posting_time, creation
1487 )
1488 select * from batch_ledger
1489 where
1490 cumulative_total < 0.0
1491 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1492 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301493 """,
1494 args,
1495 as_dict=1,
1496 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301497
1498
1499def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1500 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1501 return True
1502 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1503 return True
1504 return False