blob: a781479cf634be8086107483a85ef5de018a1250 [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 Menatecdb4932022-04-17 19:06:13 +05306from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
9from frappe import _
10from frappe.model.meta import get_field_precision
Ankush 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 Menata2819982022-04-08 13:20:25 +0530181 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530182 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530183 doc.cancel()
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)
Ankush Menatecdb4932022-04-17 19:06:13 +0530217 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530218
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530219 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530220 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530221 validate_item_warehouse(args[i])
222
Ankush Menat494bd9e2022-03-28 18:52:46 +0530223 obj = update_entries_after(
224 {
225 "item_code": args[i].get("item_code"),
226 "warehouse": args[i].get("warehouse"),
227 "posting_date": args[i].get("posting_date"),
228 "posting_time": args[i].get("posting_time"),
229 "creation": args[i].get("creation"),
230 "distinct_item_warehouses": distinct_item_warehouses,
231 },
232 allow_negative_stock=allow_negative_stock,
233 via_landed_cost_voucher=via_landed_cost_voucher,
234 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530235 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530236
Ankush Menat494bd9e2022-03-28 18:52:46 +0530237 distinct_item_warehouses[
238 (args[i].get("item_code"), args[i].get("warehouse"))
239 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530240
Nabin Hait97bce3a2021-07-12 13:24:43 +0530241 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530242 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530243 if ("args_idx" not in data and not data.reposting_status) or (
244 data.sle_changed and data.reposting_status
245 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530246 data.args_idx = len(args)
247 args.append(data.sle)
248 elif data.sle_changed and not data.reposting_status:
249 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530250
Nabin Hait97bce3a2021-07-12 13:24:43 +0530251 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530252 i += 1
253
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530254 if doc and i % 2 == 0:
Ankush Menatecdb4932022-04-17 19:06:13 +0530255 update_args_in_repost_item_valuation(
256 doc, i, args, distinct_item_warehouses, affected_transactions
257 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530258
259 if doc and args:
Ankush Menatecdb4932022-04-17 19:06:13 +0530260 update_args_in_repost_item_valuation(
261 doc, i, args, distinct_item_warehouses, affected_transactions
262 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530263
Ankush Menat494bd9e2022-03-28 18:52:46 +0530264
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530265def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530266 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530267 if not args.get(field):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530268 validation_msg = f"The field {frappe.unscrub(args.get(field))} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530269 frappe.throw(_(validation_msg))
270
Ankush Menat494bd9e2022-03-28 18:52:46 +0530271
Ankush Menatecdb4932022-04-17 19:06:13 +0530272def update_args_in_repost_item_valuation(
273 doc, index, args, distinct_item_warehouses, affected_transactions
274):
Ankush Menatecdb4932022-04-17 19:06:13 +0530275 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530276 {
277 "items_to_be_repost": json.dumps(args, default=str),
278 "distinct_item_and_warehouse": json.dumps(
279 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
280 ),
281 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530282 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530283 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530284 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530285
Ankush Menatecdb4932022-04-17 19:06:13 +0530286 if not frappe.flags.in_test:
287 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530288
Ankush Menat494bd9e2022-03-28 18:52:46 +0530289 frappe.publish_realtime(
290 "item_reposting_progress",
291 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
292 )
293
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530294
295def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
296 if doc and doc.items_to_be_repost:
297 return json.loads(doc.items_to_be_repost) or []
298
Ankush Menat494bd9e2022-03-28 18:52:46 +0530299 return frappe.db.get_all(
300 "Stock Ledger Entry",
Nabin Haita77b8c92020-12-21 14:45:50 +0530301 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
Nabin Hait186a0452021-02-18 14:14:21 +0530302 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
Nabin Haita77b8c92020-12-21 14:45:50 +0530303 order_by="creation asc",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530304 group_by="item_code, warehouse",
Nabin Haita77b8c92020-12-21 14:45:50 +0530305 )
Nabin Hait74c281c2013-08-19 16:17:18 +0530306
Ankush Menat494bd9e2022-03-28 18:52:46 +0530307
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530308def get_distinct_item_warehouse(args=None, doc=None):
309 distinct_item_warehouses = {}
310 if doc and doc.distinct_item_and_warehouse:
311 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530312 distinct_item_warehouses = {
313 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
314 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530315 else:
316 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530317 distinct_item_warehouses.setdefault(
318 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
319 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530320
321 return distinct_item_warehouses
322
Ankush Menat494bd9e2022-03-28 18:52:46 +0530323
Ankush Menatecdb4932022-04-17 19:06:13 +0530324def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
325 if not doc.affected_transactions:
326 return set()
327
328 transactions = frappe.parse_json(doc.affected_transactions)
329 return {tuple(transaction) for transaction in transactions}
330
331
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530332def get_current_index(doc=None):
333 if doc and doc.current_index:
334 return doc.current_index
335
Ankush Menat494bd9e2022-03-28 18:52:46 +0530336
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530337class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530338 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530339 update valution rate and qty after transaction
340 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530341
Ankush Menat494bd9e2022-03-28 18:52:46 +0530342 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530343
Ankush Menat494bd9e2022-03-28 18:52:46 +0530344 args = {
345 "item_code": "ABC",
346 "warehouse": "XYZ",
347 "posting_date": "2012-12-12",
348 "posting_time": "12:00"
349 }
Nabin Hait902e8602013-01-08 18:29:24 +0530350 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530351
352 def __init__(
353 self,
354 args,
355 allow_zero_rate=False,
356 allow_negative_stock=None,
357 via_landed_cost_voucher=False,
358 verbose=1,
359 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530360 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530361 self.verbose = verbose
362 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530363 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530364 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530365 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
366 item_code=self.item_code
367 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530368
Nabin Haita77b8c92020-12-21 14:45:50 +0530369 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530370 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530371 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530372
Nabin Haita77b8c92020-12-21 14:45:50 +0530373 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
374 self.get_precision()
375 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530376
377 self.new_items_found = False
378 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530379 self.affected_transactions: Set[Tuple[str, str]] = set()
Nabin Haita77b8c92020-12-21 14:45:50 +0530380
381 self.data = frappe._dict()
382 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530383 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530384
Nabin Haita77b8c92020-12-21 14:45:50 +0530385 def get_precision(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530386 company_base_currency = frappe.get_cached_value("Company", self.company, "default_currency")
387 self.precision = get_field_precision(
388 frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), currency=company_base_currency
389 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530390
391 def initialize_previous_data(self, args):
392 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530393 Get previous sl entries for current item for each related warehouse
394 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530395
Ankush Menat494bd9e2022-03-28 18:52:46 +0530396 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530397
Ankush Menat494bd9e2022-03-28 18:52:46 +0530398 self.data = {
399 warehouse1: {
400 'previus_sle': {},
401 'qty_after_transaction': 10,
402 'valuation_rate': 100,
403 'stock_value': 1000,
404 'prev_stock_value': 1000,
405 'stock_queue': '[[10, 100]]',
406 'stock_value_difference': 1000
407 }
408 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530409
410 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530411 self.data.setdefault(args.warehouse, frappe._dict())
412 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530413 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530414 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530415
Ankush Menatc1d986a2021-08-31 19:43:42 +0530416 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
417 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
418
Ankush Menat494bd9e2022-03-28 18:52:46 +0530419 warehouse_dict.update(
420 {
421 "prev_stock_value": previous_sle.stock_value or 0.0,
422 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
423 "stock_value_difference": 0.0,
424 }
425 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530426
Nabin Haita77b8c92020-12-21 14:45:50 +0530427 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530428 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530429
Nabin Haita77b8c92020-12-21 14:45:50 +0530430 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530431 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530432 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530433 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530434 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530435 entries_to_fix = self.get_future_entries_to_fix()
436
437 i = 0
438 while i < len(entries_to_fix):
439 sle = entries_to_fix[i]
440 i += 1
441
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530442 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530443
Nabin Haita77b8c92020-12-21 14:45:50 +0530444 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530445 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530446
Nabin Hait186a0452021-02-18 14:14:21 +0530447 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530448
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530449 if self.exceptions:
450 self.raise_exceptions()
451
Nabin Hait186a0452021-02-18 14:14:21 +0530452 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530453 sl_entries = self.get_sle_against_current_voucher()
454 for sle in sl_entries:
455 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530456
Nabin Haita77b8c92020-12-21 14:45:50 +0530457 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530458 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530459
Ankush Menat494bd9e2022-03-28 18:52:46 +0530460 return frappe.db.sql(
461 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530462 select
463 *, timestamp(posting_date, posting_time) as "timestamp"
464 from
465 `tabStock Ledger Entry`
466 where
467 item_code = %(item_code)s
468 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530469 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530470 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
471
Nabin Haita77b8c92020-12-21 14:45:50 +0530472 order by
473 creation ASC
474 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530475 """,
476 self.args,
477 as_dict=1,
478 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530479
Nabin Haita77b8c92020-12-21 14:45:50 +0530480 def get_future_entries_to_fix(self):
481 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530482 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
483 {"item_code": self.item_code, "warehouse": self.args.warehouse}
484 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530485
Nabin Haita77b8c92020-12-21 14:45:50 +0530486 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530487
Nabin Haita77b8c92020-12-21 14:45:50 +0530488 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530489 dependant_sle = get_sle_by_voucher_detail_no(
490 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
491 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530492
Nabin Haita77b8c92020-12-21 14:45:50 +0530493 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530494 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530495 elif (
496 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
497 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530498 return entries_to_fix
499 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530500 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530501 return entries_to_fix
502 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
503 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530504 else:
505 return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
506
507 def update_distinct_item_warehouses(self, dependant_sle):
508 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530509 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530510 if key not in self.distinct_item_warehouses:
511 self.distinct_item_warehouses[key] = val
512 self.new_items_found = True
513 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530514 existing_sle_posting_date = (
515 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
516 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530517 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
518 val.sle_changed = True
519 self.distinct_item_warehouses[key] = val
520 self.new_items_found = True
521
522 def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
Nabin Haita77b8c92020-12-21 14:45:50 +0530523 self.initialize_previous_data(dependant_sle)
524
Ankush Menat494bd9e2022-03-28 18:52:46 +0530525 args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict(
526 {"item_code": self.item_code, "warehouse": dependant_sle.warehouse}
527 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530528 future_sle_for_dependant = list(self.get_sle_after_datetime(args))
529
530 entries_to_fix.extend(future_sle_for_dependant)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530531 return sorted(entries_to_fix, key=lambda k: k["timestamp"])
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530532
533 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530534 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
535
Nabin Haita77b8c92020-12-21 14:45:50 +0530536 # previous sle data for this warehouse
537 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530538 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530539
Anand Doshi0dc79f42015-04-06 12:59:34 +0530540 if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530541 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530542 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530543 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530544 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530545 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530546
Nabin Haita77b8c92020-12-21 14:45:50 +0530547 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530548 if not self.args.get("sle_id"):
549 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530550
Ankush Menat66bf21f2022-01-16 20:45:59 +0530551 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530552 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530553 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530554 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530555 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530556
Ankush Menat494bd9e2022-03-28 18:52:46 +0530557 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
558 self.wh_data.valuation_rate
559 )
560 elif sle.batch_no and frappe.db.get_value(
561 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
562 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530563 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530564 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530565 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530566 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530567 self.wh_data.valuation_rate = sle.valuation_rate
568 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530569 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
570 self.wh_data.valuation_rate
571 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530572 if self.valuation_method != "Moving Average":
573 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530574 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530575 if self.valuation_method == "Moving Average":
576 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530577 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530578 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
579 self.wh_data.valuation_rate
580 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530581 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530582 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530583
Rushabh Mehta54047782013-12-26 11:07:46 +0530584 # rounding as per precision
Nabin Haita77b8c92020-12-21 14:45:50 +0530585 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530586 if not self.wh_data.qty_after_transaction:
587 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530588 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
589 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530590
Nabin Hait902e8602013-01-08 18:29:24 +0530591 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530592 sle.qty_after_transaction = self.wh_data.qty_after_transaction
593 sle.valuation_rate = self.wh_data.valuation_rate
594 sle.stock_value = self.wh_data.stock_value
595 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530596 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530597 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530598 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530599
Ankush Menat701878f2022-03-01 18:08:29 +0530600 if not self.args.get("sle_id"):
601 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530602
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530603 def validate_negative_stock(self, sle):
604 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530605 validate negative stock for entries current datetime onwards
606 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530607 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530608 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530609
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530610 if diff < 0 and abs(diff) > 0.0001:
611 # negative stock!
612 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530613 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530614 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530615 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530616 return True
617
Nabin Haita77b8c92020-12-21 14:45:50 +0530618 def get_dynamic_incoming_outgoing_rate(self, sle):
619 # Get updated incoming/outgoing rate from transaction
620 if sle.recalculate_rate:
621 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
622
623 if flt(sle.actual_qty) >= 0:
624 sle.incoming_rate = rate
625 else:
626 sle.outgoing_rate = rate
627
628 def get_incoming_outgoing_rate_from_transaction(self, sle):
629 rate = 0
630 # Material Transfer, Repack, Manufacturing
631 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530632 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530633 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
634 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530635 elif sle.voucher_type in (
636 "Purchase Receipt",
637 "Purchase Invoice",
638 "Delivery Note",
639 "Sales Invoice",
640 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530641 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530642 from erpnext.controllers.sales_and_purchase_return import (
643 get_rate_for_return, # don't move this import to top
644 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530645
646 rate = get_rate_for_return(
647 sle.voucher_type,
648 sle.voucher_no,
649 sle.item_code,
650 voucher_detail_no=sle.voucher_detail_no,
651 sle=sle,
652 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530653 else:
654 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530655 rate_field = "valuation_rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530656 else:
657 rate_field = "incoming_rate"
658
659 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530660 item_code, incoming_rate = frappe.db.get_value(
661 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
662 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530663
664 if item_code == sle.item_code:
665 rate = incoming_rate
666 else:
667 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
668 ref_doctype = "Packed Item"
669 else:
670 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530671
Ankush Menat494bd9e2022-03-28 18:52:46 +0530672 rate = frappe.db.get_value(
673 ref_doctype,
674 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
675 rate_field,
676 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530677
678 return rate
679
680 def update_outgoing_rate_on_transaction(self, sle):
681 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530682 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
683 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530684 """
685 if sle.actual_qty and sle.voucher_detail_no:
686 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
687
688 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
689 self.update_rate_on_stock_entry(sle, outgoing_rate)
690 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
691 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
692 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
693 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
694
695 def update_rate_on_stock_entry(self, sle, outgoing_rate):
696 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
697
Ankush Menat701878f2022-03-01 18:08:29 +0530698 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
699 if not sle.dependant_sle_voucher_detail_no:
700 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530701
702 def recalculate_amounts_in_stock_entry(self, voucher_no):
703 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530704 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
705 stock_entry.db_update()
706 for d in stock_entry.items:
707 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530708
Nabin Haita77b8c92020-12-21 14:45:50 +0530709 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
710 # Update item's incoming rate on transaction
711 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
712 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530713 frappe.db.set_value(
714 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
715 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530716 else:
717 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530718 frappe.db.set_value(
719 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530720 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530721 "incoming_rate",
722 outgoing_rate,
723 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530724
725 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
726 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530727 frappe.db.set_value(
728 sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
729 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530730 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530731 frappe.db.set_value(
732 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
733 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530734
735 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530736 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530737 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530738 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530739 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530740 d.db_update()
741
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530742 def get_serialized_values(self, sle):
743 incoming_rate = flt(sle.incoming_rate)
744 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530745 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530746
747 if incoming_rate < 0:
748 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530749 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530750
Nabin Hait2620bf42016-02-29 11:30:27 +0530751 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530752 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530753 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530754 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530755 # In case of delivery/stock issue, get average purchase rate
756 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530757 if not sle.is_cancelled:
758 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
759 stock_value_change = -1 * outgoing_value
760 else:
761 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530762
Nabin Haita77b8c92020-12-21 14:45:50 +0530763 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530764
Nabin Hait2620bf42016-02-29 11:30:27 +0530765 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530766 new_stock_value = (
767 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
768 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530769 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530770 # calculate new valuation rate only if stock value is positive
771 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530772 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530773
Nabin Haita77b8c92020-12-21 14:45:50 +0530774 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530775 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
776 sle.voucher_type, sle.voucher_detail_no
777 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530778 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530779 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530780
Nabin Hait328c4f92020-01-02 19:00:32 +0530781 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
782 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530783 all_serial_nos = frappe.get_all(
784 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
785 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530786
Ankush Menat494bd9e2022-03-28 18:52:46 +0530787 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 +0530788
789 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530790 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530791 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530792 incoming_rate = frappe.db.sql(
793 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530794 select incoming_rate
795 from `tabStock Ledger Entry`
796 where
797 company = %s
798 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530799 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530800 and (serial_no = %s
801 or serial_no like %s
802 or serial_no like %s
803 or serial_no like %s
804 )
805 order by posting_date desc
806 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530807 """,
808 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
809 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530810
811 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
812
813 return incoming_values
814
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530815 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530816 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530817 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530818 if new_stock_qty >= 0:
819 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530820 if flt(self.wh_data.qty_after_transaction) <= 0:
821 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530822 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530823 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
824 actual_qty * sle.incoming_rate
825 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530826
Nabin Haita77b8c92020-12-21 14:45:50 +0530827 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530828
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530829 elif sle.outgoing_rate:
830 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530831 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
832 actual_qty * sle.outgoing_rate
833 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530834
Nabin Haita77b8c92020-12-21 14:45:50 +0530835 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530836 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530837 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530838 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530839 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
840 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530841
Nabin Haita77b8c92020-12-21 14:45:50 +0530842 if not self.wh_data.valuation_rate and actual_qty > 0:
843 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530844
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530845 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800846 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530847 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530848 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
849 sle.voucher_type, sle.voucher_detail_no
850 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800851 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530852 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530853
Ankush Menatf089d392022-02-02 12:51:21 +0530854 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530855 incoming_rate = flt(sle.incoming_rate)
856 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530857 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530858
Ankush Menat494bd9e2022-03-28 18:52:46 +0530859 self.wh_data.qty_after_transaction = round_off_if_near_zero(
860 self.wh_data.qty_after_transaction + actual_qty
861 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530862
Ankush Menat97e18a12022-01-15 17:42:25 +0530863 if self.valuation_method == "LIFO":
864 stock_queue = LIFOValuation(self.wh_data.stock_queue)
865 else:
866 stock_queue = FIFOValuation(self.wh_data.stock_queue)
867
Ankush Menatb534fee2022-02-19 20:58:36 +0530868 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
869
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530870 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530871 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530872 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530873
Ankush Menat4b29fb62021-12-18 18:40:22 +0530874 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530875 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
876 sle.voucher_type, sle.voucher_detail_no
877 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530878 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530879 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530880 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530881 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530882
Ankush Menat494bd9e2022-03-28 18:52:46 +0530883 stock_queue.remove_stock(
884 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
885 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530886
Ankush Menatb534fee2022-02-19 20:58:36 +0530887 _qty, stock_value = stock_queue.get_total_stock_and_value()
888
889 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530890
Ankush Menat97e18a12022-01-15 17:42:25 +0530891 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530892 self.wh_data.stock_value = round_off_if_near_zero(
893 self.wh_data.stock_value + stock_value_difference
894 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530895
Nabin Haita77b8c92020-12-21 14:45:50 +0530896 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530897 self.wh_data.stock_queue.append(
898 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
899 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530900
Ankush Menatb534fee2022-02-19 20:58:36 +0530901 if self.wh_data.qty_after_transaction:
902 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
903
Ankush Menatce0514c2022-02-15 11:41:41 +0530904 def update_batched_values(self, sle):
905 incoming_rate = flt(sle.incoming_rate)
906 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530907
Ankush Menat494bd9e2022-03-28 18:52:46 +0530908 self.wh_data.qty_after_transaction = round_off_if_near_zero(
909 self.wh_data.qty_after_transaction + actual_qty
910 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530911
912 if actual_qty > 0:
913 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530914 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530915 outgoing_rate = get_batch_incoming_rate(
916 item_code=sle.item_code,
917 warehouse=sle.warehouse,
918 batch_no=sle.batch_no,
919 posting_date=sle.posting_date,
920 posting_time=sle.posting_time,
921 creation=sle.creation,
922 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530923 if outgoing_rate is None:
924 # This can *only* happen if qty available for the batch is zero.
925 # in such case fall back various other rates.
926 # future entries will correct the overall accounting as each
927 # batch individually uses moving average rates.
928 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530929 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530930
Ankush Menat494bd9e2022-03-28 18:52:46 +0530931 self.wh_data.stock_value = round_off_if_near_zero(
932 self.wh_data.stock_value + stock_value_difference
933 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530934 if self.wh_data.qty_after_transaction:
935 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530936
Javier Wong9b11d9b2017-04-14 18:24:04 +0800937 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530938 ref_item_dt = ""
939
940 if voucher_type == "Stock Entry":
941 ref_item_dt = voucher_type + " Detail"
942 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
943 ref_item_dt = voucher_type + " Item"
944
945 if ref_item_dt:
946 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
947 else:
948 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530949
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530950 def get_fallback_rate(self, sle) -> float:
951 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +0530952 This should only get used for negative stock."""
953 return get_valuation_rate(
954 sle.item_code,
955 sle.warehouse,
956 sle.voucher_type,
957 sle.voucher_no,
958 self.allow_zero_rate,
959 currency=erpnext.get_company_currency(sle.company),
960 company=sle.company,
961 batch_no=sle.batch_no,
962 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530963
Nabin Haita77b8c92020-12-21 14:45:50 +0530964 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530965 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530966 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
967 sle = sle[0] if sle else frappe._dict()
968 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530969
Nabin Haita77b8c92020-12-21 14:45:50 +0530970 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530971 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530972 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530973
974 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530975 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530976 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530977 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530978
Ankush Menat494bd9e2022-03-28 18:52:46 +0530979 if (
980 exceptions[0]["voucher_type"],
981 exceptions[0]["voucher_no"],
982 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +0530983
Nabin Haita77b8c92020-12-21 14:45:50 +0530984 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530985 abs(deficiency),
986 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
987 frappe.get_desk_link("Warehouse", warehouse),
988 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530989 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530990 msg = _(
991 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
992 ).format(
993 abs(deficiency),
994 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
995 frappe.get_desk_link("Warehouse", warehouse),
996 exceptions[0]["posting_date"],
997 exceptions[0]["posting_time"],
998 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
999 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301000
Nabin Haita77b8c92020-12-21 14:45:50 +05301001 if msg:
1002 msg_list.append(msg)
1003
1004 if msg_list:
1005 message = "\n\n".join(msg_list)
1006 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301007 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301008 else:
1009 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301010
Nabin Haita77b8c92020-12-21 14:45:50 +05301011 def update_bin(self):
1012 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301013 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301014 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301015
Ankush Menat494bd9e2022-03-28 18:52:46 +05301016 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301017 if data.valuation_rate is not None:
1018 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat494bd9e2022-03-28 18:52:46 +05301019 frappe.db.set_value("Bin", bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301020
marination8418c4b2021-06-22 21:35:25 +05301021
1022def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
1023 """get stock ledger entries filtered by specific posting datetime conditions"""
1024
Ankush Menat494bd9e2022-03-28 18:52:46 +05301025 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301026 if not args.get("posting_date"):
1027 args["posting_date"] = "1900-01-01"
1028 if not args.get("posting_time"):
1029 args["posting_time"] = "00:00"
1030
1031 voucher_condition = ""
1032 if exclude_current_voucher:
1033 voucher_no = args.get("voucher_no")
1034 voucher_condition = f"and voucher_no != '{voucher_no}'"
1035
Ankush Menat494bd9e2022-03-28 18:52:46 +05301036 sle = frappe.db.sql(
1037 """
marination8418c4b2021-06-22 21:35:25 +05301038 select *, timestamp(posting_date, posting_time) as "timestamp"
1039 from `tabStock Ledger Entry`
1040 where item_code = %(item_code)s
1041 and warehouse = %(warehouse)s
1042 and is_cancelled = 0
1043 {voucher_condition}
1044 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
1045 order by timestamp(posting_date, posting_time) desc, creation desc
1046 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301047 for update""".format(
1048 voucher_condition=voucher_condition
1049 ),
1050 args,
1051 as_dict=1,
1052 )
marination8418c4b2021-06-22 21:35:25 +05301053
1054 return sle[0] if sle else frappe._dict()
1055
Ankush Menat494bd9e2022-03-28 18:52:46 +05301056
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301057def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301058 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301059 get the last sle on or before the current time-bucket,
1060 to get actual qty before transaction, this function
1061 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301062
Ankush Menat494bd9e2022-03-28 18:52:46 +05301063 args = {
1064 "item_code": "ABC",
1065 "warehouse": "XYZ",
1066 "posting_date": "2012-12-12",
1067 "posting_time": "12:00",
1068 "sle": "name of reference Stock Ledger Entry"
1069 }
Anand Doshi1b531862013-01-10 19:29:51 +05301070 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301071 args["name"] = args.get("sle", None) or ""
1072 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301073 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301074
Ankush Menat494bd9e2022-03-28 18:52:46 +05301075
1076def get_stock_ledger_entries(
1077 previous_sle,
1078 operator=None,
1079 order="desc",
1080 limit=None,
1081 for_update=False,
1082 debug=False,
1083 check_serial_no=True,
1084):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301085 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301086 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1087 operator
1088 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301089 if previous_sle.get("warehouse"):
1090 conditions += " and warehouse = %(warehouse)s"
1091 elif previous_sle.get("warehouse_condition"):
1092 conditions += " and " + previous_sle.get("warehouse_condition")
1093
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301094 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301095 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1096 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301097 conditions += (
1098 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301099 (
1100 serial_no = {0}
1101 or serial_no like {1}
1102 or serial_no like {2}
1103 or serial_no like {3}
1104 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301105 """
1106 ).format(
1107 frappe.db.escape(serial_no),
1108 frappe.db.escape("{}\n%".format(serial_no)),
1109 frappe.db.escape("%\n{}".format(serial_no)),
1110 frappe.db.escape("%\n{}\n%".format(serial_no)),
1111 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301112
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301113 if not previous_sle.get("posting_date"):
1114 previous_sle["posting_date"] = "1900-01-01"
1115 if not previous_sle.get("posting_time"):
1116 previous_sle["posting_time"] = "00:00"
1117
1118 if operator in (">", "<=") and previous_sle.get("name"):
1119 conditions += " and name!=%(name)s"
1120
Ankush Menat494bd9e2022-03-28 18:52:46 +05301121 return frappe.db.sql(
1122 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301123 select *, timestamp(posting_date, posting_time) as "timestamp"
1124 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301125 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301126 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301127 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301128 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301129 %(limit)s %(for_update)s"""
1130 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301131 "conditions": conditions,
1132 "limit": limit or "",
1133 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301134 "order": order,
1135 },
1136 previous_sle,
1137 as_dict=1,
1138 debug=debug,
1139 )
1140
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301141
Nabin Haita77b8c92020-12-21 14:45:50 +05301142def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301143 return frappe.db.get_value(
1144 "Stock Ledger Entry",
1145 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle]},
1146 [
1147 "item_code",
1148 "warehouse",
1149 "posting_date",
1150 "posting_time",
1151 "timestamp(posting_date, posting_time) as timestamp",
1152 ],
1153 as_dict=1,
1154 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301155
Ankush Menatce0514c2022-02-15 11:41:41 +05301156
Ankush Menat494bd9e2022-03-28 18:52:46 +05301157def get_batch_incoming_rate(
1158 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1159):
1160
1161 Timestamp = CustomFunction("timestamp", ["date", "time"])
Ankush Menat102fff22022-02-19 15:51:04 +05301162
1163 sle = frappe.qb.DocType("Stock Ledger Entry")
1164
Ankush Menat494bd9e2022-03-28 18:52:46 +05301165 timestamp_condition = Timestamp(sle.posting_date, sle.posting_time) < Timestamp(
1166 posting_date, posting_time
1167 )
Ankush Menat102fff22022-02-19 15:51:04 +05301168 if creation:
1169 timestamp_condition |= (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301170 Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time)
1171 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301172
1173 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301174 frappe.qb.from_(sle)
1175 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1176 .where(
1177 (sle.item_code == item_code)
1178 & (sle.warehouse == warehouse)
1179 & (sle.batch_no == batch_no)
1180 & (sle.is_cancelled == 0)
1181 )
1182 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301183 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301184
1185 if batch_details and batch_details[0].batch_qty:
1186 return batch_details[0].batch_value / batch_details[0].batch_qty
1187
1188
Ankush Menat494bd9e2022-03-28 18:52:46 +05301189def get_valuation_rate(
1190 item_code,
1191 warehouse,
1192 voucher_type,
1193 voucher_no,
1194 allow_zero_rate=False,
1195 currency=None,
1196 company=None,
1197 raise_error_if_no_rate=True,
1198 batch_no=None,
1199):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301200
Ankush Menatf7ffe042021-11-01 13:21:14 +05301201 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301202 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301203
Ankush Menat342d09a2022-02-19 14:28:51 +05301204 last_valuation_rate = None
1205
1206 # Get moving average rate of a specific batch number
1207 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301208 last_valuation_rate = frappe.db.sql(
1209 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301210 select sum(stock_value_difference) / sum(actual_qty)
1211 from `tabStock Ledger Entry`
1212 where
1213 item_code = %s
1214 AND warehouse = %s
1215 AND batch_no = %s
1216 AND is_cancelled = 0
1217 AND NOT (voucher_no = %s AND voucher_type = %s)
1218 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301219 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1220 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301221
Ankush Menatf7ffe042021-11-01 13:21:14 +05301222 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301223 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301224 last_valuation_rate = frappe.db.sql(
1225 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301226 from `tabStock Ledger Entry` force index (item_warehouse)
1227 where
1228 item_code = %s
1229 AND warehouse = %s
1230 AND valuation_rate >= 0
1231 AND is_cancelled = 0
1232 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301233 order by posting_date desc, posting_time desc, name desc limit 1""",
1234 (item_code, warehouse, voucher_no, voucher_type),
1235 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301236
1237 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301238 # Get valuation rate from last sle for the item against any warehouse
Ankush Menat494bd9e2022-03-28 18:52:46 +05301239 last_valuation_rate = frappe.db.sql(
1240 """select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301241 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301242 where
1243 item_code = %s
1244 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301245 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301246 AND NOT(voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301247 order by posting_date desc, posting_time desc, name desc limit 1""",
1248 (item_code, voucher_no, voucher_type),
1249 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301250
Nabin Haita645f362018-03-01 10:31:24 +05301251 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301252 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301253
1254 # If negative stock allowed, and item delivered without any incoming entry,
1255 # system does not found any SLE, then take valuation rate from Item
1256 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301257
1258 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301259 # try Item Standard rate
1260 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301261
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301262 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301263 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301264 valuation_rate = frappe.db.get_value(
1265 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1266 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301267
Ankush Menat494bd9e2022-03-28 18:52:46 +05301268 if (
1269 not allow_zero_rate
1270 and not valuation_rate
1271 and raise_error_if_no_rate
1272 and cint(erpnext.is_perpetual_inventory_enabled(company))
1273 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301274 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301275
Ankush Menat494bd9e2022-03-28 18:52:46 +05301276 message = _(
1277 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1278 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301279 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301280 solutions = (
1281 "<li>"
1282 + _(
1283 "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."
1284 ).format(voucher_type)
1285 + "</li>"
1286 )
1287 solutions += (
1288 "<li>"
1289 + _("If not, you can Cancel / Submit this entry")
1290 + " {0} ".format(frappe.bold("after"))
1291 + _("performing either one below:")
1292 + "</li>"
1293 )
Marica97715f22020-05-11 20:45:37 +05301294 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1295 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1296 msg = message + solutions + sub_solutions + "</li>"
1297
1298 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301299
1300 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301301
Ankush Menat494bd9e2022-03-28 18:52:46 +05301302
Ankush Menate7109c12021-08-26 16:40:45 +05301303def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301304 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301305 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301306 qty_shift = args.actual_qty
1307
1308 # find difference/shift in qty caused by stock reconciliation
1309 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301310 qty_shift = get_stock_reco_qty_shift(args)
1311
1312 # find the next nearest stock reco so that we only recalculate SLEs till that point
1313 next_stock_reco_detail = get_next_stock_reco(args)
1314 if next_stock_reco_detail:
1315 detail = next_stock_reco_detail[0]
1316 # add condition to update SLEs before this date & time
1317 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301318
Ankush Menat494bd9e2022-03-28 18:52:46 +05301319 frappe.db.sql(
1320 """
Nabin Hait186a0452021-02-18 14:14:21 +05301321 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301322 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301323 where
1324 item_code = %(item_code)s
1325 and warehouse = %(warehouse)s
1326 and voucher_no != %(voucher_no)s
1327 and is_cancelled = 0
1328 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1329 or (
1330 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1331 and creation > %(creation)s
1332 )
1333 )
marination40389772021-07-02 17:13:45 +05301334 {datetime_limit_condition}
Ankush Menat494bd9e2022-03-28 18:52:46 +05301335 """.format(
1336 qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition
1337 ),
1338 args,
1339 )
Nabin Hait186a0452021-02-18 14:14:21 +05301340
1341 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1342
Ankush Menat494bd9e2022-03-28 18:52:46 +05301343
marination40389772021-07-02 17:13:45 +05301344def get_stock_reco_qty_shift(args):
1345 stock_reco_qty_shift = 0
1346 if args.get("is_cancelled"):
1347 if args.get("previous_qty_after_transaction"):
1348 # get qty (balance) that was set at submission
1349 last_balance = args.get("previous_qty_after_transaction")
1350 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1351 else:
1352 stock_reco_qty_shift = flt(args.actual_qty)
1353 else:
1354 # reco is being submitted
Ankush Menat494bd9e2022-03-28 18:52:46 +05301355 last_balance = get_previous_sle_of_current_voucher(args, exclude_current_voucher=True).get(
1356 "qty_after_transaction"
1357 )
marination40389772021-07-02 17:13:45 +05301358
1359 if last_balance is not None:
1360 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1361 else:
1362 stock_reco_qty_shift = args.qty_after_transaction
1363
1364 return stock_reco_qty_shift
1365
Ankush Menat494bd9e2022-03-28 18:52:46 +05301366
marination40389772021-07-02 17:13:45 +05301367def get_next_stock_reco(args):
1368 """Returns next nearest stock reconciliaton's details."""
1369
Ankush Menat494bd9e2022-03-28 18:52:46 +05301370 return frappe.db.sql(
1371 """
marination40389772021-07-02 17:13:45 +05301372 select
1373 name, posting_date, posting_time, creation, voucher_no
1374 from
marination8c441262021-07-02 17:46:05 +05301375 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301376 where
1377 item_code = %(item_code)s
1378 and warehouse = %(warehouse)s
1379 and voucher_type = 'Stock Reconciliation'
1380 and voucher_no != %(voucher_no)s
1381 and is_cancelled = 0
1382 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1383 or (
1384 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1385 and creation > %(creation)s
1386 )
1387 )
1388 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301389 """,
1390 args,
1391 as_dict=1,
1392 )
1393
marination40389772021-07-02 17:13:45 +05301394
1395def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301396 return f"""
1397 and
1398 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1399 or (
1400 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1401 and creation < '{detail.creation}'
1402 )
1403 )"""
1404
Ankush Menat494bd9e2022-03-28 18:52:46 +05301405
Ankush Menate7109c12021-08-26 16:40:45 +05301406def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301407 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301408 return
1409 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1410 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301411
Ankush Menat5eba5752021-12-07 23:03:52 +05301412 neg_sle = get_future_sle_with_negative_qty(args)
1413 if neg_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_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301418 frappe.get_desk_link("Item", args.item_code),
1419 frappe.get_desk_link("Warehouse", args.warehouse),
1420 neg_sle[0]["posting_date"],
1421 neg_sle[0]["posting_time"],
1422 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1423 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301424
Ankush Menat494bd9e2022-03-28 18:52:46 +05301425 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301426
1427 if not args.batch_no:
1428 return
1429
1430 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1431 if neg_batch_sle:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301432 message = _(
1433 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1434 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301435 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301436 frappe.get_desk_link("Batch", args.batch_no),
1437 frappe.get_desk_link("Warehouse", args.warehouse),
1438 neg_batch_sle[0]["posting_date"],
1439 neg_batch_sle[0]["posting_time"],
1440 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1441 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301442 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301443
Nabin Haita77b8c92020-12-21 14:45:50 +05301444
1445def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301446 return frappe.db.sql(
1447 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301448 select
1449 qty_after_transaction, posting_date, posting_time,
1450 voucher_type, voucher_no
1451 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301452 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301453 item_code = %(item_code)s
1454 and warehouse = %(warehouse)s
1455 and voucher_no != %(voucher_no)s
1456 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1457 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301458 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301459 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301460 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301461 """,
1462 args,
1463 as_dict=1,
1464 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301465
Ankush Menat5eba5752021-12-07 23:03:52 +05301466
1467def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301468 return frappe.db.sql(
1469 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301470 with batch_ledger as (
1471 select
1472 posting_date, posting_time, voucher_type, voucher_no,
1473 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1474 from `tabStock Ledger Entry`
1475 where
1476 item_code = %(item_code)s
1477 and warehouse = %(warehouse)s
1478 and batch_no=%(batch_no)s
1479 and is_cancelled = 0
1480 order by posting_date, posting_time, creation
1481 )
1482 select * from batch_ledger
1483 where
1484 cumulative_total < 0.0
1485 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1486 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301487 """,
1488 args,
1489 as_dict=1,
1490 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301491
1492
1493def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1494 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1495 return True
1496 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1497 return True
1498 return False