blob: f716ff6d9b5e664e8c7f9696240d7d24e5793ce2 [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",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530622 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530623 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530624 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530625 from erpnext.controllers.sales_and_purchase_return import (
626 get_rate_for_return, # don't move this import to top
627 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530628
629 rate = get_rate_for_return(
630 sle.voucher_type,
631 sle.voucher_no,
632 sle.item_code,
633 voucher_detail_no=sle.voucher_detail_no,
634 sle=sle,
635 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530636 else:
637 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530638 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530639 elif sle.voucher_type == "Subcontracting Receipt":
640 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530641 else:
642 rate_field = "incoming_rate"
643
644 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530645 item_code, incoming_rate = frappe.db.get_value(
646 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
647 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530648
649 if item_code == sle.item_code:
650 rate = incoming_rate
651 else:
652 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
653 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530654 elif sle == "Subcontracting Receipt":
655 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530656 else:
657 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530658
Ankush Menat494bd9e2022-03-28 18:52:46 +0530659 rate = frappe.db.get_value(
660 ref_doctype,
661 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
662 rate_field,
663 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530664
665 return rate
666
667 def update_outgoing_rate_on_transaction(self, sle):
668 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530669 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
670 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530671 """
672 if sle.actual_qty and sle.voucher_detail_no:
673 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
674
675 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
676 self.update_rate_on_stock_entry(sle, outgoing_rate)
677 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
678 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
679 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
680 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530681 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
682 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530683
684 def update_rate_on_stock_entry(self, sle, outgoing_rate):
685 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
686
Ankush Menat701878f2022-03-01 18:08:29 +0530687 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
688 if not sle.dependant_sle_voucher_detail_no:
689 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530690
691 def recalculate_amounts_in_stock_entry(self, voucher_no):
692 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530693 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
694 stock_entry.db_update()
695 for d in stock_entry.items:
696 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530697
Nabin Haita77b8c92020-12-21 14:45:50 +0530698 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
699 # Update item's incoming rate on transaction
700 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
701 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530702 frappe.db.set_value(
703 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
704 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530705 else:
706 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530707 frappe.db.set_value(
708 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530709 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530710 "incoming_rate",
711 outgoing_rate,
712 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530713
714 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
715 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530716 frappe.db.set_value(
717 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
718 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530719 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530720 frappe.db.set_value(
721 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
722 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530723
Sagar Sharma323bdf82022-05-17 15:14:07 +0530724 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
725 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
726 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
727 else:
728 frappe.db.set_value(
729 "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
730 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530731
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530732 def get_serialized_values(self, sle):
733 incoming_rate = flt(sle.incoming_rate)
734 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530735 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530736
737 if incoming_rate < 0:
738 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530739 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530740
Nabin Hait2620bf42016-02-29 11:30:27 +0530741 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530742 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530743 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530744 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530745 # In case of delivery/stock issue, get average purchase rate
746 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530747 if not sle.is_cancelled:
748 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
749 stock_value_change = -1 * outgoing_value
750 else:
751 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530752
Nabin Haita77b8c92020-12-21 14:45:50 +0530753 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530754
Nabin Hait2620bf42016-02-29 11:30:27 +0530755 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530756 new_stock_value = (
757 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
758 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530759 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530760 # calculate new valuation rate only if stock value is positive
761 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530762 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530763
Nabin Haita77b8c92020-12-21 14:45:50 +0530764 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530765 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
766 sle.voucher_type, sle.voucher_detail_no
767 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530768 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530769 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530770
Nabin Hait328c4f92020-01-02 19:00:32 +0530771 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
772 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530773 all_serial_nos = frappe.get_all(
774 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
775 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530776
Ankush Menat494bd9e2022-03-28 18:52:46 +0530777 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 +0530778
779 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530780 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530781 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530782 incoming_rate = frappe.db.sql(
783 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530784 select incoming_rate
785 from `tabStock Ledger Entry`
786 where
787 company = %s
788 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530789 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530790 and (serial_no = %s
791 or serial_no like %s
792 or serial_no like %s
793 or serial_no like %s
794 )
795 order by posting_date desc
796 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530797 """,
798 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
799 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530800
801 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
802
803 return incoming_values
804
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530805 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530806 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530807 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530808 if new_stock_qty >= 0:
809 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530810 if flt(self.wh_data.qty_after_transaction) <= 0:
811 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530812 else:
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.incoming_rate
815 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530816
Nabin Haita77b8c92020-12-21 14:45:50 +0530817 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530818
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530819 elif sle.outgoing_rate:
820 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530821 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
822 actual_qty * sle.outgoing_rate
823 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530824
Nabin Haita77b8c92020-12-21 14:45:50 +0530825 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530826 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530827 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530828 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530829 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
830 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530831
Nabin Haita77b8c92020-12-21 14:45:50 +0530832 if not self.wh_data.valuation_rate and actual_qty > 0:
833 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530834
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530835 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800836 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530837 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530838 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
839 sle.voucher_type, sle.voucher_detail_no
840 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800841 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530842 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530843
Ankush Menatf089d392022-02-02 12:51:21 +0530844 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530845 incoming_rate = flt(sle.incoming_rate)
846 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530847 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530848
Ankush Menat494bd9e2022-03-28 18:52:46 +0530849 self.wh_data.qty_after_transaction = round_off_if_near_zero(
850 self.wh_data.qty_after_transaction + actual_qty
851 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530852
Ankush Menat97e18a12022-01-15 17:42:25 +0530853 if self.valuation_method == "LIFO":
854 stock_queue = LIFOValuation(self.wh_data.stock_queue)
855 else:
856 stock_queue = FIFOValuation(self.wh_data.stock_queue)
857
Ankush Menatb534fee2022-02-19 20:58:36 +0530858 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
859
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530860 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530861 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530862 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530863
Ankush Menat4b29fb62021-12-18 18:40:22 +0530864 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530865 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
866 sle.voucher_type, sle.voucher_detail_no
867 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530868 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530869 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530870 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530871 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530872
Ankush Menat494bd9e2022-03-28 18:52:46 +0530873 stock_queue.remove_stock(
874 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
875 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530876
Ankush Menatb534fee2022-02-19 20:58:36 +0530877 _qty, stock_value = stock_queue.get_total_stock_and_value()
878
879 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530880
Ankush Menat97e18a12022-01-15 17:42:25 +0530881 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530882 self.wh_data.stock_value = round_off_if_near_zero(
883 self.wh_data.stock_value + stock_value_difference
884 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530885
Nabin Haita77b8c92020-12-21 14:45:50 +0530886 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530887 self.wh_data.stock_queue.append(
888 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
889 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530890
Ankush Menatb534fee2022-02-19 20:58:36 +0530891 if self.wh_data.qty_after_transaction:
892 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
893
Ankush Menatce0514c2022-02-15 11:41:41 +0530894 def update_batched_values(self, sle):
895 incoming_rate = flt(sle.incoming_rate)
896 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530897
Ankush Menat494bd9e2022-03-28 18:52:46 +0530898 self.wh_data.qty_after_transaction = round_off_if_near_zero(
899 self.wh_data.qty_after_transaction + actual_qty
900 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530901
902 if actual_qty > 0:
903 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530904 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530905 outgoing_rate = get_batch_incoming_rate(
906 item_code=sle.item_code,
907 warehouse=sle.warehouse,
908 batch_no=sle.batch_no,
909 posting_date=sle.posting_date,
910 posting_time=sle.posting_time,
911 creation=sle.creation,
912 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530913 if outgoing_rate is None:
914 # This can *only* happen if qty available for the batch is zero.
915 # in such case fall back various other rates.
916 # future entries will correct the overall accounting as each
917 # batch individually uses moving average rates.
918 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530919 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530920
Ankush Menat494bd9e2022-03-28 18:52:46 +0530921 self.wh_data.stock_value = round_off_if_near_zero(
922 self.wh_data.stock_value + stock_value_difference
923 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530924 if self.wh_data.qty_after_transaction:
925 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530926
Javier Wong9b11d9b2017-04-14 18:24:04 +0800927 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530928 ref_item_dt = ""
929
930 if voucher_type == "Stock Entry":
931 ref_item_dt = voucher_type + " Detail"
932 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
933 ref_item_dt = voucher_type + " Item"
934
935 if ref_item_dt:
936 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
937 else:
938 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530939
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530940 def get_fallback_rate(self, sle) -> float:
941 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530942 This should only get used for negative stock."""
943 return get_valuation_rate(
944 sle.item_code,
945 sle.warehouse,
946 sle.voucher_type,
947 sle.voucher_no,
948 self.allow_zero_rate,
949 currency=erpnext.get_company_currency(sle.company),
950 company=sle.company,
951 batch_no=sle.batch_no,
952 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530953
Nabin Haita77b8c92020-12-21 14:45:50 +0530954 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530955 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530956 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
957 sle = sle[0] if sle else frappe._dict()
958 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530959
Nabin Haita77b8c92020-12-21 14:45:50 +0530960 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530961 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530962 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530963
964 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530965 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530966 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530967 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530968
Ankush Menat494bd9e2022-03-28 18:52:46 +0530969 if (
970 exceptions[0]["voucher_type"],
971 exceptions[0]["voucher_no"],
972 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +0530973
Nabin Haita77b8c92020-12-21 14:45:50 +0530974 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530975 abs(deficiency),
976 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
977 frappe.get_desk_link("Warehouse", warehouse),
978 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530979 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530980 msg = _(
981 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
982 ).format(
983 abs(deficiency),
984 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
985 frappe.get_desk_link("Warehouse", warehouse),
986 exceptions[0]["posting_date"],
987 exceptions[0]["posting_time"],
988 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
989 )
Rushabh Mehta538607e2016-06-12 11:03:00 +0530990
Nabin Haita77b8c92020-12-21 14:45:50 +0530991 if msg:
992 msg_list.append(msg)
993
994 if msg_list:
995 message = "\n\n".join(msg_list)
996 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530997 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +0530998 else:
999 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301000
Nabin Haita77b8c92020-12-21 14:45:50 +05301001 def update_bin(self):
1002 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301003 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301004 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301005
Ankush Menat494bd9e2022-03-28 18:52:46 +05301006 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301007 if data.valuation_rate is not None:
1008 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301009 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301010
marination8418c4b2021-06-22 21:35:25 +05301011
1012def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1013 """get stock ledger entries filtered by specific posting datetime conditions"""
1014
Ankush Menat494bd9e2022-03-28 18:52:46 +05301015 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301016 if not args.get("posting_date"):
1017 args["posting_date"] = "1900-01-01"
1018 if not args.get("posting_time"):
1019 args["posting_time"] = "00:00"
1020
1021 voucher_condition = ""
1022 if exclude_current_voucher:
1023 voucher_no = args.get("voucher_no")
1024 voucher_condition = f"and voucher_no != '{voucher_no}'"
1025
Ankush Menat494bd9e2022-03-28 18:52:46 +05301026 sle = frappe.db.sql(
1027 """
marination8418c4b2021-06-22 21:35:25 +05301028 select *, timestamp(posting_date, posting_time) as "timestamp"
1029 from `tabStock Ledger Entry`
1030 where item_code = %(item_code)s
1031 and warehouse = %(warehouse)s
1032 and is_cancelled = 0
1033 {voucher_condition}
1034 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1035 order by timestamp(posting_date, posting_time) desc, creation desc
1036 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301037 for update""".format(
1038 voucher_condition=voucher_condition
1039 ),
1040 args,
1041 as_dict=1,
1042 )
marination8418c4b2021-06-22 21:35:25 +05301043
1044 return sle[0] if sle else frappe._dict()
1045
Ankush Menat494bd9e2022-03-28 18:52:46 +05301046
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301047def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301048 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301049 get the last sle on or before the current time-bucket,
1050 to get actual qty before transaction, this function
1051 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301052
Ankush Menat494bd9e2022-03-28 18:52:46 +05301053 args = {
1054 "item_code": "ABC",
1055 "warehouse": "XYZ",
1056 "posting_date": "2012-12-12",
1057 "posting_time": "12:00",
1058 "sle": "name of reference Stock Ledger Entry"
1059 }
Anand Doshi1b531862013-01-10 19:29:51 +05301060 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301061 args["name"] = args.get("sle", None) or ""
1062 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301063 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301064
Ankush Menat494bd9e2022-03-28 18:52:46 +05301065
1066def get_stock_ledger_entries(
1067 previous_sle,
1068 operator=None,
1069 order="desc",
1070 limit=None,
1071 for_update=False,
1072 debug=False,
1073 check_serial_no=True,
1074):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301075 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301076 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1077 operator
1078 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301079 if previous_sle.get("warehouse"):
1080 conditions += " and warehouse = %(warehouse)s"
1081 elif previous_sle.get("warehouse_condition"):
1082 conditions += " and " + previous_sle.get("warehouse_condition")
1083
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301084 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301085 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1086 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301087 conditions += (
1088 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301089 (
1090 serial_no = {0}
1091 or serial_no like {1}
1092 or serial_no like {2}
1093 or serial_no like {3}
1094 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301095 """
1096 ).format(
1097 frappe.db.escape(serial_no),
1098 frappe.db.escape("{}\n%".format(serial_no)),
1099 frappe.db.escape("%\n{}".format(serial_no)),
1100 frappe.db.escape("%\n{}\n%".format(serial_no)),
1101 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301102
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301103 if not previous_sle.get("posting_date"):
1104 previous_sle["posting_date"] = "1900-01-01"
1105 if not previous_sle.get("posting_time"):
1106 previous_sle["posting_time"] = "00:00"
1107
1108 if operator in (">", "<=") and previous_sle.get("name"):
1109 conditions += " and name!=%(name)s"
1110
Ankush Menat494bd9e2022-03-28 18:52:46 +05301111 return frappe.db.sql(
1112 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301113 select *, timestamp(posting_date, posting_time) as "timestamp"
1114 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301115 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301116 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301117 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301118 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301119 %(limit)s %(for_update)s"""
1120 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301121 "conditions": conditions,
1122 "limit": limit or "",
1123 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301124 "order": order,
1125 },
1126 previous_sle,
1127 as_dict=1,
1128 debug=debug,
1129 )
1130
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301131
Nabin Haita77b8c92020-12-21 14:45:50 +05301132def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301133 return frappe.db.get_value(
1134 "Stock Ledger Entry",
1135 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1136 [
1137 "item_code",
1138 "warehouse",
1139 "posting_date",
1140 "posting_time",
1141 "timestamp(posting_date, posting_time) as timestamp",
1142 ],
1143 as_dict=1,
1144 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301145
Ankush Menatce0514c2022-02-15 11:41:41 +05301146
Ankush Menat494bd9e2022-03-28 18:52:46 +05301147def get_batch_incoming_rate(
1148 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1149):
1150
1151 Timestamp = CustomFunction("timestamp", ["date", "time"])
Ankush Menat102fff22022-02-19 15:51:04 +05301152
1153 sle = frappe.qb.DocType("Stock Ledger Entry")
1154
Ankush Menat494bd9e2022-03-28 18:52:46 +05301155 timestamp_condition = Timestamp(sle.posting_date, sle.posting_time) < Timestamp(
1156 posting_date, posting_time
1157 )
Ankush Menat102fff22022-02-19 15:51:04 +05301158 if creation:
1159 timestamp_condition |= (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301160 Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time)
1161 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301162
1163 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301164 frappe.qb.from_(sle)
1165 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1166 .where(
1167 (sle.item_code == item_code)
1168 & (sle.warehouse == warehouse)
1169 & (sle.batch_no == batch_no)
1170 & (sle.is_cancelled == 0)
1171 )
1172 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301173 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301174
1175 if batch_details and batch_details[0].batch_qty:
1176 return batch_details[0].batch_value / batch_details[0].batch_qty
1177
1178
Ankush Menat494bd9e2022-03-28 18:52:46 +05301179def get_valuation_rate(
1180 item_code,
1181 warehouse,
1182 voucher_type,
1183 voucher_no,
1184 allow_zero_rate=False,
1185 currency=None,
1186 company=None,
1187 raise_error_if_no_rate=True,
1188 batch_no=None,
1189):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301190
Ankush Menatf7ffe042021-11-01 13:21:14 +05301191 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301192 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301193
Ankush Menat342d09a2022-02-19 14:28:51 +05301194 last_valuation_rate = None
1195
1196 # Get moving average rate of a specific batch number
1197 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301198 last_valuation_rate = frappe.db.sql(
1199 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301200 select sum(stock_value_difference) / sum(actual_qty)
1201 from `tabStock Ledger Entry`
1202 where
1203 item_code = %s
1204 AND warehouse = %s
1205 AND batch_no = %s
1206 AND is_cancelled = 0
1207 AND NOT (voucher_no = %s AND voucher_type = %s)
1208 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301209 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1210 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301211
Ankush Menatf7ffe042021-11-01 13:21:14 +05301212 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301213 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301214 last_valuation_rate = frappe.db.sql(
1215 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301216 from `tabStock Ledger Entry` force index (item_warehouse)
1217 where
1218 item_code = %s
1219 AND warehouse = %s
1220 AND valuation_rate >= 0
1221 AND is_cancelled = 0
1222 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301223 order by posting_date desc, posting_time desc, name desc limit 1""",
1224 (item_code, warehouse, voucher_no, voucher_type),
1225 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301226
1227 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301228 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301229 last_valuation_rate = frappe.db.sql(
1230 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301231 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301232 where
1233 item_code = %s
1234 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301235 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301236 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301237 order by posting_date desc, posting_time desc, name desc limit 1""",
1238 (item_code, voucher_no, voucher_type),
1239 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301240
Nabin Haita645f362018-03-01 10:31:24 +05301241 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301242 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301243
1244 # If negative stock allowed, and item delivered without any incoming entry,
1245 # system does not found any SLE, then take valuation rate from Item
1246 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301247
1248 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301249 # try Item Standard rate
1250 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301251
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301252 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301253 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301254 valuation_rate = frappe.db.get_value(
1255 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1256 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301257
Ankush Menat494bd9e2022-03-28 18:52:46 +05301258 if (
1259 not allow_zero_rate
1260 and not valuation_rate
1261 and raise_error_if_no_rate
1262 and cint(erpnext.is_perpetual_inventory_enabled(company))
1263 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301264 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301265
Ankush Menat494bd9e2022-03-28 18:52:46 +05301266 message = _(
1267 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1268 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301269 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301270 solutions = (
1271 "<li>"
1272 + _(
1273 "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."
1274 ).format(voucher_type)
1275 + "</li>"
1276 )
1277 solutions += (
1278 "<li>"
1279 + _("If not, you can Cancel / Submit this entry")
1280 + " {0} ".format(frappe.bold("after"))
1281 + _("performing either one below:")
1282 + "</li>"
1283 )
Marica97715f22020-05-11 20:45:37 +05301284 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1285 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1286 msg = message + solutions + sub_solutions + "</li>"
1287
1288 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301289
1290 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301291
Ankush Menat494bd9e2022-03-28 18:52:46 +05301292
Ankush Menate7109c12021-08-26 16:40:45 +05301293def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301294 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301295 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301296 qty_shift = args.actual_qty
1297
1298 # find difference/shift in qty caused by stock reconciliation
1299 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301300 qty_shift = get_stock_reco_qty_shift(args)
1301
1302 # find the next nearest stock reco so that we only recalculate SLEs till that point
1303 next_stock_reco_detail = get_next_stock_reco(args)
1304 if next_stock_reco_detail:
1305 detail = next_stock_reco_detail[0]
1306 # add condition to update SLEs before this date & time
1307 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301308
Ankush Menat494bd9e2022-03-28 18:52:46 +05301309 frappe.db.sql(
1310 """
Nabin Hait186a0452021-02-18 14:14:21 +05301311 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301312 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301313 where
1314 item_code = %(item_code)s
1315 and warehouse = %(warehouse)s
1316 and voucher_no != %(voucher_no)s
1317 and is_cancelled = 0
1318 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1319 or (
1320 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1321 and creation > %(creation)s
1322 )
1323 )
marination40389772021-07-02 17:13:45 +05301324 {datetime_limit_condition}
Ankush Menat494bd9e2022-03-28 18:52:46 +05301325 """.format(
1326 qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
1327 ),
1328 args,
1329 )
Nabin Hait186a0452021-02-18 14:14:21 +05301330
1331 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1332
Ankush Menat494bd9e2022-03-28 18:52:46 +05301333
marination40389772021-07-02 17:13:45 +05301334def get_stock_reco_qty_shift(args):
1335 stock_reco_qty_shift = 0
1336 if args.get("is_cancelled"):
1337 if args.get("previous_qty_after_transaction"):
1338 # get qty (balance) that was set at submission
1339 last_balance = args.get("previous_qty_after_transaction")
1340 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1341 else:
1342 stock_reco_qty_shift = flt(args.actual_qty)
1343 else:
1344 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301345 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1346 "qty_after_transaction"
1347 )
marination40389772021-07-02 17:13:45 +05301348
1349 if last_balance is not None:
1350 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1351 else:
1352 stock_reco_qty_shift = args.qty_after_transaction
1353
1354 return stock_reco_qty_shift
1355
Ankush Menat494bd9e2022-03-28 18:52:46 +05301356
marination40389772021-07-02 17:13:45 +05301357def get_next_stock_reco(args):
1358 """Returns next nearest stock reconciliaton's details."""
1359
Ankush Menat494bd9e2022-03-28 18:52:46 +05301360 return frappe.db.sql(
1361 """
marination40389772021-07-02 17:13:45 +05301362 select
1363 name, posting_date, posting_time, creation, voucher_no
1364 from
marination8c441262021-07-02 17:46:05 +05301365 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301366 where
1367 item_code = %(item_code)s
1368 and warehouse = %(warehouse)s
1369 and voucher_type = 'Stock Reconciliation'
1370 and voucher_no != %(voucher_no)s
1371 and is_cancelled = 0
1372 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1373 or (
1374 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1375 and creation > %(creation)s
1376 )
1377 )
1378 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301379 """,
1380 args,
1381 as_dict=1,
1382 )
1383
marination40389772021-07-02 17:13:45 +05301384
1385def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301386 return f"""
1387 and
1388 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1389 or (
1390 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1391 and creation < '{detail.creation}'
1392 )
1393 )"""
1394
Ankush Menat494bd9e2022-03-28 18:52:46 +05301395
Ankush Menate7109c12021-08-26 16:40:45 +05301396def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301397 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301398 return
1399 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1400 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301401
Ankush Menat5eba5752021-12-07 23:03:52 +05301402 neg_sle = get_future_sle_with_negative_qty(args)
1403 if 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)
1421 if neg_batch_sle:
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
1435def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301436 return frappe.db.sql(
1437 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301438 select
1439 qty_after_transaction, posting_date, posting_time,
1440 voucher_type, voucher_no
1441 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301442 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301443 item_code = %(item_code)s
1444 and warehouse = %(warehouse)s
1445 and voucher_no != %(voucher_no)s
1446 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1447 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301448 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301449 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301450 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301451 """,
1452 args,
1453 as_dict=1,
1454 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301455
Ankush Menat5eba5752021-12-07 23:03:52 +05301456
1457def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301458 return frappe.db.sql(
1459 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301460 with batch_ledger as (
1461 select
1462 posting_date, posting_time, voucher_type, voucher_no,
1463 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1464 from `tabStock Ledger Entry`
1465 where
1466 item_code = %(item_code)s
1467 and warehouse = %(warehouse)s
1468 and batch_no=%(batch_no)s
1469 and is_cancelled = 0
1470 order by posting_date, posting_time, creation
1471 )
1472 select * from batch_ledger
1473 where
1474 cumulative_total < 0.0
1475 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1476 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301477 """,
1478 args,
1479 as_dict=1,
1480 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301481
1482
1483def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1484 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1485 return True
1486 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1487 return True
1488 return False