blob: 9f182a153d18715fffbd88b97aba263d3b3420a0 [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
s-aga-rf0acb202023-04-12 14:13:54 +053016from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
17 get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
18)
Chillar Anand915b3432021-09-02 16:44:59 +053019from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053020 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053021 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053022 get_valuation_method,
23)
Ankush Menatb534fee2022-02-19 20:58:36 +053024from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053025
Nabin Hait97bce3a2021-07-12 13:24:43 +053026
Ankush Menat494bd9e2022-03-28 18:52:46 +053027class NegativeStockError(frappe.ValidationError):
28 pass
29
30
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053031class SerialNoExistsInFutureTransaction(frappe.ValidationError):
32 pass
Nabin Hait902e8602013-01-08 18:29:24 +053033
Anand Doshi5b004ff2013-09-25 19:55:41 +053034
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053035def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053036 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053037
Ankush Menat494bd9e2022-03-28 18:52:46 +053038 args:
39 - allow_negative_stock: disable negative stock valiations if true
40 - via_landed_cost_voucher: landed cost voucher cancels and reposts
41 entries of purchase document. This flag is used to identify if
42 cancellation and repost is happening via landed cost voucher, in
43 such cases certain validations need to be ignored (like negative
44 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053045 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053046 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053047
Nabin Haitca775742013-09-26 16:16:44 +053048 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053049 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053050 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053051 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053052 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053053
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053054 args = get_args_for_future_sle(sl_entries[0])
55 future_sle_exists(args, sl_entries)
56
Nabin Haitca775742013-09-26 16:16:44 +053057 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053058 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053059 validate_serial_no(sle)
60
Nabin Haita77b8c92020-12-21 14:45:50 +053061 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053062 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053063
Ankush Menat494bd9e2022-03-28 18:52:46 +053064 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
65 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
66 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
67 )
68 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053069
Ankush Menat494bd9e2022-03-28 18:52:46 +053070 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
71 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
72 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
73 )
74 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053075
Ankush Menat494bd9e2022-03-28 18:52:46 +053076 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053077 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053078
Nabin Haita77b8c92020-12-21 14:45:50 +053079 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053080
81 if sle.get("voucher_type") == "Stock Reconciliation":
82 # preserve previous_qty_after_transaction for qty reposting
83 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
84
Ankush Menat494bd9e2022-03-28 18:52:46 +053085 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053086 if is_stock_item:
87 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053088 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053089 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053090 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +053091 frappe.msgprint(
92 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
93 )
94
Ankush Menatcef84c22021-12-03 12:18:59 +053095
96def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
97 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
98 if not args.get("posting_date"):
99 args["posting_date"] = nowdate()
100
marination7a5fd712022-07-04 17:46:54 +0530101 if not (args.get("is_cancelled") and via_landed_cost_voucher):
102 # Reposts only current voucher SL Entries
103 # Updates valuation rate, stock value, stock queue for current transaction
104 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
marination7a5fd712022-07-04 17:46:54 +0530120 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530121 update_qty_in_future_sle(args, allow_negative_stock)
122
Nabin Haitadeb9762014-10-06 11:53:52 +0530123
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530124def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530125 return frappe._dict(
126 {
127 "voucher_type": row.get("voucher_type"),
128 "voucher_no": row.get("voucher_no"),
129 "posting_date": row.get("posting_date"),
130 "posting_time": row.get("posting_time"),
131 }
132 )
133
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530134
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530135def validate_serial_no(sle):
136 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530137
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530138 for sn in get_serial_nos(sle.serial_no):
139 args = copy.deepcopy(sle)
140 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530141 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530142
143 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530144 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530145 voucher_type = frappe.bold(row.voucher_type)
146 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530147 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530148
149 if vouchers:
150 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530151 msg = (
152 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
153 The list of the transactions are as below."""
154 + "<br><br><ul><li>"
155 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530156
Ankush Menat494bd9e2022-03-28 18:52:46 +0530157 msg += "</li><li>".join(vouchers)
158 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530159
Ankush Menat494bd9e2022-03-28 18:52:46 +0530160 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530161 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
162
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163
Nabin Hait186a0452021-02-18 14:14:21 +0530164def validate_cancellation(args):
165 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530166 repost_entry = frappe.db.get_value(
167 "Repost Item Valuation",
168 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
169 ["name", "status"],
170 as_dict=1,
171 )
Nabin Hait186a0452021-02-18 14:14:21 +0530172
173 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530174 if repost_entry.status == "In Progress":
175 frappe.throw(
176 _(
177 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
178 )
179 )
180 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530181 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530182 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530183 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530184 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530185
Ankush Menat494bd9e2022-03-28 18:52:46 +0530186
Nabin Hait9653f602013-08-20 15:37:33 +0530187def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530188 frappe.db.sql(
189 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530190 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530191 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530192 (now(), frappe.session.user, voucher_type, voucher_no),
193 )
194
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530195
Nabin Hait54c865e2015-03-27 15:38:31 +0530196def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530197 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530198 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530199 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530200 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530201 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530202 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530203 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530204
Ankush Menat494bd9e2022-03-28 18:52:46 +0530205
206def repost_future_sle(
207 args=None,
208 voucher_type=None,
209 voucher_no=None,
210 allow_negative_stock=None,
211 via_landed_cost_voucher=False,
212 doc=None,
213):
Nabin Haite1fa7232022-07-20 15:19:09 +0530214 if not args:
215 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530216
217 items_to_be_repost = get_items_to_be_repost(
218 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
219 )
220 if items_to_be_repost:
221 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530222
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530223 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Ankush Menatecdb4932022-04-17 19:06:13 +0530224 affected_transactions = get_affected_transactions(doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530225
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530226 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530227 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530228 validate_item_warehouse(args[i])
229
Ankush Menat494bd9e2022-03-28 18:52:46 +0530230 obj = update_entries_after(
231 {
232 "item_code": args[i].get("item_code"),
233 "warehouse": args[i].get("warehouse"),
234 "posting_date": args[i].get("posting_date"),
235 "posting_time": args[i].get("posting_time"),
236 "creation": args[i].get("creation"),
237 "distinct_item_warehouses": distinct_item_warehouses,
238 },
239 allow_negative_stock=allow_negative_stock,
240 via_landed_cost_voucher=via_landed_cost_voucher,
241 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530242 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530243
Ankush Menat494bd9e2022-03-28 18:52:46 +0530244 distinct_item_warehouses[
245 (args[i].get("item_code"), args[i].get("warehouse"))
246 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530247
Nabin Hait97bce3a2021-07-12 13:24:43 +0530248 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530249 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530250 if ("args_idx" not in data and not data.reposting_status) or (
251 data.sle_changed and data.reposting_status
252 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530253 data.args_idx = len(args)
254 args.append(data.sle)
255 elif data.sle_changed and not data.reposting_status:
256 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530257
Nabin Hait97bce3a2021-07-12 13:24:43 +0530258 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530259 i += 1
260
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530261 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530262 update_args_in_repost_item_valuation(
263 doc, i, args, distinct_item_warehouses, affected_transactions
264 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530265
Ankush Menat494bd9e2022-03-28 18:52:46 +0530266
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530267def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530268 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530269 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530270 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530271 frappe.throw(_(validation_msg))
272
Ankush Menat494bd9e2022-03-28 18:52:46 +0530273
Ankush Menatecdb4932022-04-17 19:06:13 +0530274def update_args_in_repost_item_valuation(
275 doc, index, args, distinct_item_warehouses, affected_transactions
276):
Ankush Menatecdb4932022-04-17 19:06:13 +0530277 doc.db_set(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530278 {
279 "items_to_be_repost": json.dumps(args, default=str),
280 "distinct_item_and_warehouse": json.dumps(
281 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
282 ),
283 "current_index": index,
Ankush Menat8f519542022-04-19 01:26:16 +0530284 "affected_transactions": frappe.as_json(affected_transactions),
Ankush Menatecdb4932022-04-17 19:06:13 +0530285 }
Ankush Menat494bd9e2022-03-28 18:52:46 +0530286 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530287
Ankush Menatecdb4932022-04-17 19:06:13 +0530288 if not frappe.flags.in_test:
289 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530290
Ankush Menat494bd9e2022-03-28 18:52:46 +0530291 frappe.publish_realtime(
292 "item_reposting_progress",
293 {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
294 )
295
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530297def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
298 items_to_be_repost = []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530299 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530300 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530301
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530302 if not items_to_be_repost and voucher_type and voucher_no:
303 items_to_be_repost = frappe.db.get_all(
304 "Stock Ledger Entry",
305 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
306 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
307 order_by="creation asc",
308 group_by="item_code, warehouse",
309 )
310
Nabin Haite1fa7232022-07-20 15:19:09 +0530311 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530312
Ankush Menat494bd9e2022-03-28 18:52:46 +0530313
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530314def get_distinct_item_warehouse(args=None, doc=None):
315 distinct_item_warehouses = {}
316 if doc and doc.distinct_item_and_warehouse:
317 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530318 distinct_item_warehouses = {
319 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
320 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530321 else:
322 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530323 distinct_item_warehouses.setdefault(
324 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
325 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530326
327 return distinct_item_warehouses
328
Ankush Menat494bd9e2022-03-28 18:52:46 +0530329
Ankush Menatecdb4932022-04-17 19:06:13 +0530330def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
331 if not doc.affected_transactions:
332 return set()
333
334 transactions = frappe.parse_json(doc.affected_transactions)
335 return {tuple(transaction) for transaction in transactions}
336
337
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530338def get_current_index(doc=None):
339 if doc and doc.current_index:
340 return doc.current_index
341
Ankush Menat494bd9e2022-03-28 18:52:46 +0530342
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530343class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530344 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530345 update valution rate and qty after transaction
346 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530347
Ankush Menat494bd9e2022-03-28 18:52:46 +0530348 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530349
Ankush Menat494bd9e2022-03-28 18:52:46 +0530350 args = {
351 "item_code": "ABC",
352 "warehouse": "XYZ",
353 "posting_date": "2012-12-12",
354 "posting_time": "12:00"
355 }
Nabin Hait902e8602013-01-08 18:29:24 +0530356 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530357
358 def __init__(
359 self,
360 args,
361 allow_zero_rate=False,
362 allow_negative_stock=None,
363 via_landed_cost_voucher=False,
364 verbose=1,
365 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530366 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530367 self.verbose = verbose
368 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530369 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530370 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530371 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
372 item_code=self.item_code
373 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530374
Nabin Haita77b8c92020-12-21 14:45:50 +0530375 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530376 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530377 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530378
Nabin Haita77b8c92020-12-21 14:45:50 +0530379 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530380 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530381 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530382
383 self.new_items_found = False
384 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530385 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-rf0acb202023-04-12 14:13:54 +0530386 self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
Nabin Haita77b8c92020-12-21 14:45:50 +0530387
388 self.data = frappe._dict()
389 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530390 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530391
Maricad6078aa2022-06-17 15:13:13 +0530392 def set_precision(self):
393 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
394 self.currency_precision = get_field_precision(
395 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530396 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530397
398 def initialize_previous_data(self, args):
399 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530400 Get previous sl entries for current item for each related warehouse
401 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530402
Ankush Menat494bd9e2022-03-28 18:52:46 +0530403 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530404
Ankush Menat494bd9e2022-03-28 18:52:46 +0530405 self.data = {
406 warehouse1: {
407 'previus_sle': {},
408 'qty_after_transaction': 10,
409 'valuation_rate': 100,
410 'stock_value': 1000,
411 'prev_stock_value': 1000,
412 'stock_queue': '[[10, 100]]',
413 'stock_value_difference': 1000
414 }
415 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530416
417 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530418 self.data.setdefault(args.warehouse, frappe._dict())
419 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530420 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530421 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530422
Ankush Menatc1d986a2021-08-31 19:43:42 +0530423 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
424 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
425
Ankush Menat494bd9e2022-03-28 18:52:46 +0530426 warehouse_dict.update(
427 {
428 "prev_stock_value": previous_sle.stock_value or 0.0,
429 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
430 "stock_value_difference": 0.0,
431 }
432 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530433
Nabin Haita77b8c92020-12-21 14:45:50 +0530434 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530435 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530436
Nabin Haita77b8c92020-12-21 14:45:50 +0530437 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530438 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530439 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530440 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530441 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530442 entries_to_fix = self.get_future_entries_to_fix()
443
444 i = 0
445 while i < len(entries_to_fix):
446 sle = entries_to_fix[i]
447 i += 1
448
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530449 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530450
Nabin Haita77b8c92020-12-21 14:45:50 +0530451 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530452 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530453
Nabin Hait186a0452021-02-18 14:14:21 +0530454 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530455
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530456 if self.exceptions:
457 self.raise_exceptions()
458
Nabin Hait186a0452021-02-18 14:14:21 +0530459 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530460 sl_entries = self.get_sle_against_current_voucher()
461 for sle in sl_entries:
462 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530463
Nabin Haita77b8c92020-12-21 14:45:50 +0530464 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530465 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530466
Ankush Menat494bd9e2022-03-28 18:52:46 +0530467 return frappe.db.sql(
468 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530469 select
470 *, timestamp(posting_date, posting_time) as "timestamp"
471 from
472 `tabStock Ledger Entry`
473 where
474 item_code = %(item_code)s
475 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530476 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530477 and (
478 posting_date = %(posting_date)s and
479 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
480 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530481 order by
482 creation ASC
483 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530484 """,
485 self.args,
486 as_dict=1,
487 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530488
Nabin Haita77b8c92020-12-21 14:45:50 +0530489 def get_future_entries_to_fix(self):
490 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530491 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
492 {"item_code": self.item_code, "warehouse": self.args.warehouse}
493 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530494
Nabin Haita77b8c92020-12-21 14:45:50 +0530495 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530496
Nabin Haita77b8c92020-12-21 14:45:50 +0530497 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530498 dependant_sle = get_sle_by_voucher_detail_no(
499 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
500 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530501
Nabin Haita77b8c92020-12-21 14:45:50 +0530502 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530503 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530504 elif (
505 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
506 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530507 return entries_to_fix
508 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530509 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530510 return entries_to_fix
511 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
512 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530513 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530514 self.initialize_previous_data(dependant_sle)
515 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530516 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530517
518 def update_distinct_item_warehouses(self, dependant_sle):
519 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530520 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530521 if key not in self.distinct_item_warehouses:
522 self.distinct_item_warehouses[key] = val
523 self.new_items_found = True
524 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530525 existing_sle_posting_date = (
526 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
527 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530528 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
529 val.sle_changed = True
530 self.distinct_item_warehouses[key] = val
531 self.new_items_found = True
532
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530533 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
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530551 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530552 sle.voucher_type == "Stock Reconciliation"
553 and sle.batch_no
554 and sle.voucher_detail_no
555 and sle.actual_qty < 0
556 ):
557 self.reset_actual_qty_for_stock_reco(sle)
558
559 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530560 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
561 and sle.voucher_detail_no
562 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530563 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530564 ):
565 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
566
Ankush Menat66bf21f2022-01-16 20:45:59 +0530567 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530568 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530569 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530570 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530571 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530572
Ankush Menat494bd9e2022-03-28 18:52:46 +0530573 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
574 self.wh_data.valuation_rate
575 )
576 elif sle.batch_no and frappe.db.get_value(
577 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
578 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530579 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530580 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530581 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530582 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530583 self.wh_data.valuation_rate = sle.valuation_rate
584 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530585 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
586 self.wh_data.valuation_rate
587 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530588 if self.valuation_method != "Moving Average":
589 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530590 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530591 if self.valuation_method == "Moving Average":
592 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530593 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530594 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
595 self.wh_data.valuation_rate
596 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530597 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530598 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530599
Rushabh Mehta54047782013-12-26 11:07:46 +0530600 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530601 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530602 if not self.wh_data.qty_after_transaction:
603 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530604 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
605 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530606
Nabin Hait902e8602013-01-08 18:29:24 +0530607 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530608 sle.qty_after_transaction = self.wh_data.qty_after_transaction
609 sle.valuation_rate = self.wh_data.valuation_rate
610 sle.stock_value = self.wh_data.stock_value
611 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530612 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530613 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530614
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530615 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530616
Ankush Menat701878f2022-03-01 18:08:29 +0530617 if not self.args.get("sle_id"):
618 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530619
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530620 def reset_actual_qty_for_stock_reco(self, sle):
621 current_qty = frappe.get_cached_value(
622 "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
623 )
624
625 if current_qty:
626 sle.actual_qty = current_qty * -1
627 elif current_qty == 0:
628 sle.is_cancelled = 1
629
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530630 def validate_negative_stock(self, sle):
631 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530632 validate negative stock for entries current datetime onwards
633 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530634 """
s-aga-rf0acb202023-04-12 14:13:54 +0530635 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530636 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530637
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530638 if diff < 0 and abs(diff) > 0.0001:
639 # negative stock!
640 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530641 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530642 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530643 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530644 return True
645
Nabin Haita77b8c92020-12-21 14:45:50 +0530646 def get_dynamic_incoming_outgoing_rate(self, sle):
647 # Get updated incoming/outgoing rate from transaction
648 if sle.recalculate_rate:
649 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
650
651 if flt(sle.actual_qty) >= 0:
652 sle.incoming_rate = rate
653 else:
654 sle.outgoing_rate = rate
655
656 def get_incoming_outgoing_rate_from_transaction(self, sle):
657 rate = 0
658 # Material Transfer, Repack, Manufacturing
659 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530660 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530661 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
662 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530663 elif sle.voucher_type in (
664 "Purchase Receipt",
665 "Purchase Invoice",
666 "Delivery Note",
667 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530668 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530669 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530670 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530671 from erpnext.controllers.sales_and_purchase_return import (
672 get_rate_for_return, # don't move this import to top
673 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530674
675 rate = get_rate_for_return(
676 sle.voucher_type,
677 sle.voucher_no,
678 sle.item_code,
679 voucher_detail_no=sle.voucher_detail_no,
680 sle=sle,
681 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530682
683 elif (
684 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530685 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530686 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530687 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530688 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530689 else:
690 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530691 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530692 elif sle.voucher_type == "Subcontracting Receipt":
693 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530694 else:
695 rate_field = "incoming_rate"
696
697 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530698 item_code, incoming_rate = frappe.db.get_value(
699 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
700 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530701
702 if item_code == sle.item_code:
703 rate = incoming_rate
704 else:
705 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
706 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530707 elif sle == "Subcontracting Receipt":
708 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530709 else:
710 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530711
Ankush Menat494bd9e2022-03-28 18:52:46 +0530712 rate = frappe.db.get_value(
713 ref_doctype,
714 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
715 rate_field,
716 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530717
718 return rate
719
720 def update_outgoing_rate_on_transaction(self, sle):
721 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530722 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
723 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530724 """
725 if sle.actual_qty and sle.voucher_detail_no:
726 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
727
728 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
729 self.update_rate_on_stock_entry(sle, outgoing_rate)
730 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
731 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
732 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
733 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530734 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
735 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530736
737 def update_rate_on_stock_entry(self, sle, outgoing_rate):
738 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
739
Ankush Menat701878f2022-03-01 18:08:29 +0530740 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
741 if not sle.dependant_sle_voucher_detail_no:
742 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530743
744 def recalculate_amounts_in_stock_entry(self, voucher_no):
745 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530746 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
747 stock_entry.db_update()
748 for d in stock_entry.items:
749 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530750
Nabin Haita77b8c92020-12-21 14:45:50 +0530751 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
752 # Update item's incoming rate on transaction
753 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
754 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530755 frappe.db.set_value(
756 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
757 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530758 else:
759 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530760 frappe.db.set_value(
761 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530762 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530763 "incoming_rate",
764 outgoing_rate,
765 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530766
767 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
768 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530769 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
770 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
771 ):
772 frappe.db.set_value(
773 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
774 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530775 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530776 frappe.db.set_value(
777 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
778 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530779
780 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530781 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530782 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530783 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530784 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530785 d.db_update()
786
Sagar Sharma323bdf82022-05-17 15:14:07 +0530787 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530788 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
789 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530790 else:
791 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530792 "Subcontracting Receipt Supplied Item",
793 sle.voucher_detail_no,
794 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530795 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530796
s-aga-ra6cb6c62023-05-03 09:51:58 +0530797 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530798 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530799 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530800 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530801 d.db_update()
802
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530803 def get_serialized_values(self, sle):
804 incoming_rate = flt(sle.incoming_rate)
805 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530806 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530807
808 if incoming_rate < 0:
809 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530810 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530811
Nabin Hait2620bf42016-02-29 11:30:27 +0530812 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530813 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530814 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530815 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530816 # In case of delivery/stock issue, get average purchase rate
817 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530818 if not sle.is_cancelled:
819 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
820 stock_value_change = -1 * outgoing_value
821 else:
822 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530823
Nabin Haita77b8c92020-12-21 14:45:50 +0530824 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530825
Nabin Hait2620bf42016-02-29 11:30:27 +0530826 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530827 new_stock_value = (
828 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
829 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530830 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530831 # calculate new valuation rate only if stock value is positive
832 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530833 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530834
Nabin Haita77b8c92020-12-21 14:45:50 +0530835 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530836 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
837 sle.voucher_type, sle.voucher_detail_no
838 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530839 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530840 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530841
Nabin Hait328c4f92020-01-02 19:00:32 +0530842 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
843 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530844 all_serial_nos = frappe.get_all(
845 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
846 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530847
Ankush Menat494bd9e2022-03-28 18:52:46 +0530848 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 +0530849
850 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530851 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530852 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530853 incoming_rate = frappe.db.sql(
854 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530855 select incoming_rate
856 from `tabStock Ledger Entry`
857 where
858 company = %s
859 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530860 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530861 and (serial_no = %s
862 or serial_no like %s
863 or serial_no like %s
864 or serial_no like %s
865 )
866 order by posting_date desc
867 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530868 """,
869 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
870 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530871
872 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
873
874 return incoming_values
875
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530876 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530877 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530878 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530879 if new_stock_qty >= 0:
880 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530881 if flt(self.wh_data.qty_after_transaction) <= 0:
882 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530883 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530884 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
885 actual_qty * sle.incoming_rate
886 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530887
Nabin Haita77b8c92020-12-21 14:45:50 +0530888 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530889
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530890 elif sle.outgoing_rate:
891 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530892 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
893 actual_qty * sle.outgoing_rate
894 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530895
Nabin Haita77b8c92020-12-21 14:45:50 +0530896 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530897 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530898 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530899 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530900 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
901 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530902
Nabin Haita77b8c92020-12-21 14:45:50 +0530903 if not self.wh_data.valuation_rate and actual_qty > 0:
904 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530905
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530906 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800907 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530908 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530909 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
910 sle.voucher_type, sle.voucher_detail_no
911 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800912 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530913 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530914
Ankush Menatf089d392022-02-02 12:51:21 +0530915 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530916 incoming_rate = flt(sle.incoming_rate)
917 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530918 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530919
Ankush Menat494bd9e2022-03-28 18:52:46 +0530920 self.wh_data.qty_after_transaction = round_off_if_near_zero(
921 self.wh_data.qty_after_transaction + actual_qty
922 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530923
Ankush Menat97e18a12022-01-15 17:42:25 +0530924 if self.valuation_method == "LIFO":
925 stock_queue = LIFOValuation(self.wh_data.stock_queue)
926 else:
927 stock_queue = FIFOValuation(self.wh_data.stock_queue)
928
Ankush Menatb534fee2022-02-19 20:58:36 +0530929 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
930
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530931 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530932 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530933 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530934
Ankush Menat4b29fb62021-12-18 18:40:22 +0530935 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530936 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
937 sle.voucher_type, sle.voucher_detail_no
938 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530939 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530940 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530941 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530942 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530943
Ankush Menat494bd9e2022-03-28 18:52:46 +0530944 stock_queue.remove_stock(
945 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
946 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530947
Ankush Menatb534fee2022-02-19 20:58:36 +0530948 _qty, stock_value = stock_queue.get_total_stock_and_value()
949
950 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530951
Ankush Menat97e18a12022-01-15 17:42:25 +0530952 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530953 self.wh_data.stock_value = round_off_if_near_zero(
954 self.wh_data.stock_value + stock_value_difference
955 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530956
Nabin Haita77b8c92020-12-21 14:45:50 +0530957 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530958 self.wh_data.stock_queue.append(
959 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
960 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530961
Ankush Menatb534fee2022-02-19 20:58:36 +0530962 if self.wh_data.qty_after_transaction:
963 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
964
Ankush Menatce0514c2022-02-15 11:41:41 +0530965 def update_batched_values(self, sle):
966 incoming_rate = flt(sle.incoming_rate)
967 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530968
Ankush Menat494bd9e2022-03-28 18:52:46 +0530969 self.wh_data.qty_after_transaction = round_off_if_near_zero(
970 self.wh_data.qty_after_transaction + actual_qty
971 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530972
973 if actual_qty > 0:
974 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530975 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530976 outgoing_rate = get_batch_incoming_rate(
977 item_code=sle.item_code,
978 warehouse=sle.warehouse,
979 batch_no=sle.batch_no,
980 posting_date=sle.posting_date,
981 posting_time=sle.posting_time,
982 creation=sle.creation,
983 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530984 if outgoing_rate is None:
985 # This can *only* happen if qty available for the batch is zero.
986 # in such case fall back various other rates.
987 # future entries will correct the overall accounting as each
988 # batch individually uses moving average rates.
989 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530990 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530991
Ankush Menat494bd9e2022-03-28 18:52:46 +0530992 self.wh_data.stock_value = round_off_if_near_zero(
993 self.wh_data.stock_value + stock_value_difference
994 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530995 if self.wh_data.qty_after_transaction:
996 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530997
Javier Wong9b11d9b2017-04-14 18:24:04 +0800998 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530999 ref_item_dt = ""
1000
1001 if voucher_type == "Stock Entry":
1002 ref_item_dt = voucher_type + " Detail"
1003 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1004 ref_item_dt = voucher_type + " Item"
1005
1006 if ref_item_dt:
1007 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1008 else:
1009 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301010
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301011 def get_fallback_rate(self, sle) -> float:
1012 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301013 This should only get used for negative stock."""
1014 return get_valuation_rate(
1015 sle.item_code,
1016 sle.warehouse,
1017 sle.voucher_type,
1018 sle.voucher_no,
1019 self.allow_zero_rate,
1020 currency=erpnext.get_company_currency(sle.company),
1021 company=sle.company,
1022 batch_no=sle.batch_no,
1023 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301024
Nabin Haita77b8c92020-12-21 14:45:50 +05301025 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301026 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301027 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1028 sle = sle[0] if sle else frappe._dict()
1029 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301030
Nabin Haita77b8c92020-12-21 14:45:50 +05301031 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301032 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301033 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301034
1035 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301036 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301037 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301038 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301039
Ankush Menat494bd9e2022-03-28 18:52:46 +05301040 if (
1041 exceptions[0]["voucher_type"],
1042 exceptions[0]["voucher_no"],
1043 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301044
Nabin Haita77b8c92020-12-21 14:45:50 +05301045 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301046 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301047 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1048 frappe.get_desk_link("Warehouse", warehouse),
1049 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301050 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301051 msg = _(
1052 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1053 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301054 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301055 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1056 frappe.get_desk_link("Warehouse", warehouse),
1057 exceptions[0]["posting_date"],
1058 exceptions[0]["posting_time"],
1059 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1060 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301061
Nabin Haita77b8c92020-12-21 14:45:50 +05301062 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301063 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301064 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
1065 msg = "{0} As {1} units are reserved, you are allowed to consume only {2} units.".format(
1066 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1067 )
s-aga-rf0acb202023-04-12 14:13:54 +05301068
Nabin Haita77b8c92020-12-21 14:45:50 +05301069 msg_list.append(msg)
1070
1071 if msg_list:
1072 message = "\n\n".join(msg_list)
1073 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301074 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301075 else:
1076 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301077
Nabin Haita77b8c92020-12-21 14:45:50 +05301078 def update_bin(self):
1079 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301080 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301081 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301082
Ankush Menat494bd9e2022-03-28 18:52:46 +05301083 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301084 if data.valuation_rate is not None:
1085 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301086 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301087
marination8418c4b2021-06-22 21:35:25 +05301088
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301089def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301090 """get stock ledger entries filtered by specific posting datetime conditions"""
1091
Ankush Menat494bd9e2022-03-28 18:52:46 +05301092 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301093 if not args.get("posting_date"):
1094 args["posting_date"] = "1900-01-01"
1095 if not args.get("posting_time"):
1096 args["posting_time"] = "00:00"
1097
1098 voucher_condition = ""
1099 if exclude_current_voucher:
1100 voucher_no = args.get("voucher_no")
1101 voucher_condition = f"and voucher_no != '{voucher_no}'"
1102
Ankush Menat494bd9e2022-03-28 18:52:46 +05301103 sle = frappe.db.sql(
1104 """
marination8418c4b2021-06-22 21:35:25 +05301105 select *, timestamp(posting_date, posting_time) as "timestamp"
1106 from `tabStock Ledger Entry`
1107 where item_code = %(item_code)s
1108 and warehouse = %(warehouse)s
1109 and is_cancelled = 0
1110 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301111 and (
1112 posting_date < %(posting_date)s or
1113 (
1114 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301115 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301116 )
1117 )
marination8418c4b2021-06-22 21:35:25 +05301118 order by timestamp(posting_date, posting_time) desc, creation desc
1119 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301120 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301121 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301122 ),
1123 args,
1124 as_dict=1,
1125 )
marination8418c4b2021-06-22 21:35:25 +05301126
1127 return sle[0] if sle else frappe._dict()
1128
Ankush Menat494bd9e2022-03-28 18:52:46 +05301129
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301130def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301131 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301132 get the last sle on or before the current time-bucket,
1133 to get actual qty before transaction, this function
1134 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301135
Ankush Menat494bd9e2022-03-28 18:52:46 +05301136 args = {
1137 "item_code": "ABC",
1138 "warehouse": "XYZ",
1139 "posting_date": "2012-12-12",
1140 "posting_time": "12:00",
1141 "sle": "name of reference Stock Ledger Entry"
1142 }
Anand Doshi1b531862013-01-10 19:29:51 +05301143 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301144 args["name"] = args.get("sle", None) or ""
1145 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301146 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301147
Ankush Menat494bd9e2022-03-28 18:52:46 +05301148
1149def get_stock_ledger_entries(
1150 previous_sle,
1151 operator=None,
1152 order="desc",
1153 limit=None,
1154 for_update=False,
1155 debug=False,
1156 check_serial_no=True,
1157):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301158 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301159 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1160 operator
1161 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301162 if previous_sle.get("warehouse"):
1163 conditions += " and warehouse = %(warehouse)s"
1164 elif previous_sle.get("warehouse_condition"):
1165 conditions += " and " + previous_sle.get("warehouse_condition")
1166
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301167 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301168 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1169 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301170 conditions += (
1171 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301172 (
1173 serial_no = {0}
1174 or serial_no like {1}
1175 or serial_no like {2}
1176 or serial_no like {3}
1177 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301178 """
1179 ).format(
1180 frappe.db.escape(serial_no),
1181 frappe.db.escape("{}\n%".format(serial_no)),
1182 frappe.db.escape("%\n{}".format(serial_no)),
1183 frappe.db.escape("%\n{}\n%".format(serial_no)),
1184 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301185
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301186 if not previous_sle.get("posting_date"):
1187 previous_sle["posting_date"] = "1900-01-01"
1188 if not previous_sle.get("posting_time"):
1189 previous_sle["posting_time"] = "00:00"
1190
1191 if operator in (">", "<=") and previous_sle.get("name"):
1192 conditions += " and name!=%(name)s"
1193
Ankush Menat494bd9e2022-03-28 18:52:46 +05301194 return frappe.db.sql(
1195 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301196 select *, timestamp(posting_date, posting_time) as "timestamp"
1197 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301198 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301199 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301200 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301201 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301202 %(limit)s %(for_update)s"""
1203 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301204 "conditions": conditions,
1205 "limit": limit or "",
1206 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301207 "order": order,
1208 },
1209 previous_sle,
1210 as_dict=1,
1211 debug=debug,
1212 )
1213
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301214
Nabin Haita77b8c92020-12-21 14:45:50 +05301215def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301216 return frappe.db.get_value(
1217 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301218 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301219 [
1220 "item_code",
1221 "warehouse",
1222 "posting_date",
1223 "posting_time",
1224 "timestamp(posting_date, posting_time) as timestamp",
1225 ],
1226 as_dict=1,
1227 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301228
Ankush Menatce0514c2022-02-15 11:41:41 +05301229
Ankush Menat494bd9e2022-03-28 18:52:46 +05301230def get_batch_incoming_rate(
1231 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1232):
1233
Ankush Menat102fff22022-02-19 15:51:04 +05301234 sle = frappe.qb.DocType("Stock Ledger Entry")
1235
Ankush Menate1c16872022-04-21 20:01:48 +05301236 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301237 posting_date, posting_time
1238 )
Ankush Menat102fff22022-02-19 15:51:04 +05301239 if creation:
1240 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301241 CombineDatetime(sle.posting_date, sle.posting_time)
1242 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301243 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301244
1245 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301246 frappe.qb.from_(sle)
1247 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1248 .where(
1249 (sle.item_code == item_code)
1250 & (sle.warehouse == warehouse)
1251 & (sle.batch_no == batch_no)
1252 & (sle.is_cancelled == 0)
1253 )
1254 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301255 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301256
1257 if batch_details and batch_details[0].batch_qty:
1258 return batch_details[0].batch_value / batch_details[0].batch_qty
1259
1260
Ankush Menat494bd9e2022-03-28 18:52:46 +05301261def get_valuation_rate(
1262 item_code,
1263 warehouse,
1264 voucher_type,
1265 voucher_no,
1266 allow_zero_rate=False,
1267 currency=None,
1268 company=None,
1269 raise_error_if_no_rate=True,
1270 batch_no=None,
1271):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301272
Ankush Menatf7ffe042021-11-01 13:21:14 +05301273 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301274 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301275
Ankush Menat342d09a2022-02-19 14:28:51 +05301276 last_valuation_rate = None
1277
1278 # Get moving average rate of a specific batch number
1279 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301280 last_valuation_rate = frappe.db.sql(
1281 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301282 select sum(stock_value_difference) / sum(actual_qty)
1283 from `tabStock Ledger Entry`
1284 where
1285 item_code = %s
1286 AND warehouse = %s
1287 AND batch_no = %s
1288 AND is_cancelled = 0
1289 AND NOT (voucher_no = %s AND voucher_type = %s)
1290 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301291 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1292 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301293
Ankush Menatf7ffe042021-11-01 13:21:14 +05301294 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301295 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301296 last_valuation_rate = frappe.db.sql(
1297 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301298 from `tabStock Ledger Entry` force index (item_warehouse)
1299 where
1300 item_code = %s
1301 AND warehouse = %s
1302 AND valuation_rate >= 0
1303 AND is_cancelled = 0
1304 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301305 order by posting_date desc, posting_time desc, name desc limit 1""",
1306 (item_code, warehouse, voucher_no, voucher_type),
1307 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301308
Nabin Haita645f362018-03-01 10:31:24 +05301309 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301310 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301311
1312 # If negative stock allowed, and item delivered without any incoming entry,
1313 # system does not found any SLE, then take valuation rate from Item
1314 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301315
1316 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301317 # try Item Standard rate
1318 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301319
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301320 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301321 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301322 valuation_rate = frappe.db.get_value(
1323 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1324 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301325
Ankush Menat494bd9e2022-03-28 18:52:46 +05301326 if (
1327 not allow_zero_rate
1328 and not valuation_rate
1329 and raise_error_if_no_rate
1330 and cint(erpnext.is_perpetual_inventory_enabled(company))
1331 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301332 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301333
Ankush Menat494bd9e2022-03-28 18:52:46 +05301334 message = _(
1335 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1336 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301337 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301338 solutions = (
1339 "<li>"
1340 + _(
1341 "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."
1342 ).format(voucher_type)
1343 + "</li>"
1344 )
1345 solutions += (
1346 "<li>"
1347 + _("If not, you can Cancel / Submit this entry")
1348 + " {0} ".format(frappe.bold("after"))
1349 + _("performing either one below:")
1350 + "</li>"
1351 )
Marica97715f22020-05-11 20:45:37 +05301352 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1353 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1354 msg = message + solutions + sub_solutions + "</li>"
1355
1356 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301357
1358 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301359
Ankush Menat494bd9e2022-03-28 18:52:46 +05301360
Ankush Menate7109c12021-08-26 16:40:45 +05301361def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301362 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301363 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301364 qty_shift = args.actual_qty
1365
Ankush Menat7c839c42022-05-06 12:09:08 +05301366 args["time_format"] = "%H:%i:%s"
1367
marination8418c4b2021-06-22 21:35:25 +05301368 # find difference/shift in qty caused by stock reconciliation
1369 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301370 qty_shift = get_stock_reco_qty_shift(args)
1371
1372 # find the next nearest stock reco so that we only recalculate SLEs till that point
1373 next_stock_reco_detail = get_next_stock_reco(args)
1374 if next_stock_reco_detail:
1375 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301376 if detail.batch_no:
1377 regenerate_sle_for_batch_stock_reco(detail)
1378
marination40389772021-07-02 17:13:45 +05301379 # add condition to update SLEs before this date & time
1380 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301381
Ankush Menat494bd9e2022-03-28 18:52:46 +05301382 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301383 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301384 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301385 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301386 where
1387 item_code = %(item_code)s
1388 and warehouse = %(warehouse)s
1389 and voucher_no != %(voucher_no)s
1390 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301391 and (
1392 posting_date > %(posting_date)s or
1393 (
1394 posting_date = %(posting_date)s and
1395 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1396 )
1397 )
marination40389772021-07-02 17:13:45 +05301398 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301399 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301400 args,
1401 )
Nabin Hait186a0452021-02-18 14:14:21 +05301402
1403 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1404
Ankush Menat494bd9e2022-03-28 18:52:46 +05301405
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301406def regenerate_sle_for_batch_stock_reco(detail):
1407 doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301408 doc.recalculate_current_qty(detail.item_code, detail.batch_no)
Rohit Waghchauref2253dd2023-04-22 11:16:12 +05301409
1410 if not frappe.db.exists(
1411 "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
1412 ):
Rohit Waghchaure2d5ccc02023-05-01 21:17:18 +05301413 doc.repost_future_sle_and_gle(force=True)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301414
1415
marination40389772021-07-02 17:13:45 +05301416def get_stock_reco_qty_shift(args):
1417 stock_reco_qty_shift = 0
1418 if args.get("is_cancelled"):
1419 if args.get("previous_qty_after_transaction"):
1420 # get qty (balance) that was set at submission
1421 last_balance = args.get("previous_qty_after_transaction")
1422 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1423 else:
1424 stock_reco_qty_shift = flt(args.actual_qty)
1425 else:
1426 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301427 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301428 "qty_after_transaction"
1429 )
marination40389772021-07-02 17:13:45 +05301430
1431 if last_balance is not None:
1432 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1433 else:
1434 stock_reco_qty_shift = args.qty_after_transaction
1435
1436 return stock_reco_qty_shift
1437
Ankush Menat494bd9e2022-03-28 18:52:46 +05301438
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301439def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301440 """Returns next nearest stock reconciliaton's details."""
1441
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301442 sle = frappe.qb.DocType("Stock Ledger Entry")
1443
1444 query = (
1445 frappe.qb.from_(sle)
1446 .select(
1447 sle.name,
1448 sle.posting_date,
1449 sle.posting_time,
1450 sle.creation,
1451 sle.voucher_no,
1452 sle.item_code,
1453 sle.batch_no,
1454 sle.actual_qty,
1455 )
1456 .where(
1457 (sle.item_code == kwargs.get("item_code"))
1458 & (sle.warehouse == kwargs.get("warehouse"))
1459 & (sle.voucher_type == "Stock Reconciliation")
1460 & (sle.voucher_no != kwargs.get("voucher_no"))
1461 & (sle.is_cancelled == 0)
1462 & (
1463 (
1464 CombineDatetime(sle.posting_date, sle.posting_time)
1465 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301466 )
1467 | (
1468 (
1469 CombineDatetime(sle.posting_date, sle.posting_time)
1470 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301471 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301472 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301473 )
1474 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301475 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301476 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1477 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301478 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301479 )
1480
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301481 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301482 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301483
1484 return query.run(as_dict=True)
1485
marination40389772021-07-02 17:13:45 +05301486
1487def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301488 return f"""
1489 and
1490 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1491 or (
1492 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1493 and creation < '{detail.creation}'
1494 )
1495 )"""
1496
Ankush Menat494bd9e2022-03-28 18:52:46 +05301497
Ankush Menate7109c12021-08-26 16:40:45 +05301498def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301499 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301500 return
1501 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1502 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301503
Ankush Menat5eba5752021-12-07 23:03:52 +05301504 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301505
1506 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301507 message = _(
1508 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1509 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301510 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301511 frappe.get_desk_link("Item", args.item_code),
1512 frappe.get_desk_link("Warehouse", args.warehouse),
1513 neg_sle[0]["posting_date"],
1514 neg_sle[0]["posting_time"],
1515 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1516 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301517
Ankush Menat494bd9e2022-03-28 18:52:46 +05301518 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301519
1520 if not args.batch_no:
1521 return
1522
1523 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301524 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301525 message = _(
1526 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1527 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301528 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301529 frappe.get_desk_link("Batch", args.batch_no),
1530 frappe.get_desk_link("Warehouse", args.warehouse),
1531 neg_batch_sle[0]["posting_date"],
1532 neg_batch_sle[0]["posting_time"],
1533 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1534 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301535 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301536
Nabin Haita77b8c92020-12-21 14:45:50 +05301537
Maricad6078aa2022-06-17 15:13:13 +05301538def is_negative_with_precision(neg_sle, is_batch=False):
1539 """
1540 Returns whether system precision rounded qty is insufficient.
1541 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1542 """
1543
1544 if not neg_sle:
1545 return False
1546
1547 field = "cumulative_total" if is_batch else "qty_after_transaction"
1548 precision = cint(frappe.db.get_default("float_precision")) or 2
1549 qty_deficit = flt(neg_sle[0][field], precision)
1550
1551 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1552
1553
Nabin Haita77b8c92020-12-21 14:45:50 +05301554def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301555 return frappe.db.sql(
1556 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301557 select
1558 qty_after_transaction, posting_date, posting_time,
1559 voucher_type, voucher_no
1560 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301561 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301562 item_code = %(item_code)s
1563 and warehouse = %(warehouse)s
1564 and voucher_no != %(voucher_no)s
1565 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1566 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301567 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301568 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301569 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301570 """,
1571 args,
1572 as_dict=1,
1573 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301574
Ankush Menat5eba5752021-12-07 23:03:52 +05301575
1576def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301577 return frappe.db.sql(
1578 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301579 with batch_ledger as (
1580 select
1581 posting_date, posting_time, voucher_type, voucher_no,
1582 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1583 from `tabStock Ledger Entry`
1584 where
1585 item_code = %(item_code)s
1586 and warehouse = %(warehouse)s
1587 and batch_no=%(batch_no)s
1588 and is_cancelled = 0
1589 order by posting_date, posting_time, creation
1590 )
1591 select * from batch_ledger
1592 where
1593 cumulative_total < 0.0
1594 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1595 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301596 """,
1597 args,
1598 as_dict=1,
1599 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301600
1601
1602def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1603 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1604 return True
1605 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1606 return True
1607 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301608
1609
1610def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1611 """
1612 For inter company transfer, incoming rate is the average of the outgoing rate
1613 """
1614 rate = 0.0
1615
1616 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1617
1618 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1619
1620 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1621
1622 if reference_name:
1623 rate = frappe.get_cached_value(
1624 doctype,
1625 reference_name,
1626 "incoming_rate",
1627 )
1628
1629 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301630
1631
1632def is_internal_transfer(sle):
1633 data = frappe.get_cached_value(
1634 sle.voucher_type,
1635 sle.voucher_no,
1636 ["is_internal_supplier", "represents_company", "company"],
1637 as_dict=True,
1638 )
1639
1640 if data.is_internal_supplier and data.represents_company == data.company:
1641 return True