blob: 6106809273f81842bf800a41a799f89530dfe007 [file] [log] [blame]
Maricad6078aa2022-06-17 15:13:13 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait902e8602013-01-08 18:29:24 +05303
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05304import copy
Nabin Hait26d46552013-01-09 15:23:05 +05305import json
Ankush Menatecdb4932022-04-17 19:06:13 +05306from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
9from frappe import _
10from frappe.model.meta import get_field_precision
Ankush Menate1c16872022-04-21 20:01:48 +053011from frappe.query_builder.functions import CombineDatetime, Sum
Ankush Menatcef84c22021-12-03 12:18:59 +053012from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
Achilles Rasquinha361366e2018-02-14 17:08:59 +053013
Chillar Anand915b3432021-09-02 16:44:59 +053014import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053015from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
Chillar Anand915b3432021-09-02 16:44:59 +053016from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053017 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053018 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053019 get_valuation_method,
20)
Ankush Menatb534fee2022-02-19 20:58:36 +053021from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053022
Nabin Hait97bce3a2021-07-12 13:24:43 +053023
Ankush Menat494bd9e2022-03-28 18:52:46 +053024class NegativeStockError(frappe.ValidationError):
25 pass
26
27
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053028class SerialNoExistsInFutureTransaction(frappe.ValidationError):
29 pass
Nabin Hait902e8602013-01-08 18:29:24 +053030
Anand Doshi5b004ff2013-09-25 19:55:41 +053031
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053032def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053033 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053034
Ankush Menat494bd9e2022-03-28 18:52:46 +053035 args:
36 - allow_negative_stock: disable negative stock valiations if true
37 - via_landed_cost_voucher: landed cost voucher cancels and reposts
38 entries of purchase document. This flag is used to identify if
39 cancellation and repost is happening via landed cost voucher, in
40 such cases certain validations need to be ignored (like negative
41 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053042 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053043 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053044
Nabin Haitca775742013-09-26 16:16:44 +053045 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053046 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053047 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053048 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053049 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053050
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053051 args = get_args_for_future_sle(sl_entries[0])
52 future_sle_exists(args, sl_entries)
53
Nabin Haitca775742013-09-26 16:16:44 +053054 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053055 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053056 validate_serial_no(sle)
57
Nabin Haita77b8c92020-12-21 14:45:50 +053058 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053059 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053060
Ankush Menat494bd9e2022-03-28 18:52:46 +053061 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
62 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
63 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
64 )
65 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053066
Ankush Menat494bd9e2022-03-28 18:52:46 +053067 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
68 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
69 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
70 )
71 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053072
Ankush Menat494bd9e2022-03-28 18:52:46 +053073 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053074 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053075
Nabin Haita77b8c92020-12-21 14:45:50 +053076 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053077
78 if sle.get("voucher_type") == "Stock Reconciliation":
79 # preserve previous_qty_after_transaction for qty reposting
80 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
81
Ankush Menat494bd9e2022-03-28 18:52:46 +053082 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053083 if is_stock_item:
84 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053085 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053086 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053087 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +053088 frappe.msgprint(
89 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
90 )
91
Ankush Menatcef84c22021-12-03 12:18:59 +053092
93def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
94 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
95 if not args.get("posting_date"):
96 args["posting_date"] = nowdate()
97
marination7a5fd712022-07-04 17:46:54 +053098 if not (args.get("is_cancelled") and via_landed_cost_voucher):
99 # Reposts only current voucher SL Entries
100 # Updates valuation rate, stock value, stock queue for current transaction
101 update_entries_after(
102 {
103 "item_code": args.get("item_code"),
104 "warehouse": args.get("warehouse"),
105 "posting_date": args.get("posting_date"),
106 "posting_time": args.get("posting_time"),
107 "voucher_type": args.get("voucher_type"),
108 "voucher_no": args.get("voucher_no"),
109 "sle_id": args.get("name"),
110 "creation": args.get("creation"),
111 },
112 allow_negative_stock=allow_negative_stock,
113 via_landed_cost_voucher=via_landed_cost_voucher,
114 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530115
116 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530117 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530118 update_qty_in_future_sle(args, allow_negative_stock)
119
Nabin Haitadeb9762014-10-06 11:53:52 +0530120
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530121def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530122 return frappe._dict(
123 {
124 "voucher_type": row.get("voucher_type"),
125 "voucher_no": row.get("voucher_no"),
126 "posting_date": row.get("posting_date"),
127 "posting_time": row.get("posting_time"),
128 }
129 )
130
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530131
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530132def validate_serial_no(sle):
133 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530134
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530135 for sn in get_serial_nos(sle.serial_no):
136 args = copy.deepcopy(sle)
137 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530138 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530139
140 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530141 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530142 voucher_type = frappe.bold(row.voucher_type)
143 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530144 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530145
146 if vouchers:
147 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530148 msg = (
149 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
150 The list of the transactions are as below."""
151 + "<br><br><ul><li>"
152 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530153
Ankush Menat494bd9e2022-03-28 18:52:46 +0530154 msg += "</li><li>".join(vouchers)
155 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530156
Ankush Menat494bd9e2022-03-28 18:52:46 +0530157 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530158 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
159
Ankush Menat494bd9e2022-03-28 18:52:46 +0530160
Nabin Hait186a0452021-02-18 14:14:21 +0530161def validate_cancellation(args):
162 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163 repost_entry = frappe.db.get_value(
164 "Repost Item Valuation",
165 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
166 ["name", "status"],
167 as_dict=1,
168 )
Nabin Hait186a0452021-02-18 14:14:21 +0530169
170 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530171 if repost_entry.status == "In Progress":
172 frappe.throw(
173 _(
174 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
175 )
176 )
177 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530178 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530179 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530180 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530181 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530182
Ankush Menat494bd9e2022-03-28 18:52:46 +0530183
Nabin Hait9653f602013-08-20 15:37:33 +0530184def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530185 frappe.db.sql(
186 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530187 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530188 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530189 (now(), frappe.session.user, voucher_type, voucher_no),
190 )
191
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530192
Nabin Hait54c865e2015-03-27 15:38:31 +0530193def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530194 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530195 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530196 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530197 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530198 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530199 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530200 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530201
Ankush Menat494bd9e2022-03-28 18:52:46 +0530202
203def repost_future_sle(
204 args=None,
205 voucher_type=None,
206 voucher_no=None,
207 allow_negative_stock=None,
208 via_landed_cost_voucher=False,
209 doc=None,
210):
Nabin Haite1fa7232022-07-20 15:19:09 +0530211 if not args:
212 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530213
214 items_to_be_repost = get_items_to_be_repost(
215 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
216 )
217 if items_to_be_repost:
218 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530219
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530220 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Ankush Menatecdb4932022-04-17 19:06:13 +0530221 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530222
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530223 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530224 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530225 validate_item_warehouse(args[i])
226
Ankush Menat494bd9e2022-03-28 18:52:46 +0530227 obj = update_entries_after(
228 {
229 "item_code": args[i].get("item_code"),
230 "warehouse": args[i].get("warehouse"),
231 "posting_date": args[i].get("posting_date"),
232 "posting_time": args[i].get("posting_time"),
233 "creation": args[i].get("creation"),
234 "distinct_item_warehouses": distinct_item_warehouses,
235 },
236 allow_negative_stock=allow_negative_stock,
237 via_landed_cost_voucher=via_landed_cost_voucher,
238 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530239 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530240
Ankush Menat494bd9e2022-03-28 18:52:46 +0530241 distinct_item_warehouses[
242 (args[i].get("item_code"), args[i].get("warehouse"))
243 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530244
Nabin Hait97bce3a2021-07-12 13:24:43 +0530245 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530246 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530247 if ("args_idx" not in data and not data.reposting_status) or (
248 data.sle_changed and data.reposting_status
249 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530250 data.args_idx = len(args)
251 args.append(data.sle)
252 elif data.sle_changed and not data.reposting_status:
253 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530254
Nabin Hait97bce3a2021-07-12 13:24:43 +0530255 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530256 i += 1
257
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530258 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530259 update_args_in_repost_item_valuation(
260 doc, i, args, distinct_item_warehouses, affected_transactions
261 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530262
Ankush Menat494bd9e2022-03-28 18:52:46 +0530263
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530264def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530265 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530266 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530267 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530268 frappe.throw(_(validation_msg))
269
Ankush Menat494bd9e2022-03-28 18:52:46 +0530270
Ankush Menatecdb4932022-04-17 19:06:13 +0530271def update_args_in_repost_item_valuation(
272 doc, index, args, distinct_item_warehouses, affected_transactions
273):
Ankush Menatecdb4932022-04-17 19:06:13 +0530274 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530275 {
276 "items_to_be_repost": json.dumps(args, default=str),
277 "distinct_item_and_warehouse": json.dumps(
278 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
279 ),
280 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530281 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530282 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530283 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530284
Ankush Menatecdb4932022-04-17 19:06:13 +0530285 if not frappe.flags.in_test:
286 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530287
Ankush Menat494bd9e2022-03-28 18:52:46 +0530288 frappe.publish_realtime(
289 "item_reposting_progress",
290 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
291 )
292
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530293
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530294def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
295 items_to_be_repost = []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530297 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530298
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530299 if not items_to_be_repost and voucher_type and voucher_no:
300 items_to_be_repost = frappe.db.get_all(
301 "Stock Ledger Entry",
302 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
303 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
304 order_by="creation asc",
305 group_by="item_code, warehouse",
306 )
307
Nabin Haite1fa7232022-07-20 15:19:09 +0530308 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530309
Ankush Menat494bd9e2022-03-28 18:52:46 +0530310
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530311def get_distinct_item_warehouse(args=None, doc=None):
312 distinct_item_warehouses = {}
313 if doc and doc.distinct_item_and_warehouse:
314 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530315 distinct_item_warehouses = {
316 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
317 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530318 else:
319 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530320 distinct_item_warehouses.setdefault(
321 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
322 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530323
324 return distinct_item_warehouses
325
Ankush Menat494bd9e2022-03-28 18:52:46 +0530326
Ankush Menatecdb4932022-04-17 19:06:13 +0530327def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
328 if not doc.affected_transactions:
329 return set()
330
331 transactions = frappe.parse_json(doc.affected_transactions)
332 return {tuple(transaction) for transaction in transactions}
333
334
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530335def get_current_index(doc=None):
336 if doc and doc.current_index:
337 return doc.current_index
338
Ankush Menat494bd9e2022-03-28 18:52:46 +0530339
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530340class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530341 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530342 update valution rate and qty after transaction
343 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530344
Ankush Menat494bd9e2022-03-28 18:52:46 +0530345 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530346
Ankush Menat494bd9e2022-03-28 18:52:46 +0530347 args = {
348 "item_code": "ABC",
349 "warehouse": "XYZ",
350 "posting_date": "2012-12-12",
351 "posting_time": "12:00"
352 }
Nabin Hait902e8602013-01-08 18:29:24 +0530353 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530354
355 def __init__(
356 self,
357 args,
358 allow_zero_rate=False,
359 allow_negative_stock=None,
360 via_landed_cost_voucher=False,
361 verbose=1,
362 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530363 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530364 self.verbose = verbose
365 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530366 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530367 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530368 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
369 item_code=self.item_code
370 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530371
Nabin Haita77b8c92020-12-21 14:45:50 +0530372 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530373 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530374 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530375
Nabin Haita77b8c92020-12-21 14:45:50 +0530376 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530377 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530378 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530379
380 self.new_items_found = False
381 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530382 self.affected_transactions: Set[Tuple[str, str]] = set()
Nabin Haita77b8c92020-12-21 14:45:50 +0530383
384 self.data = frappe._dict()
385 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530386 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530387
Maricad6078aa2022-06-17 15:13:13 +0530388 def set_precision(self):
389 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
390 self.currency_precision = get_field_precision(
391 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530392 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530393
394 def initialize_previous_data(self, args):
395 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530396 Get previous sl entries for current item for each related warehouse
397 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530398
Ankush Menat494bd9e2022-03-28 18:52:46 +0530399 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530400
Ankush Menat494bd9e2022-03-28 18:52:46 +0530401 self.data = {
402 warehouse1: {
403 'previus_sle': {},
404 'qty_after_transaction': 10,
405 'valuation_rate': 100,
406 'stock_value': 1000,
407 'prev_stock_value': 1000,
408 'stock_queue': '[[10, 100]]',
409 'stock_value_difference': 1000
410 }
411 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530412
413 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530414 self.data.setdefault(args.warehouse, frappe._dict())
415 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530416 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530417 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530418
Ankush Menatc1d986a2021-08-31 19:43:42 +0530419 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
420 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
421
Ankush Menat494bd9e2022-03-28 18:52:46 +0530422 warehouse_dict.update(
423 {
424 "prev_stock_value": previous_sle.stock_value or 0.0,
425 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
426 "stock_value_difference": 0.0,
427 }
428 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530429
Nabin Haita77b8c92020-12-21 14:45:50 +0530430 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530431 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530432
Nabin Haita77b8c92020-12-21 14:45:50 +0530433 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530434 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530435 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530436 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530437 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530438 entries_to_fix = self.get_future_entries_to_fix()
439
440 i = 0
441 while i < len(entries_to_fix):
442 sle = entries_to_fix[i]
443 i += 1
444
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530445 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530446
Nabin Haita77b8c92020-12-21 14:45:50 +0530447 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530448 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530449
Nabin Hait186a0452021-02-18 14:14:21 +0530450 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530451
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530452 if self.exceptions:
453 self.raise_exceptions()
454
Nabin Hait186a0452021-02-18 14:14:21 +0530455 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530456 sl_entries = self.get_sle_against_current_voucher()
457 for sle in sl_entries:
458 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530459
Nabin Haita77b8c92020-12-21 14:45:50 +0530460 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530461 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530462
Ankush Menat494bd9e2022-03-28 18:52:46 +0530463 return frappe.db.sql(
464 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530465 select
466 *, timestamp(posting_date, posting_time) as "timestamp"
467 from
468 `tabStock Ledger Entry`
469 where
470 item_code = %(item_code)s
471 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530472 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530473 and (
474 posting_date = %(posting_date)s and
475 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
476 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530477 order by
478 creation ASC
479 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530480 """,
481 self.args,
482 as_dict=1,
483 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530484
Nabin Haita77b8c92020-12-21 14:45:50 +0530485 def get_future_entries_to_fix(self):
486 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530487 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
488 {"item_code": self.item_code, "warehouse": self.args.warehouse}
489 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530490
Nabin Haita77b8c92020-12-21 14:45:50 +0530491 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530492
Nabin Haita77b8c92020-12-21 14:45:50 +0530493 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530494 dependant_sle = get_sle_by_voucher_detail_no(
495 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
496 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530497
Nabin Haita77b8c92020-12-21 14:45:50 +0530498 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530499 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530500 elif (
501 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
502 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530503 return entries_to_fix
504 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530505 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530506 return entries_to_fix
507 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
508 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530509 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530510 self.initialize_previous_data(dependant_sle)
511 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530512 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530513
514 def update_distinct_item_warehouses(self, dependant_sle):
515 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530516 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530517 if key not in self.distinct_item_warehouses:
518 self.distinct_item_warehouses[key] = val
519 self.new_items_found = True
520 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530521 existing_sle_posting_date = (
522 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
523 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530524 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
525 val.sle_changed = True
526 self.distinct_item_warehouses[key] = val
527 self.new_items_found = True
528
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530529 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530530 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
531
Nabin Haita77b8c92020-12-21 14:45:50 +0530532 # previous sle data for this warehouse
533 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530534 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530535
Anand Doshi0dc79f42015-04-06 12:59:34 +0530536 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 +0530537 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530538 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530539 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530540 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530541 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530542
Nabin Haita77b8c92020-12-21 14:45:50 +0530543 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530544 if not self.args.get("sle_id"):
545 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530546
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530547 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530548 sle.voucher_type == "Stock Reconciliation"
549 and sle.batch_no
550 and sle.voucher_detail_no
551 and sle.actual_qty < 0
552 ):
553 self.reset_actual_qty_for_stock_reco(sle)
554
555 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530556 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
557 and sle.voucher_detail_no
558 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530559 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530560 ):
561 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
562
Ankush Menat66bf21f2022-01-16 20:45:59 +0530563 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530564 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530565 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530566 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530567 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530568
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 )
572 elif sle.batch_no and frappe.db.get_value(
573 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
574 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530575 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530576 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530577 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530578 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530579 self.wh_data.valuation_rate = sle.valuation_rate
580 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530581 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
582 self.wh_data.valuation_rate
583 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530584 if self.valuation_method != "Moving Average":
585 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530586 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530587 if self.valuation_method == "Moving Average":
588 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530589 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530590 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
591 self.wh_data.valuation_rate
592 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530593 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530594 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530595
Rushabh Mehta54047782013-12-26 11:07:46 +0530596 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530597 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530598 if not self.wh_data.qty_after_transaction:
599 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530600 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
601 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530602
Nabin Hait902e8602013-01-08 18:29:24 +0530603 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530604 sle.qty_after_transaction = self.wh_data.qty_after_transaction
605 sle.valuation_rate = self.wh_data.valuation_rate
606 sle.stock_value = self.wh_data.stock_value
607 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530608 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530609 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530610
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530611 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530612
Ankush Menat701878f2022-03-01 18:08:29 +0530613 if not self.args.get("sle_id"):
614 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530615
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530616 def reset_actual_qty_for_stock_reco(self, sle):
617 current_qty = frappe.get_cached_value(
618 "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
619 )
620
621 if current_qty:
622 sle.actual_qty = current_qty * -1
623 elif current_qty == 0:
624 sle.is_cancelled = 1
625
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530626 def validate_negative_stock(self, sle):
627 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530628 validate negative stock for entries current datetime onwards
629 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530630 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530631 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Maricad6078aa2022-06-17 15:13:13 +0530632 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530633
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530634 if diff < 0 and abs(diff) > 0.0001:
635 # negative stock!
636 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530637 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530638 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530639 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530640 return True
641
Nabin Haita77b8c92020-12-21 14:45:50 +0530642 def get_dynamic_incoming_outgoing_rate(self, sle):
643 # Get updated incoming/outgoing rate from transaction
644 if sle.recalculate_rate:
645 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
646
647 if flt(sle.actual_qty) >= 0:
648 sle.incoming_rate = rate
649 else:
650 sle.outgoing_rate = rate
651
652 def get_incoming_outgoing_rate_from_transaction(self, sle):
653 rate = 0
654 # Material Transfer, Repack, Manufacturing
655 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530656 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530657 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
658 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530659 elif sle.voucher_type in (
660 "Purchase Receipt",
661 "Purchase Invoice",
662 "Delivery Note",
663 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530664 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530665 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530666 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530667 from erpnext.controllers.sales_and_purchase_return import (
668 get_rate_for_return, # don't move this import to top
669 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530670
671 rate = get_rate_for_return(
672 sle.voucher_type,
673 sle.voucher_no,
674 sle.item_code,
675 voucher_detail_no=sle.voucher_detail_no,
676 sle=sle,
677 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530678
679 elif (
680 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530681 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530682 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530683 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530684 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530685 else:
686 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530687 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530688 elif sle.voucher_type == "Subcontracting Receipt":
689 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530690 else:
691 rate_field = "incoming_rate"
692
693 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530694 item_code, incoming_rate = frappe.db.get_value(
695 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
696 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530697
698 if item_code == sle.item_code:
699 rate = incoming_rate
700 else:
701 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
702 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530703 elif sle == "Subcontracting Receipt":
704 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530705 else:
706 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530707
Ankush Menat494bd9e2022-03-28 18:52:46 +0530708 rate = frappe.db.get_value(
709 ref_doctype,
710 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
711 rate_field,
712 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530713
714 return rate
715
716 def update_outgoing_rate_on_transaction(self, sle):
717 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530718 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
719 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530720 """
721 if sle.actual_qty and sle.voucher_detail_no:
722 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
723
724 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
725 self.update_rate_on_stock_entry(sle, outgoing_rate)
726 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
727 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
728 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
729 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530730 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
731 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530732
733 def update_rate_on_stock_entry(self, sle, outgoing_rate):
734 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
735
Ankush Menat701878f2022-03-01 18:08:29 +0530736 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
737 if not sle.dependant_sle_voucher_detail_no:
738 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530739
740 def recalculate_amounts_in_stock_entry(self, voucher_no):
741 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530742 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
743 stock_entry.db_update()
744 for d in stock_entry.items:
745 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530746
Nabin Haita77b8c92020-12-21 14:45:50 +0530747 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
748 # Update item's incoming rate on transaction
749 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
750 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530751 frappe.db.set_value(
752 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
753 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530754 else:
755 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530756 frappe.db.set_value(
757 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530758 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530759 "incoming_rate",
760 outgoing_rate,
761 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530762
763 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
764 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530765 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
766 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
767 ):
768 frappe.db.set_value(
769 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
770 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530771 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530772 frappe.db.set_value(
773 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
774 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530775
776 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530777 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530778 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530779 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530780 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530781 d.db_update()
782
Sagar Sharma323bdf82022-05-17 15:14:07 +0530783 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530784 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
785 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530786 else:
787 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530788 "Subcontracting Receipt Supplied Item",
789 sle.voucher_detail_no,
790 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530791 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530792
s-aga-ra6cb6c62023-05-03 09:51:58 +0530793 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530794 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530795 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530796 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530797 d.db_update()
798
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530799 def get_serialized_values(self, sle):
800 incoming_rate = flt(sle.incoming_rate)
801 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530802 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530803
804 if incoming_rate < 0:
805 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530806 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530807
Nabin Hait2620bf42016-02-29 11:30:27 +0530808 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530809 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530810 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530811 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530812 # In case of delivery/stock issue, get average purchase rate
813 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530814 if not sle.is_cancelled:
815 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
816 stock_value_change = -1 * outgoing_value
817 else:
818 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530819
Nabin Haita77b8c92020-12-21 14:45:50 +0530820 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530821
Nabin Hait2620bf42016-02-29 11:30:27 +0530822 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530823 new_stock_value = (
824 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
825 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530826 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530827 # calculate new valuation rate only if stock value is positive
828 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530829 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530830
Nabin Haita77b8c92020-12-21 14:45:50 +0530831 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530832 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
833 sle.voucher_type, sle.voucher_detail_no
834 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530835 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530836 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530837
Nabin Hait328c4f92020-01-02 19:00:32 +0530838 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
839 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530840 all_serial_nos = frappe.get_all(
841 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
842 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530843
Ankush Menat494bd9e2022-03-28 18:52:46 +0530844 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 +0530845
846 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530847 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530848 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530849 incoming_rate = frappe.db.sql(
850 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530851 select incoming_rate
852 from `tabStock Ledger Entry`
853 where
854 company = %s
855 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530856 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530857 and (serial_no = %s
858 or serial_no like %s
859 or serial_no like %s
860 or serial_no like %s
861 )
862 order by posting_date desc
863 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530864 """,
865 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
866 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530867
868 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
869
870 return incoming_values
871
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530872 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530873 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530874 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530875 if new_stock_qty >= 0:
876 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530877 if flt(self.wh_data.qty_after_transaction) <= 0:
878 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530879 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530880 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
881 actual_qty * sle.incoming_rate
882 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530883
Nabin Haita77b8c92020-12-21 14:45:50 +0530884 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530885
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530886 elif sle.outgoing_rate:
887 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530888 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
889 actual_qty * sle.outgoing_rate
890 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530891
Nabin Haita77b8c92020-12-21 14:45:50 +0530892 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530893 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530894 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530895 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530896 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
897 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530898
Nabin Haita77b8c92020-12-21 14:45:50 +0530899 if not self.wh_data.valuation_rate and actual_qty > 0:
900 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530901
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530902 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800903 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530904 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530905 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
906 sle.voucher_type, sle.voucher_detail_no
907 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800908 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530909 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530910
Ankush Menatf089d392022-02-02 12:51:21 +0530911 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530912 incoming_rate = flt(sle.incoming_rate)
913 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530914 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530915
Ankush Menat494bd9e2022-03-28 18:52:46 +0530916 self.wh_data.qty_after_transaction = round_off_if_near_zero(
917 self.wh_data.qty_after_transaction + actual_qty
918 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530919
Ankush Menat97e18a12022-01-15 17:42:25 +0530920 if self.valuation_method == "LIFO":
921 stock_queue = LIFOValuation(self.wh_data.stock_queue)
922 else:
923 stock_queue = FIFOValuation(self.wh_data.stock_queue)
924
Ankush Menatb534fee2022-02-19 20:58:36 +0530925 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
926
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530927 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530928 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530929 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530930
Ankush Menat4b29fb62021-12-18 18:40:22 +0530931 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530932 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
933 sle.voucher_type, sle.voucher_detail_no
934 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530935 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530936 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530937 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530938 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530939
Ankush Menat494bd9e2022-03-28 18:52:46 +0530940 stock_queue.remove_stock(
941 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
942 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530943
Ankush Menatb534fee2022-02-19 20:58:36 +0530944 _qty, stock_value = stock_queue.get_total_stock_and_value()
945
946 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530947
Ankush Menat97e18a12022-01-15 17:42:25 +0530948 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530949 self.wh_data.stock_value = round_off_if_near_zero(
950 self.wh_data.stock_value + stock_value_difference
951 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530952
Nabin Haita77b8c92020-12-21 14:45:50 +0530953 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530954 self.wh_data.stock_queue.append(
955 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
956 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530957
Ankush Menatb534fee2022-02-19 20:58:36 +0530958 if self.wh_data.qty_after_transaction:
959 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
960
Ankush Menatce0514c2022-02-15 11:41:41 +0530961 def update_batched_values(self, sle):
962 incoming_rate = flt(sle.incoming_rate)
963 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530964
Ankush Menat494bd9e2022-03-28 18:52:46 +0530965 self.wh_data.qty_after_transaction = round_off_if_near_zero(
966 self.wh_data.qty_after_transaction + actual_qty
967 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530968
969 if actual_qty > 0:
970 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530971 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530972 outgoing_rate = get_batch_incoming_rate(
973 item_code=sle.item_code,
974 warehouse=sle.warehouse,
975 batch_no=sle.batch_no,
976 posting_date=sle.posting_date,
977 posting_time=sle.posting_time,
978 creation=sle.creation,
979 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530980 if outgoing_rate is None:
981 # This can *only* happen if qty available for the batch is zero.
982 # in such case fall back various other rates.
983 # future entries will correct the overall accounting as each
984 # batch individually uses moving average rates.
985 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530986 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530987
Ankush Menat494bd9e2022-03-28 18:52:46 +0530988 self.wh_data.stock_value = round_off_if_near_zero(
989 self.wh_data.stock_value + stock_value_difference
990 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530991 if self.wh_data.qty_after_transaction:
992 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530993
Javier Wong9b11d9b2017-04-14 18:24:04 +0800994 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530995 ref_item_dt = ""
996
997 if voucher_type == "Stock Entry":
998 ref_item_dt = voucher_type + " Detail"
999 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1000 ref_item_dt = voucher_type + " Item"
1001
1002 if ref_item_dt:
1003 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1004 else:
1005 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301006
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301007 def get_fallback_rate(self, sle) -> float:
1008 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301009 This should only get used for negative stock."""
1010 return get_valuation_rate(
1011 sle.item_code,
1012 sle.warehouse,
1013 sle.voucher_type,
1014 sle.voucher_no,
1015 self.allow_zero_rate,
1016 currency=erpnext.get_company_currency(sle.company),
1017 company=sle.company,
1018 batch_no=sle.batch_no,
1019 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301020
Nabin Haita77b8c92020-12-21 14:45:50 +05301021 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301022 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301023 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1024 sle = sle[0] if sle else frappe._dict()
1025 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301026
Nabin Haita77b8c92020-12-21 14:45:50 +05301027 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301028 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301029 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301030
1031 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301032 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301033 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301034 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301035
Ankush Menat494bd9e2022-03-28 18:52:46 +05301036 if (
1037 exceptions[0]["voucher_type"],
1038 exceptions[0]["voucher_no"],
1039 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301040
Nabin Haita77b8c92020-12-21 14:45:50 +05301041 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301042 abs(deficiency),
1043 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1044 frappe.get_desk_link("Warehouse", warehouse),
1045 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301046 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301047 msg = _(
1048 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1049 ).format(
1050 abs(deficiency),
1051 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1052 frappe.get_desk_link("Warehouse", warehouse),
1053 exceptions[0]["posting_date"],
1054 exceptions[0]["posting_time"],
1055 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1056 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301057
Nabin Haita77b8c92020-12-21 14:45:50 +05301058 if msg:
1059 msg_list.append(msg)
1060
1061 if msg_list:
1062 message = "\n\n".join(msg_list)
1063 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301064 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301065 else:
1066 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301067
Nabin Haita77b8c92020-12-21 14:45:50 +05301068 def update_bin(self):
1069 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301070 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301071 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301072
Ankush Menat494bd9e2022-03-28 18:52:46 +05301073 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301074 if data.valuation_rate is not None:
1075 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301076 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301077
marination8418c4b2021-06-22 21:35:25 +05301078
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301079def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301080 """get stock ledger entries filtered by specific posting datetime conditions"""
1081
Ankush Menat494bd9e2022-03-28 18:52:46 +05301082 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301083 if not args.get("posting_date"):
1084 args["posting_date"] = "1900-01-01"
1085 if not args.get("posting_time"):
1086 args["posting_time"] = "00:00"
1087
1088 voucher_condition = ""
1089 if exclude_current_voucher:
1090 voucher_no = args.get("voucher_no")
1091 voucher_condition = f"and voucher_no != '{voucher_no}'"
1092
Ankush Menat494bd9e2022-03-28 18:52:46 +05301093 sle = frappe.db.sql(
1094 """
marination8418c4b2021-06-22 21:35:25 +05301095 select *, timestamp(posting_date, posting_time) as "timestamp"
1096 from `tabStock Ledger Entry`
1097 where item_code = %(item_code)s
1098 and warehouse = %(warehouse)s
1099 and is_cancelled = 0
1100 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301101 and (
1102 posting_date < %(posting_date)s or
1103 (
1104 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301105 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301106 )
1107 )
marination8418c4b2021-06-22 21:35:25 +05301108 order by timestamp(posting_date, posting_time) desc, creation desc
1109 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301110 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301111 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301112 ),
1113 args,
1114 as_dict=1,
1115 )
marination8418c4b2021-06-22 21:35:25 +05301116
1117 return sle[0] if sle else frappe._dict()
1118
Ankush Menat494bd9e2022-03-28 18:52:46 +05301119
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301120def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301121 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301122 get the last sle on or before the current time-bucket,
1123 to get actual qty before transaction, this function
1124 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301125
Ankush Menat494bd9e2022-03-28 18:52:46 +05301126 args = {
1127 "item_code": "ABC",
1128 "warehouse": "XYZ",
1129 "posting_date": "2012-12-12",
1130 "posting_time": "12:00",
1131 "sle": "name of reference Stock Ledger Entry"
1132 }
Anand Doshi1b531862013-01-10 19:29:51 +05301133 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301134 args["name"] = args.get("sle", None) or ""
1135 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301136 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301137
Ankush Menat494bd9e2022-03-28 18:52:46 +05301138
1139def get_stock_ledger_entries(
1140 previous_sle,
1141 operator=None,
1142 order="desc",
1143 limit=None,
1144 for_update=False,
1145 debug=False,
1146 check_serial_no=True,
1147):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301148 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301149 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1150 operator
1151 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301152 if previous_sle.get("warehouse"):
1153 conditions += " and warehouse = %(warehouse)s"
1154 elif previous_sle.get("warehouse_condition"):
1155 conditions += " and " + previous_sle.get("warehouse_condition")
1156
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301157 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301158 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1159 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301160 conditions += (
1161 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301162 (
1163 serial_no = {0}
1164 or serial_no like {1}
1165 or serial_no like {2}
1166 or serial_no like {3}
1167 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301168 """
1169 ).format(
1170 frappe.db.escape(serial_no),
1171 frappe.db.escape("{}\n%".format(serial_no)),
1172 frappe.db.escape("%\n{}".format(serial_no)),
1173 frappe.db.escape("%\n{}\n%".format(serial_no)),
1174 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301175
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301176 if not previous_sle.get("posting_date"):
1177 previous_sle["posting_date"] = "1900-01-01"
1178 if not previous_sle.get("posting_time"):
1179 previous_sle["posting_time"] = "00:00"
1180
1181 if operator in (">", "<=") and previous_sle.get("name"):
1182 conditions += " and name!=%(name)s"
1183
Ankush Menat494bd9e2022-03-28 18:52:46 +05301184 return frappe.db.sql(
1185 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301186 select *, timestamp(posting_date, posting_time) as "timestamp"
1187 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301188 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301189 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301190 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301191 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301192 %(limit)s %(for_update)s"""
1193 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301194 "conditions": conditions,
1195 "limit": limit or "",
1196 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301197 "order": order,
1198 },
1199 previous_sle,
1200 as_dict=1,
1201 debug=debug,
1202 )
1203
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301204
Nabin Haita77b8c92020-12-21 14:45:50 +05301205def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301206 return frappe.db.get_value(
1207 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301208 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301209 [
1210 "item_code",
1211 "warehouse",
1212 "posting_date",
1213 "posting_time",
1214 "timestamp(posting_date, posting_time) as timestamp",
1215 ],
1216 as_dict=1,
1217 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301218
Ankush Menatce0514c2022-02-15 11:41:41 +05301219
Ankush Menat494bd9e2022-03-28 18:52:46 +05301220def get_batch_incoming_rate(
1221 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1222):
1223
Ankush Menat102fff22022-02-19 15:51:04 +05301224 sle = frappe.qb.DocType("Stock Ledger Entry")
1225
Ankush Menate1c16872022-04-21 20:01:48 +05301226 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301227 posting_date, posting_time
1228 )
Ankush Menat102fff22022-02-19 15:51:04 +05301229 if creation:
1230 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301231 CombineDatetime(sle.posting_date, sle.posting_time)
1232 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301233 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301234
1235 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301236 frappe.qb.from_(sle)
1237 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1238 .where(
1239 (sle.item_code == item_code)
1240 & (sle.warehouse == warehouse)
1241 & (sle.batch_no == batch_no)
1242 & (sle.is_cancelled == 0)
1243 )
1244 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301245 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301246
1247 if batch_details and batch_details[0].batch_qty:
1248 return batch_details[0].batch_value / batch_details[0].batch_qty
1249
1250
Ankush Menat494bd9e2022-03-28 18:52:46 +05301251def get_valuation_rate(
1252 item_code,
1253 warehouse,
1254 voucher_type,
1255 voucher_no,
1256 allow_zero_rate=False,
1257 currency=None,
1258 company=None,
1259 raise_error_if_no_rate=True,
1260 batch_no=None,
1261):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301262
Ankush Menatf7ffe042021-11-01 13:21:14 +05301263 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301264 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301265
Ankush Menat342d09a2022-02-19 14:28:51 +05301266 last_valuation_rate = None
1267
1268 # Get moving average rate of a specific batch number
1269 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301270 last_valuation_rate = frappe.db.sql(
1271 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301272 select sum(stock_value_difference) / sum(actual_qty)
1273 from `tabStock Ledger Entry`
1274 where
1275 item_code = %s
1276 AND warehouse = %s
1277 AND batch_no = %s
1278 AND is_cancelled = 0
1279 AND NOT (voucher_no = %s AND voucher_type = %s)
1280 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301281 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1282 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301283
Ankush Menatf7ffe042021-11-01 13:21:14 +05301284 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301285 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301286 last_valuation_rate = frappe.db.sql(
1287 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301288 from `tabStock Ledger Entry` force index (item_warehouse)
1289 where
1290 item_code = %s
1291 AND warehouse = %s
1292 AND valuation_rate >= 0
1293 AND is_cancelled = 0
1294 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301295 order by posting_date desc, posting_time desc, name desc limit 1""",
1296 (item_code, warehouse, voucher_no, voucher_type),
1297 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301298
Nabin Haita645f362018-03-01 10:31:24 +05301299 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301300 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301301
1302 # If negative stock allowed, and item delivered without any incoming entry,
1303 # system does not found any SLE, then take valuation rate from Item
1304 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301305
1306 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301307 # try Item Standard rate
1308 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301309
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301310 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301311 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301312 valuation_rate = frappe.db.get_value(
1313 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1314 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301315
Ankush Menat494bd9e2022-03-28 18:52:46 +05301316 if (
1317 not allow_zero_rate
1318 and not valuation_rate
1319 and raise_error_if_no_rate
1320 and cint(erpnext.is_perpetual_inventory_enabled(company))
1321 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301322 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301323
Ankush Menat494bd9e2022-03-28 18:52:46 +05301324 message = _(
1325 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1326 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301327 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301328 solutions = (
1329 "<li>"
1330 + _(
1331 "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."
1332 ).format(voucher_type)
1333 + "</li>"
1334 )
1335 solutions += (
1336 "<li>"
1337 + _("If not, you can Cancel / Submit this entry")
1338 + " {0} ".format(frappe.bold("after"))
1339 + _("performing either one below:")
1340 + "</li>"
1341 )
Marica97715f22020-05-11 20:45:37 +05301342 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1343 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1344 msg = message + solutions + sub_solutions + "</li>"
1345
1346 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301347
1348 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301349
Ankush Menat494bd9e2022-03-28 18:52:46 +05301350
Ankush Menate7109c12021-08-26 16:40:45 +05301351def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301352 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301353 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301354 qty_shift = args.actual_qty
1355
Ankush Menat7c839c42022-05-06 12:09:08 +05301356 args["time_format"] = "%H:%i:%s"
1357
marination8418c4b2021-06-22 21:35:25 +05301358 # find difference/shift in qty caused by stock reconciliation
1359 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301360 qty_shift = get_stock_reco_qty_shift(args)
1361
1362 # find the next nearest stock reco so that we only recalculate SLEs till that point
1363 next_stock_reco_detail = get_next_stock_reco(args)
1364 if next_stock_reco_detail:
1365 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301366 if detail.batch_no:
1367 regenerate_sle_for_batch_stock_reco(detail)
1368
marination40389772021-07-02 17:13:45 +05301369 # add condition to update SLEs before this date & time
1370 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301371
Ankush Menat494bd9e2022-03-28 18:52:46 +05301372 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301373 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301374 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301375 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301376 where
1377 item_code = %(item_code)s
1378 and warehouse = %(warehouse)s
1379 and voucher_no != %(voucher_no)s
1380 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301381 and (
1382 posting_date > %(posting_date)s or
1383 (
1384 posting_date = %(posting_date)s and
1385 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1386 )
1387 )
marination40389772021-07-02 17:13:45 +05301388 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301389 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301390 args,
1391 )
Nabin Hait186a0452021-02-18 14:14:21 +05301392
1393 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1394
Ankush Menat494bd9e2022-03-28 18:52:46 +05301395
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301396def regenerate_sle_for_batch_stock_reco(detail):
1397 doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301398 doc.recalculate_current_qty(detail.item_code, detail.batch_no)
Rohit Waghchauref2253dd2023-04-22 11:16:12 +05301399
1400 if not frappe.db.exists(
1401 "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
1402 ):
Rohit Waghchaure2d5ccc02023-05-01 21:17:18 +05301403 doc.repost_future_sle_and_gle(force=True)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301404
1405
marination40389772021-07-02 17:13:45 +05301406def get_stock_reco_qty_shift(args):
1407 stock_reco_qty_shift = 0
1408 if args.get("is_cancelled"):
1409 if args.get("previous_qty_after_transaction"):
1410 # get qty (balance) that was set at submission
1411 last_balance = args.get("previous_qty_after_transaction")
1412 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1413 else:
1414 stock_reco_qty_shift = flt(args.actual_qty)
1415 else:
1416 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301417 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301418 "qty_after_transaction"
1419 )
marination40389772021-07-02 17:13:45 +05301420
1421 if last_balance is not None:
1422 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1423 else:
1424 stock_reco_qty_shift = args.qty_after_transaction
1425
1426 return stock_reco_qty_shift
1427
Ankush Menat494bd9e2022-03-28 18:52:46 +05301428
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301429def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301430 """Returns next nearest stock reconciliaton's details."""
1431
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301432 sle = frappe.qb.DocType("Stock Ledger Entry")
1433
1434 query = (
1435 frappe.qb.from_(sle)
1436 .select(
1437 sle.name,
1438 sle.posting_date,
1439 sle.posting_time,
1440 sle.creation,
1441 sle.voucher_no,
1442 sle.item_code,
1443 sle.batch_no,
1444 sle.actual_qty,
1445 )
1446 .where(
1447 (sle.item_code == kwargs.get("item_code"))
1448 & (sle.warehouse == kwargs.get("warehouse"))
1449 & (sle.voucher_type == "Stock Reconciliation")
1450 & (sle.voucher_no != kwargs.get("voucher_no"))
1451 & (sle.is_cancelled == 0)
1452 & (
1453 (
1454 CombineDatetime(sle.posting_date, sle.posting_time)
1455 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301456 )
1457 | (
1458 (
1459 CombineDatetime(sle.posting_date, sle.posting_time)
1460 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301461 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301462 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301463 )
1464 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301465 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301466 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1467 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301468 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301469 )
1470
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301471 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301472 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301473
1474 return query.run(as_dict=True)
1475
marination40389772021-07-02 17:13:45 +05301476
1477def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301478 return f"""
1479 and
1480 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1481 or (
1482 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1483 and creation < '{detail.creation}'
1484 )
1485 )"""
1486
Ankush Menat494bd9e2022-03-28 18:52:46 +05301487
Ankush Menate7109c12021-08-26 16:40:45 +05301488def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301489 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301490 return
1491 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1492 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301493
Ankush Menat5eba5752021-12-07 23:03:52 +05301494 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301495
1496 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301497 message = _(
1498 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1499 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301500 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301501 frappe.get_desk_link("Item", args.item_code),
1502 frappe.get_desk_link("Warehouse", args.warehouse),
1503 neg_sle[0]["posting_date"],
1504 neg_sle[0]["posting_time"],
1505 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1506 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301507
Ankush Menat494bd9e2022-03-28 18:52:46 +05301508 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301509
1510 if not args.batch_no:
1511 return
1512
1513 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301514 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301515 message = _(
1516 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1517 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301518 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301519 frappe.get_desk_link("Batch", args.batch_no),
1520 frappe.get_desk_link("Warehouse", args.warehouse),
1521 neg_batch_sle[0]["posting_date"],
1522 neg_batch_sle[0]["posting_time"],
1523 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1524 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301525 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301526
Nabin Haita77b8c92020-12-21 14:45:50 +05301527
Maricad6078aa2022-06-17 15:13:13 +05301528def is_negative_with_precision(neg_sle, is_batch=False):
1529 """
1530 Returns whether system precision rounded qty is insufficient.
1531 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1532 """
1533
1534 if not neg_sle:
1535 return False
1536
1537 field = "cumulative_total" if is_batch else "qty_after_transaction"
1538 precision = cint(frappe.db.get_default("float_precision")) or 2
1539 qty_deficit = flt(neg_sle[0][field], precision)
1540
1541 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1542
1543
Nabin Haita77b8c92020-12-21 14:45:50 +05301544def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301545 return frappe.db.sql(
1546 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301547 select
1548 qty_after_transaction, posting_date, posting_time,
1549 voucher_type, voucher_no
1550 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301551 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301552 item_code = %(item_code)s
1553 and warehouse = %(warehouse)s
1554 and voucher_no != %(voucher_no)s
1555 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1556 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301557 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301558 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301559 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301560 """,
1561 args,
1562 as_dict=1,
1563 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301564
Ankush Menat5eba5752021-12-07 23:03:52 +05301565
1566def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301567 return frappe.db.sql(
1568 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301569 with batch_ledger as (
1570 select
1571 posting_date, posting_time, voucher_type, voucher_no,
1572 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1573 from `tabStock Ledger Entry`
1574 where
1575 item_code = %(item_code)s
1576 and warehouse = %(warehouse)s
1577 and batch_no=%(batch_no)s
1578 and is_cancelled = 0
1579 order by posting_date, posting_time, creation
1580 )
1581 select * from batch_ledger
1582 where
1583 cumulative_total < 0.0
1584 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1585 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301586 """,
1587 args,
1588 as_dict=1,
1589 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301590
1591
1592def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1593 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1594 return True
1595 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1596 return True
1597 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301598
1599
1600def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1601 """
1602 For inter company transfer, incoming rate is the average of the outgoing rate
1603 """
1604 rate = 0.0
1605
1606 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1607
1608 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1609
1610 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1611
1612 if reference_name:
1613 rate = frappe.get_cached_value(
1614 doctype,
1615 reference_name,
1616 "incoming_rate",
1617 )
1618
1619 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301620
1621
1622def is_internal_transfer(sle):
1623 data = frappe.get_cached_value(
1624 sle.voucher_type,
1625 sle.voucher_no,
1626 ["is_internal_supplier", "represents_company", "company"],
1627 as_dict=True,
1628 )
1629
1630 if data.is_internal_supplier and data.represents_company == data.company:
1631 return True