blob: 967b2b2294d59e9ebd8a6ee7d1caf05cda654afa [file] [log] [blame]
Anand Doshi885e0742015-03-03 14:55:30 +05301# Copyright (c) 2015, 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 Menateb8b4242022-02-12 13:08:28 +05306from typing import Optional
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
9from frappe import _
10from frappe.model.meta import get_field_precision
Ankush Menat102fff22022-02-19 15:51:04 +053011from frappe.query_builder.functions import Sum
Ankush Menatcef84c22021-12-03 12:18:59 +053012from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
Ankush Menat102fff22022-02-19 15:51:04 +053013from pypika import CustomFunction
Achilles Rasquinha361366e2018-02-14 17:08:59 +053014
Chillar Anand915b3432021-09-02 16:44:59 +053015import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053016from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
Chillar Anand915b3432021-09-02 16:44:59 +053017from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053018 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053019 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053020 get_valuation_method,
21)
Ankush Menatb534fee2022-02-19 20:58:36 +053022from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053023
Nabin Hait97bce3a2021-07-12 13:24:43 +053024
Ankush Menat494bd9e2022-03-28 18:52:46 +053025class NegativeStockError(frappe.ValidationError):
26 pass
27
28
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053029class SerialNoExistsInFutureTransaction(frappe.ValidationError):
30 pass
Nabin Hait902e8602013-01-08 18:29:24 +053031
Anand Doshi5b004ff2013-09-25 19:55:41 +053032
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053033def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053034 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053035
Ankush Menat494bd9e2022-03-28 18:52:46 +053036 args:
37 - allow_negative_stock: disable negative stock valiations if true
38 - via_landed_cost_voucher: landed cost voucher cancels and reposts
39 entries of purchase document. This flag is used to identify if
40 cancellation and repost is happening via landed cost voucher, in
41 such cases certain validations need to be ignored (like negative
42 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053043 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053044 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053045
Nabin Haitca775742013-09-26 16:16:44 +053046 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053047 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053048 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053049 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053050 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053051
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053052 args = get_args_for_future_sle(sl_entries[0])
53 future_sle_exists(args, sl_entries)
54
Nabin Haitca775742013-09-26 16:16:44 +053055 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053056 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053057 validate_serial_no(sle)
58
Nabin Haita77b8c92020-12-21 14:45:50 +053059 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053060 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053061
Ankush Menat494bd9e2022-03-28 18:52:46 +053062 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
63 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
64 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
65 )
66 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053067
Ankush Menat494bd9e2022-03-28 18:52:46 +053068 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
69 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
70 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
71 )
72 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053073
Ankush Menat494bd9e2022-03-28 18:52:46 +053074 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053075 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053076
Nabin Haita77b8c92020-12-21 14:45:50 +053077 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053078
79 if sle.get("voucher_type") == "Stock Reconciliation":
80 # preserve previous_qty_after_transaction for qty reposting
81 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
82
Ankush Menat494bd9e2022-03-28 18:52:46 +053083 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053084 if is_stock_item:
85 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053086 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053087 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053088 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +053089 frappe.msgprint(
90 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
91 )
92
Ankush Menatcef84c22021-12-03 12:18:59 +053093
94def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
95 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
96 if not args.get("posting_date"):
97 args["posting_date"] = nowdate()
98
99 if args.get("is_cancelled") and via_landed_cost_voucher:
100 return
101
102 # Reposts only current voucher SL Entries
103 # Updates valuation rate, stock value, stock queue for current transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530104 update_entries_after(
105 {
106 "item_code": args.get("item_code"),
107 "warehouse": args.get("warehouse"),
108 "posting_date": args.get("posting_date"),
109 "posting_time": args.get("posting_time"),
110 "voucher_type": args.get("voucher_type"),
111 "voucher_no": args.get("voucher_no"),
112 "sle_id": args.get("name"),
113 "creation": args.get("creation"),
114 },
115 allow_negative_stock=allow_negative_stock,
116 via_landed_cost_voucher=via_landed_cost_voucher,
117 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530118
119 # update qty in future sle and Validate negative qty
120 update_qty_in_future_sle(args, allow_negative_stock)
121
Nabin Haitadeb9762014-10-06 11:53:52 +0530122
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530123def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530124 return frappe._dict(
125 {
126 "voucher_type": row.get("voucher_type"),
127 "voucher_no": row.get("voucher_no"),
128 "posting_date": row.get("posting_date"),
129 "posting_time": row.get("posting_time"),
130 }
131 )
132
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530133
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530134def validate_serial_no(sle):
135 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530136
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530137 for sn in get_serial_nos(sle.serial_no):
138 args = copy.deepcopy(sle)
139 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530140 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530141
142 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530143 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530144 voucher_type = frappe.bold(row.voucher_type)
145 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530146 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530147
148 if vouchers:
149 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530150 msg = (
151 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
152 The list of the transactions are as below."""
153 + "<br><br><ul><li>"
154 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530155
Ankush Menat494bd9e2022-03-28 18:52:46 +0530156 msg += "</li><li>".join(vouchers)
157 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530158
Ankush Menat494bd9e2022-03-28 18:52:46 +0530159 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530160 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
161
Ankush Menat494bd9e2022-03-28 18:52:46 +0530162
Nabin Hait186a0452021-02-18 14:14:21 +0530163def validate_cancellation(args):
164 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530165 repost_entry = frappe.db.get_value(
166 "Repost Item Valuation",
167 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
168 ["name", "status"],
169 as_dict=1,
170 )
Nabin Hait186a0452021-02-18 14:14:21 +0530171
172 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530173 if repost_entry.status == "In Progress":
174 frappe.throw(
175 _(
176 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
177 )
178 )
179 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530180 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menataa024fc2021-11-18 12:51:26 +0530181 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530182 doc.cancel()
183 doc.delete()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530184
Ankush Menat494bd9e2022-03-28 18:52:46 +0530185
Nabin Hait9653f602013-08-20 15:37:33 +0530186def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530187 frappe.db.sql(
188 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530189 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530190 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530191 (now(), frappe.session.user, voucher_type, voucher_no),
192 )
193
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530194
Nabin Hait54c865e2015-03-27 15:38:31 +0530195def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530196 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530197 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530198 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530199 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530200 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530201 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530202 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530203
Ankush Menat494bd9e2022-03-28 18:52:46 +0530204
205def repost_future_sle(
206 args=None,
207 voucher_type=None,
208 voucher_no=None,
209 allow_negative_stock=None,
210 via_landed_cost_voucher=False,
211 doc=None,
212):
Nabin Haita77b8c92020-12-21 14:45:50 +0530213 if not args and voucher_type and voucher_no:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530214 args = get_items_to_be_repost(voucher_type, voucher_no, doc)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530215
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530216 distinct_item_warehouses = get_distinct_item_warehouse(args, 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 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530234
Ankush Menat494bd9e2022-03-28 18:52:46 +0530235 distinct_item_warehouses[
236 (args[i].get("item_code"), args[i].get("warehouse"))
237 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530238
Nabin Hait97bce3a2021-07-12 13:24:43 +0530239 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530240 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530241 if ("args_idx" not in data and not data.reposting_status) or (
242 data.sle_changed and data.reposting_status
243 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530244 data.args_idx = len(args)
245 args.append(data.sle)
246 elif data.sle_changed and not data.reposting_status:
247 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530248
Nabin Hait97bce3a2021-07-12 13:24:43 +0530249 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530250 i += 1
251
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530252 if doc and i % 2 == 0:
253 update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
254
255 if doc and args:
256 update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
257
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
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530266def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530267 frappe.db.set_value(
268 doc.doctype,
269 doc.name,
270 {
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,
276 },
277 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530278
279 frappe.db.commit()
280
Ankush Menat494bd9e2022-03-28 18:52:46 +0530281 frappe.publish_realtime(
282 "item_reposting_progress",
283 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
284 )
285
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530286
287def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
288 if doc and doc.items_to_be_repost:
289 return json.loads(doc.items_to_be_repost) or []
290
Ankush Menat494bd9e2022-03-28 18:52:46 +0530291 return frappe.db.get_all(
292 "Stock Ledger Entry",
Nabin Haita77b8c92020-12-21 14:45:50 +0530293 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
Nabin Hait186a0452021-02-18 14:14:21 +0530294 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
Nabin Haita77b8c92020-12-21 14:45:50 +0530295 order_by="creation asc",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530296 group_by="item_code, warehouse",
Nabin Haita77b8c92020-12-21 14:45:50 +0530297 )
Nabin Hait74c281c2013-08-19 16:17:18 +0530298
Ankush Menat494bd9e2022-03-28 18:52:46 +0530299
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530300def get_distinct_item_warehouse(args=None, doc=None):
301 distinct_item_warehouses = {}
302 if doc and doc.distinct_item_and_warehouse:
303 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530304 distinct_item_warehouses = {
305 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
306 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530307 else:
308 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530309 distinct_item_warehouses.setdefault(
310 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
311 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530312
313 return distinct_item_warehouses
314
Ankush Menat494bd9e2022-03-28 18:52:46 +0530315
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530316def get_current_index(doc=None):
317 if doc and doc.current_index:
318 return doc.current_index
319
Ankush Menat494bd9e2022-03-28 18:52:46 +0530320
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530321class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530322 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530323 update valution rate and qty after transaction
324 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530325
Ankush Menat494bd9e2022-03-28 18:52:46 +0530326 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530327
Ankush Menat494bd9e2022-03-28 18:52:46 +0530328 args = {
329 "item_code": "ABC",
330 "warehouse": "XYZ",
331 "posting_date": "2012-12-12",
332 "posting_time": "12:00"
333 }
Nabin Hait902e8602013-01-08 18:29:24 +0530334 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530335
336 def __init__(
337 self,
338 args,
339 allow_zero_rate=False,
340 allow_negative_stock=None,
341 via_landed_cost_voucher=False,
342 verbose=1,
343 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530344 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530345 self.verbose = verbose
346 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530347 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530348 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530349 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
350 item_code=self.item_code
351 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530352
Nabin Haita77b8c92020-12-21 14:45:50 +0530353 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530354 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530355 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530356
Nabin Haita77b8c92020-12-21 14:45:50 +0530357 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
358 self.get_precision()
359 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530360
361 self.new_items_found = False
362 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Nabin Haita77b8c92020-12-21 14:45:50 +0530363
364 self.data = frappe._dict()
365 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530366 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530367
Nabin Haita77b8c92020-12-21 14:45:50 +0530368 def get_precision(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530369 company_base_currency = frappe.get_cached_value("Company", self.company, "default_currency")
370 self.precision = get_field_precision(
371 frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), currency=company_base_currency
372 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530373
374 def initialize_previous_data(self, args):
375 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530376 Get previous sl entries for current item for each related warehouse
377 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530378
Ankush Menat494bd9e2022-03-28 18:52:46 +0530379 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530380
Ankush Menat494bd9e2022-03-28 18:52:46 +0530381 self.data = {
382 warehouse1: {
383 'previus_sle': {},
384 'qty_after_transaction': 10,
385 'valuation_rate': 100,
386 'stock_value': 1000,
387 'prev_stock_value': 1000,
388 'stock_queue': '[[10, 100]]',
389 'stock_value_difference': 1000
390 }
391 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530392
393 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530394 self.data.setdefault(args.warehouse, frappe._dict())
395 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530396 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530397 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530398
Ankush Menatc1d986a2021-08-31 19:43:42 +0530399 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
400 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
401
Ankush Menat494bd9e2022-03-28 18:52:46 +0530402 warehouse_dict.update(
403 {
404 "prev_stock_value": previous_sle.stock_value or 0.0,
405 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
406 "stock_value_difference": 0.0,
407 }
408 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530409
Nabin Haita77b8c92020-12-21 14:45:50 +0530410 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530411 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530412
Nabin Haita77b8c92020-12-21 14:45:50 +0530413 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530414 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530415 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530416 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530417 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530418 entries_to_fix = self.get_future_entries_to_fix()
419
420 i = 0
421 while i < len(entries_to_fix):
422 sle = entries_to_fix[i]
423 i += 1
424
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530425 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530426
Nabin Haita77b8c92020-12-21 14:45:50 +0530427 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530428 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530429
Nabin Hait186a0452021-02-18 14:14:21 +0530430 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530431
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530432 if self.exceptions:
433 self.raise_exceptions()
434
Nabin Hait186a0452021-02-18 14:14:21 +0530435 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530436 sl_entries = self.get_sle_against_current_voucher()
437 for sle in sl_entries:
438 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530439
Nabin Haita77b8c92020-12-21 14:45:50 +0530440 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530441 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530442
Ankush Menat494bd9e2022-03-28 18:52:46 +0530443 return frappe.db.sql(
444 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530445 select
446 *, timestamp(posting_date, posting_time) as "timestamp"
447 from
448 `tabStock Ledger Entry`
449 where
450 item_code = %(item_code)s
451 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530452 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530453 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
454
Nabin Haita77b8c92020-12-21 14:45:50 +0530455 order by
456 creation ASC
457 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530458 """,
459 self.args,
460 as_dict=1,
461 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530462
Nabin Haita77b8c92020-12-21 14:45:50 +0530463 def get_future_entries_to_fix(self):
464 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530465 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
466 {"item_code": self.item_code, "warehouse": self.args.warehouse}
467 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530468
Nabin Haita77b8c92020-12-21 14:45:50 +0530469 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530470
Nabin Haita77b8c92020-12-21 14:45:50 +0530471 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530472 dependant_sle = get_sle_by_voucher_detail_no(
473 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
474 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530475
Nabin Haita77b8c92020-12-21 14:45:50 +0530476 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530477 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530478 elif (
479 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
480 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530481 return entries_to_fix
482 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530483 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530484 return entries_to_fix
485 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
486 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530487 else:
488 return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
489
490 def update_distinct_item_warehouses(self, dependant_sle):
491 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530492 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530493 if key not in self.distinct_item_warehouses:
494 self.distinct_item_warehouses[key] = val
495 self.new_items_found = True
496 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530497 existing_sle_posting_date = (
498 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
499 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530500 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
501 val.sle_changed = True
502 self.distinct_item_warehouses[key] = val
503 self.new_items_found = True
504
505 def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
Nabin Haita77b8c92020-12-21 14:45:50 +0530506 self.initialize_previous_data(dependant_sle)
507
Ankush Menat494bd9e2022-03-28 18:52:46 +0530508 args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict(
509 {"item_code": self.item_code, "warehouse": dependant_sle.warehouse}
510 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530511 future_sle_for_dependant = list(self.get_sle_after_datetime(args))
512
513 entries_to_fix.extend(future_sle_for_dependant)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530514 return sorted(entries_to_fix, key=lambda k: k["timestamp"])
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530515
516 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530517 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
518
Nabin Haita77b8c92020-12-21 14:45:50 +0530519 # previous sle data for this warehouse
520 self.wh_data = self.data[sle.warehouse]
521
Anand Doshi0dc79f42015-04-06 12:59:34 +0530522 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 +0530523 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530524 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530525 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530526 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530527 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530528
Nabin Haita77b8c92020-12-21 14:45:50 +0530529 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530530 if not self.args.get("sle_id"):
531 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530532
Ankush Menat66bf21f2022-01-16 20:45:59 +0530533 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530534 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530535 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530536 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530537 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530538
Ankush Menat494bd9e2022-03-28 18:52:46 +0530539 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
540 self.wh_data.valuation_rate
541 )
542 elif sle.batch_no and frappe.db.get_value(
543 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
544 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530545 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530546 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530547 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530548 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530549 self.wh_data.valuation_rate = sle.valuation_rate
550 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530551 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
552 self.wh_data.valuation_rate
553 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530554 if self.valuation_method != "Moving Average":
555 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530556 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530557 if self.valuation_method == "Moving Average":
558 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530559 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530560 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
561 self.wh_data.valuation_rate
562 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530563 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530564 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530565
Rushabh Mehta54047782013-12-26 11:07:46 +0530566 # rounding as per precision
Nabin Haita77b8c92020-12-21 14:45:50 +0530567 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530568 if not self.wh_data.qty_after_transaction:
569 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530570 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
571 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530572
Nabin Hait902e8602013-01-08 18:29:24 +0530573 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530574 sle.qty_after_transaction = self.wh_data.qty_after_transaction
575 sle.valuation_rate = self.wh_data.valuation_rate
576 sle.stock_value = self.wh_data.stock_value
577 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530578 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530579 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530580 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530581
Ankush Menat701878f2022-03-01 18:08:29 +0530582 if not self.args.get("sle_id"):
583 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530584
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530585 def validate_negative_stock(self, sle):
586 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530587 validate negative stock for entries current datetime onwards
588 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530589 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530590 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530591
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530592 if diff < 0 and abs(diff) > 0.0001:
593 # negative stock!
594 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530595 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530596 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530597 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530598 return True
599
Nabin Haita77b8c92020-12-21 14:45:50 +0530600 def get_dynamic_incoming_outgoing_rate(self, sle):
601 # Get updated incoming/outgoing rate from transaction
602 if sle.recalculate_rate:
603 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
604
605 if flt(sle.actual_qty) >= 0:
606 sle.incoming_rate = rate
607 else:
608 sle.outgoing_rate = rate
609
610 def get_incoming_outgoing_rate_from_transaction(self, sle):
611 rate = 0
612 # Material Transfer, Repack, Manufacturing
613 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530614 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530615 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
616 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530617 elif sle.voucher_type in (
618 "Purchase Receipt",
619 "Purchase Invoice",
620 "Delivery Note",
621 "Sales Invoice",
622 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530623 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530624 from erpnext.controllers.sales_and_purchase_return import (
625 get_rate_for_return, # don't move this import to top
626 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530627
628 rate = get_rate_for_return(
629 sle.voucher_type,
630 sle.voucher_no,
631 sle.item_code,
632 voucher_detail_no=sle.voucher_detail_no,
633 sle=sle,
634 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530635 else:
636 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530637 rate_field = "valuation_rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530638 else:
639 rate_field = "incoming_rate"
640
641 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530642 item_code, incoming_rate = frappe.db.get_value(
643 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
644 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530645
646 if item_code == sle.item_code:
647 rate = incoming_rate
648 else:
649 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
650 ref_doctype = "Packed Item"
651 else:
652 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530653
Ankush Menat494bd9e2022-03-28 18:52:46 +0530654 rate = frappe.db.get_value(
655 ref_doctype,
656 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
657 rate_field,
658 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530659
660 return rate
661
662 def update_outgoing_rate_on_transaction(self, sle):
663 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530664 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
665 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530666 """
667 if sle.actual_qty and sle.voucher_detail_no:
668 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
669
670 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
671 self.update_rate_on_stock_entry(sle, outgoing_rate)
672 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
673 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
674 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
675 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
676
677 def update_rate_on_stock_entry(self, sle, outgoing_rate):
678 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
679
Ankush Menat701878f2022-03-01 18:08:29 +0530680 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
681 if not sle.dependant_sle_voucher_detail_no:
682 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530683
684 def recalculate_amounts_in_stock_entry(self, voucher_no):
685 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530686 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
687 stock_entry.db_update()
688 for d in stock_entry.items:
689 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530690
Nabin Haita77b8c92020-12-21 14:45:50 +0530691 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
692 # Update item's incoming rate on transaction
693 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
694 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530695 frappe.db.set_value(
696 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
697 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530698 else:
699 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530700 frappe.db.set_value(
701 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530702 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530703 "incoming_rate",
704 outgoing_rate,
705 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530706
707 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
708 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530709 frappe.db.set_value(
710 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
711 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530712 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530713 frappe.db.set_value(
714 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
715 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530716
717 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Ankush Menat494bd9e2022-03-28 18:52:46 +0530718 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == "Yes":
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530719 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530720 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530721 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530722 d.db_update()
723
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530724 def get_serialized_values(self, sle):
725 incoming_rate = flt(sle.incoming_rate)
726 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530727 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530728
729 if incoming_rate < 0:
730 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530731 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530732
Nabin Hait2620bf42016-02-29 11:30:27 +0530733 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530734 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530735 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530736 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530737 # In case of delivery/stock issue, get average purchase rate
738 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530739 if not sle.is_cancelled:
740 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
741 stock_value_change = -1 * outgoing_value
742 else:
743 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530744
Nabin Haita77b8c92020-12-21 14:45:50 +0530745 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530746
Nabin Hait2620bf42016-02-29 11:30:27 +0530747 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530748 new_stock_value = (
749 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
750 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530751 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530752 # calculate new valuation rate only if stock value is positive
753 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530754 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530755
Nabin Haita77b8c92020-12-21 14:45:50 +0530756 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530757 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
758 sle.voucher_type, sle.voucher_detail_no
759 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530760 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530761 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530762
Nabin Hait328c4f92020-01-02 19:00:32 +0530763 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
764 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530765 all_serial_nos = frappe.get_all(
766 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
767 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530768
Ankush Menat494bd9e2022-03-28 18:52:46 +0530769 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 +0530770
771 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530772 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530773 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530774 incoming_rate = frappe.db.sql(
775 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530776 select incoming_rate
777 from `tabStock Ledger Entry`
778 where
779 company = %s
780 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530781 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530782 and (serial_no = %s
783 or serial_no like %s
784 or serial_no like %s
785 or serial_no like %s
786 )
787 order by posting_date desc
788 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530789 """,
790 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
791 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530792
793 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
794
795 return incoming_values
796
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530797 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530798 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530799 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530800 if new_stock_qty >= 0:
801 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530802 if flt(self.wh_data.qty_after_transaction) <= 0:
803 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530804 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530805 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
806 actual_qty * sle.incoming_rate
807 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530808
Nabin Haita77b8c92020-12-21 14:45:50 +0530809 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530810
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530811 elif sle.outgoing_rate:
812 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530813 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
814 actual_qty * sle.outgoing_rate
815 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530816
Nabin Haita77b8c92020-12-21 14:45:50 +0530817 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530818 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530819 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530820 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530821 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
822 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530823
Nabin Haita77b8c92020-12-21 14:45:50 +0530824 if not self.wh_data.valuation_rate and actual_qty > 0:
825 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530826
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530827 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800828 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530829 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530830 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
831 sle.voucher_type, sle.voucher_detail_no
832 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800833 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530834 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530835
Ankush Menatf089d392022-02-02 12:51:21 +0530836 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530837 incoming_rate = flt(sle.incoming_rate)
838 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530839 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530840
Ankush Menat494bd9e2022-03-28 18:52:46 +0530841 self.wh_data.qty_after_transaction = round_off_if_near_zero(
842 self.wh_data.qty_after_transaction + actual_qty
843 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530844
Ankush Menat97e18a12022-01-15 17:42:25 +0530845 if self.valuation_method == "LIFO":
846 stock_queue = LIFOValuation(self.wh_data.stock_queue)
847 else:
848 stock_queue = FIFOValuation(self.wh_data.stock_queue)
849
Ankush Menatb534fee2022-02-19 20:58:36 +0530850 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
851
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530852 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530853 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530854 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530855
Ankush Menat4b29fb62021-12-18 18:40:22 +0530856 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530857 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
858 sle.voucher_type, sle.voucher_detail_no
859 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530860 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530861 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530862 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530863 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530864
Ankush Menat494bd9e2022-03-28 18:52:46 +0530865 stock_queue.remove_stock(
866 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
867 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530868
Ankush Menatb534fee2022-02-19 20:58:36 +0530869 _qty, stock_value = stock_queue.get_total_stock_and_value()
870
871 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530872
Ankush Menat97e18a12022-01-15 17:42:25 +0530873 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530874 self.wh_data.stock_value = round_off_if_near_zero(
875 self.wh_data.stock_value + stock_value_difference
876 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530877
Nabin Haita77b8c92020-12-21 14:45:50 +0530878 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530879 self.wh_data.stock_queue.append(
880 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
881 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530882
Ankush Menatb534fee2022-02-19 20:58:36 +0530883 if self.wh_data.qty_after_transaction:
884 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
885
Ankush Menatce0514c2022-02-15 11:41:41 +0530886 def update_batched_values(self, sle):
887 incoming_rate = flt(sle.incoming_rate)
888 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530889
Ankush Menat494bd9e2022-03-28 18:52:46 +0530890 self.wh_data.qty_after_transaction = round_off_if_near_zero(
891 self.wh_data.qty_after_transaction + actual_qty
892 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530893
894 if actual_qty > 0:
895 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530896 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530897 outgoing_rate = get_batch_incoming_rate(
898 item_code=sle.item_code,
899 warehouse=sle.warehouse,
900 batch_no=sle.batch_no,
901 posting_date=sle.posting_date,
902 posting_time=sle.posting_time,
903 creation=sle.creation,
904 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530905 if outgoing_rate is None:
906 # This can *only* happen if qty available for the batch is zero.
907 # in such case fall back various other rates.
908 # future entries will correct the overall accounting as each
909 # batch individually uses moving average rates.
910 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530911 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530912
Ankush Menat494bd9e2022-03-28 18:52:46 +0530913 self.wh_data.stock_value = round_off_if_near_zero(
914 self.wh_data.stock_value + stock_value_difference
915 )
Ankush Menatce0514c2022-02-15 11:41:41 +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
Ankush Menat4b29fb62021-12-18 18:40:22 +0530918
Javier Wong9b11d9b2017-04-14 18:24:04 +0800919 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530920 ref_item_dt = ""
921
922 if voucher_type == "Stock Entry":
923 ref_item_dt = voucher_type + " Detail"
924 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
925 ref_item_dt = voucher_type + " Item"
926
927 if ref_item_dt:
928 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
929 else:
930 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530931
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530932 def get_fallback_rate(self, sle) -> float:
933 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530934 This should only get used for negative stock."""
935 return get_valuation_rate(
936 sle.item_code,
937 sle.warehouse,
938 sle.voucher_type,
939 sle.voucher_no,
940 self.allow_zero_rate,
941 currency=erpnext.get_company_currency(sle.company),
942 company=sle.company,
943 batch_no=sle.batch_no,
944 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530945
Nabin Haita77b8c92020-12-21 14:45:50 +0530946 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530947 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530948 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
949 sle = sle[0] if sle else frappe._dict()
950 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530951
Nabin Haita77b8c92020-12-21 14:45:50 +0530952 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530953 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530954 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530955
956 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530957 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530958 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530959 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530960
Ankush Menat494bd9e2022-03-28 18:52:46 +0530961 if (
962 exceptions[0]["voucher_type"],
963 exceptions[0]["voucher_no"],
964 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +0530965
Nabin Haita77b8c92020-12-21 14:45:50 +0530966 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530967 abs(deficiency),
968 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
969 frappe.get_desk_link("Warehouse", warehouse),
970 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530971 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530972 msg = _(
973 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
974 ).format(
975 abs(deficiency),
976 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
977 frappe.get_desk_link("Warehouse", warehouse),
978 exceptions[0]["posting_date"],
979 exceptions[0]["posting_time"],
980 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
981 )
Rushabh Mehta538607e2016-06-12 11:03:00 +0530982
Nabin Haita77b8c92020-12-21 14:45:50 +0530983 if msg:
984 msg_list.append(msg)
985
986 if msg_list:
987 message = "\n\n".join(msg_list)
988 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530989 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +0530990 else:
991 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530992
Nabin Haita77b8c92020-12-21 14:45:50 +0530993 def update_bin(self):
994 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530995 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +0530996 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +0530997
Ankush Menat494bd9e2022-03-28 18:52:46 +0530998 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +0530999 if data.valuation_rate is not None:
1000 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301001 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301002
marination8418c4b2021-06-22 21:35:25 +05301003
1004def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1005 """get stock ledger entries filtered by specific posting datetime conditions"""
1006
Ankush Menat494bd9e2022-03-28 18:52:46 +05301007 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301008 if not args.get("posting_date"):
1009 args["posting_date"] = "1900-01-01"
1010 if not args.get("posting_time"):
1011 args["posting_time"] = "00:00"
1012
1013 voucher_condition = ""
1014 if exclude_current_voucher:
1015 voucher_no = args.get("voucher_no")
1016 voucher_condition = f"and voucher_no != '{voucher_no}'"
1017
Ankush Menat494bd9e2022-03-28 18:52:46 +05301018 sle = frappe.db.sql(
1019 """
marination8418c4b2021-06-22 21:35:25 +05301020 select *, timestamp(posting_date, posting_time) as "timestamp"
1021 from `tabStock Ledger Entry`
1022 where item_code = %(item_code)s
1023 and warehouse = %(warehouse)s
1024 and is_cancelled = 0
1025 {voucher_condition}
1026 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1027 order by timestamp(posting_date, posting_time) desc, creation desc
1028 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301029 for update""".format(
1030 voucher_condition=voucher_condition
1031 ),
1032 args,
1033 as_dict=1,
1034 )
marination8418c4b2021-06-22 21:35:25 +05301035
1036 return sle[0] if sle else frappe._dict()
1037
Ankush Menat494bd9e2022-03-28 18:52:46 +05301038
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301039def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301040 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301041 get the last sle on or before the current time-bucket,
1042 to get actual qty before transaction, this function
1043 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301044
Ankush Menat494bd9e2022-03-28 18:52:46 +05301045 args = {
1046 "item_code": "ABC",
1047 "warehouse": "XYZ",
1048 "posting_date": "2012-12-12",
1049 "posting_time": "12:00",
1050 "sle": "name of reference Stock Ledger Entry"
1051 }
Anand Doshi1b531862013-01-10 19:29:51 +05301052 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301053 args["name"] = args.get("sle", None) or ""
1054 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301055 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301056
Ankush Menat494bd9e2022-03-28 18:52:46 +05301057
1058def get_stock_ledger_entries(
1059 previous_sle,
1060 operator=None,
1061 order="desc",
1062 limit=None,
1063 for_update=False,
1064 debug=False,
1065 check_serial_no=True,
1066):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301067 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301068 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1069 operator
1070 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301071 if previous_sle.get("warehouse"):
1072 conditions += " and warehouse = %(warehouse)s"
1073 elif previous_sle.get("warehouse_condition"):
1074 conditions += " and " + previous_sle.get("warehouse_condition")
1075
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301076 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301077 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1078 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301079 conditions += (
1080 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301081 (
1082 serial_no = {0}
1083 or serial_no like {1}
1084 or serial_no like {2}
1085 or serial_no like {3}
1086 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301087 """
1088 ).format(
1089 frappe.db.escape(serial_no),
1090 frappe.db.escape("{}\n%".format(serial_no)),
1091 frappe.db.escape("%\n{}".format(serial_no)),
1092 frappe.db.escape("%\n{}\n%".format(serial_no)),
1093 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301094
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301095 if not previous_sle.get("posting_date"):
1096 previous_sle["posting_date"] = "1900-01-01"
1097 if not previous_sle.get("posting_time"):
1098 previous_sle["posting_time"] = "00:00"
1099
1100 if operator in (">", "<=") and previous_sle.get("name"):
1101 conditions += " and name!=%(name)s"
1102
Ankush Menat494bd9e2022-03-28 18:52:46 +05301103 return frappe.db.sql(
1104 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301105 select *, timestamp(posting_date, posting_time) as "timestamp"
1106 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301107 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301108 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301109 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301110 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301111 %(limit)s %(for_update)s"""
1112 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301113 "conditions": conditions,
1114 "limit": limit or "",
1115 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301116 "order": order,
1117 },
1118 previous_sle,
1119 as_dict=1,
1120 debug=debug,
1121 )
1122
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301123
Nabin Haita77b8c92020-12-21 14:45:50 +05301124def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301125 return frappe.db.get_value(
1126 "Stock Ledger Entry",
1127 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1128 [
1129 "item_code",
1130 "warehouse",
1131 "posting_date",
1132 "posting_time",
1133 "timestamp(posting_date, posting_time) as timestamp",
1134 ],
1135 as_dict=1,
1136 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301137
Ankush Menatce0514c2022-02-15 11:41:41 +05301138
Ankush Menat494bd9e2022-03-28 18:52:46 +05301139def get_batch_incoming_rate(
1140 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1141):
1142
1143 Timestamp = CustomFunction("timestamp", ["date", "time"])
Ankush Menat102fff22022-02-19 15:51:04 +05301144
1145 sle = frappe.qb.DocType("Stock Ledger Entry")
1146
Ankush Menat494bd9e2022-03-28 18:52:46 +05301147 timestamp_condition = Timestamp(sle.posting_date, sle.posting_time) < Timestamp(
1148 posting_date, posting_time
1149 )
Ankush Menat102fff22022-02-19 15:51:04 +05301150 if creation:
1151 timestamp_condition |= (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301152 Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time)
1153 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301154
1155 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301156 frappe.qb.from_(sle)
1157 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1158 .where(
1159 (sle.item_code == item_code)
1160 & (sle.warehouse == warehouse)
1161 & (sle.batch_no == batch_no)
1162 & (sle.is_cancelled == 0)
1163 )
1164 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301165 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301166
1167 if batch_details and batch_details[0].batch_qty:
1168 return batch_details[0].batch_value / batch_details[0].batch_qty
1169
1170
Ankush Menat494bd9e2022-03-28 18:52:46 +05301171def get_valuation_rate(
1172 item_code,
1173 warehouse,
1174 voucher_type,
1175 voucher_no,
1176 allow_zero_rate=False,
1177 currency=None,
1178 company=None,
1179 raise_error_if_no_rate=True,
1180 batch_no=None,
1181):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301182
Ankush Menatf7ffe042021-11-01 13:21:14 +05301183 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301184 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301185
Ankush Menat342d09a2022-02-19 14:28:51 +05301186 last_valuation_rate = None
1187
1188 # Get moving average rate of a specific batch number
1189 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301190 last_valuation_rate = frappe.db.sql(
1191 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301192 select sum(stock_value_difference) / sum(actual_qty)
1193 from `tabStock Ledger Entry`
1194 where
1195 item_code = %s
1196 AND warehouse = %s
1197 AND batch_no = %s
1198 AND is_cancelled = 0
1199 AND NOT (voucher_no = %s AND voucher_type = %s)
1200 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301201 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1202 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301203
Ankush Menatf7ffe042021-11-01 13:21:14 +05301204 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301205 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301206 last_valuation_rate = frappe.db.sql(
1207 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301208 from `tabStock Ledger Entry` force index (item_warehouse)
1209 where
1210 item_code = %s
1211 AND warehouse = %s
1212 AND valuation_rate >= 0
1213 AND is_cancelled = 0
1214 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301215 order by posting_date desc, posting_time desc, name desc limit 1""",
1216 (item_code, warehouse, voucher_no, voucher_type),
1217 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301218
1219 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301220 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301221 last_valuation_rate = frappe.db.sql(
1222 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301223 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301224 where
1225 item_code = %s
1226 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301227 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301228 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301229 order by posting_date desc, posting_time desc, name desc limit 1""",
1230 (item_code, voucher_no, voucher_type),
1231 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301232
Nabin Haita645f362018-03-01 10:31:24 +05301233 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301234 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301235
1236 # If negative stock allowed, and item delivered without any incoming entry,
1237 # system does not found any SLE, then take valuation rate from Item
1238 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301239
1240 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301241 # try Item Standard rate
1242 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301243
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301244 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301245 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301246 valuation_rate = frappe.db.get_value(
1247 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1248 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301249
Ankush Menat494bd9e2022-03-28 18:52:46 +05301250 if (
1251 not allow_zero_rate
1252 and not valuation_rate
1253 and raise_error_if_no_rate
1254 and cint(erpnext.is_perpetual_inventory_enabled(company))
1255 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301256 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301257
Ankush Menat494bd9e2022-03-28 18:52:46 +05301258 message = _(
1259 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1260 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301261 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301262 solutions = (
1263 "<li>"
1264 + _(
1265 "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."
1266 ).format(voucher_type)
1267 + "</li>"
1268 )
1269 solutions += (
1270 "<li>"
1271 + _("If not, you can Cancel / Submit this entry")
1272 + " {0} ".format(frappe.bold("after"))
1273 + _("performing either one below:")
1274 + "</li>"
1275 )
Marica97715f22020-05-11 20:45:37 +05301276 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1277 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1278 msg = message + solutions + sub_solutions + "</li>"
1279
1280 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301281
1282 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301283
Ankush Menat494bd9e2022-03-28 18:52:46 +05301284
Ankush Menate7109c12021-08-26 16:40:45 +05301285def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301286 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301287 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301288 qty_shift = args.actual_qty
1289
1290 # find difference/shift in qty caused by stock reconciliation
1291 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301292 qty_shift = get_stock_reco_qty_shift(args)
1293
1294 # find the next nearest stock reco so that we only recalculate SLEs till that point
1295 next_stock_reco_detail = get_next_stock_reco(args)
1296 if next_stock_reco_detail:
1297 detail = next_stock_reco_detail[0]
1298 # add condition to update SLEs before this date & time
1299 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301300
Ankush Menat494bd9e2022-03-28 18:52:46 +05301301 frappe.db.sql(
1302 """
Nabin Hait186a0452021-02-18 14:14:21 +05301303 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301304 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301305 where
1306 item_code = %(item_code)s
1307 and warehouse = %(warehouse)s
1308 and voucher_no != %(voucher_no)s
1309 and is_cancelled = 0
1310 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1311 or (
1312 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1313 and creation > %(creation)s
1314 )
1315 )
marination40389772021-07-02 17:13:45 +05301316 {datetime_limit_condition}
Ankush Menat494bd9e2022-03-28 18:52:46 +05301317 """.format(
1318 qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
1319 ),
1320 args,
1321 )
Nabin Hait186a0452021-02-18 14:14:21 +05301322
1323 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1324
Ankush Menat494bd9e2022-03-28 18:52:46 +05301325
marination40389772021-07-02 17:13:45 +05301326def get_stock_reco_qty_shift(args):
1327 stock_reco_qty_shift = 0
1328 if args.get("is_cancelled"):
1329 if args.get("previous_qty_after_transaction"):
1330 # get qty (balance) that was set at submission
1331 last_balance = args.get("previous_qty_after_transaction")
1332 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1333 else:
1334 stock_reco_qty_shift = flt(args.actual_qty)
1335 else:
1336 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301337 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1338 "qty_after_transaction"
1339 )
marination40389772021-07-02 17:13:45 +05301340
1341 if last_balance is not None:
1342 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1343 else:
1344 stock_reco_qty_shift = args.qty_after_transaction
1345
1346 return stock_reco_qty_shift
1347
Ankush Menat494bd9e2022-03-28 18:52:46 +05301348
marination40389772021-07-02 17:13:45 +05301349def get_next_stock_reco(args):
1350 """Returns next nearest stock reconciliaton's details."""
1351
Ankush Menat494bd9e2022-03-28 18:52:46 +05301352 return frappe.db.sql(
1353 """
marination40389772021-07-02 17:13:45 +05301354 select
1355 name, posting_date, posting_time, creation, voucher_no
1356 from
marination8c441262021-07-02 17:46:05 +05301357 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301358 where
1359 item_code = %(item_code)s
1360 and warehouse = %(warehouse)s
1361 and voucher_type = 'Stock Reconciliation'
1362 and voucher_no != %(voucher_no)s
1363 and is_cancelled = 0
1364 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1365 or (
1366 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1367 and creation > %(creation)s
1368 )
1369 )
1370 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301371 """,
1372 args,
1373 as_dict=1,
1374 )
1375
marination40389772021-07-02 17:13:45 +05301376
1377def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301378 return f"""
1379 and
1380 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1381 or (
1382 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1383 and creation < '{detail.creation}'
1384 )
1385 )"""
1386
Ankush Menat494bd9e2022-03-28 18:52:46 +05301387
Ankush Menate7109c12021-08-26 16:40:45 +05301388def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301389 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301390 return
1391 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1392 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301393
Ankush Menat5eba5752021-12-07 23:03:52 +05301394 neg_sle = get_future_sle_with_negative_qty(args)
1395 if neg_sle:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301396 message = _(
1397 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1398 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301399 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301400 frappe.get_desk_link("Item", args.item_code),
1401 frappe.get_desk_link("Warehouse", args.warehouse),
1402 neg_sle[0]["posting_date"],
1403 neg_sle[0]["posting_time"],
1404 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1405 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301406
Ankush Menat494bd9e2022-03-28 18:52:46 +05301407 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301408
1409 if not args.batch_no:
1410 return
1411
1412 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1413 if neg_batch_sle:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301414 message = _(
1415 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1416 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301417 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301418 frappe.get_desk_link("Batch", args.batch_no),
1419 frappe.get_desk_link("Warehouse", args.warehouse),
1420 neg_batch_sle[0]["posting_date"],
1421 neg_batch_sle[0]["posting_time"],
1422 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1423 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301424 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301425
Nabin Haita77b8c92020-12-21 14:45:50 +05301426
1427def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301428 return frappe.db.sql(
1429 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301430 select
1431 qty_after_transaction, posting_date, posting_time,
1432 voucher_type, voucher_no
1433 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301434 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301435 item_code = %(item_code)s
1436 and warehouse = %(warehouse)s
1437 and voucher_no != %(voucher_no)s
1438 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1439 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301440 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301441 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301442 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301443 """,
1444 args,
1445 as_dict=1,
1446 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301447
Ankush Menat5eba5752021-12-07 23:03:52 +05301448
1449def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301450 return frappe.db.sql(
1451 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301452 with batch_ledger as (
1453 select
1454 posting_date, posting_time, voucher_type, voucher_no,
1455 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1456 from `tabStock Ledger Entry`
1457 where
1458 item_code = %(item_code)s
1459 and warehouse = %(warehouse)s
1460 and batch_no=%(batch_no)s
1461 and is_cancelled = 0
1462 order by posting_date, posting_time, creation
1463 )
1464 select * from batch_ledger
1465 where
1466 cumulative_total < 0.0
1467 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1468 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301469 """,
1470 args,
1471 as_dict=1,
1472 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301473
1474
1475def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1476 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1477 return True
1478 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1479 return True
1480 return False