blob: 711694b23a23665a3e570a83637fcd2e387b4718 [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)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530450 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530451
Nabin Haita77b8c92020-12-21 14:45:50 +0530452 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530453 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530454
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530455 if self.exceptions:
456 self.raise_exceptions()
457
Nabin Hait186a0452021-02-18 14:14:21 +0530458 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530459 sl_entries = self.get_sle_against_current_voucher()
460 for sle in sl_entries:
461 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530462
Nabin Haita77b8c92020-12-21 14:45:50 +0530463 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530464 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530465
Ankush Menat494bd9e2022-03-28 18:52:46 +0530466 return frappe.db.sql(
467 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530468 select
469 *, timestamp(posting_date, posting_time) as "timestamp"
470 from
471 `tabStock Ledger Entry`
472 where
473 item_code = %(item_code)s
474 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530475 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530476 and (
477 posting_date = %(posting_date)s and
478 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
479 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530480 order by
481 creation ASC
482 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530483 """,
484 self.args,
485 as_dict=1,
486 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530487
Nabin Haita77b8c92020-12-21 14:45:50 +0530488 def get_future_entries_to_fix(self):
489 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530490 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
491 {"item_code": self.item_code, "warehouse": self.args.warehouse}
492 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530493
Nabin Haita77b8c92020-12-21 14:45:50 +0530494 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530495
Nabin Haita77b8c92020-12-21 14:45:50 +0530496 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530497 dependant_sle = get_sle_by_voucher_detail_no(
498 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
499 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530500
Nabin Haita77b8c92020-12-21 14:45:50 +0530501 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530502 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530503 elif (
504 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
505 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530506 return entries_to_fix
507 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530508 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530509 return entries_to_fix
510 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
511 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530512 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530513 self.initialize_previous_data(dependant_sle)
514 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530515 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530516
517 def update_distinct_item_warehouses(self, dependant_sle):
518 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530519 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530520 if key not in self.distinct_item_warehouses:
521 self.distinct_item_warehouses[key] = val
522 self.new_items_found = True
523 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530524 existing_sle_posting_date = (
525 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
526 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530527 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
528 val.sle_changed = True
529 self.distinct_item_warehouses[key] = val
530 self.new_items_found = True
531
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530532 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530533 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
534
Nabin Haita77b8c92020-12-21 14:45:50 +0530535 # previous sle data for this warehouse
536 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530537 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530538
Anand Doshi0dc79f42015-04-06 12:59:34 +0530539 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 +0530540 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530541 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530542 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530543 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530544 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530545
Nabin Haita77b8c92020-12-21 14:45:50 +0530546 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530547 if not self.args.get("sle_id"):
548 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530549
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530550 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530551 sle.voucher_type == "Stock Reconciliation"
552 and sle.batch_no
553 and sle.voucher_detail_no
554 and sle.actual_qty < 0
555 ):
556 self.reset_actual_qty_for_stock_reco(sle)
557
558 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530559 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
560 and sle.voucher_detail_no
561 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530562 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530563 ):
564 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
565
Ankush Menat66bf21f2022-01-16 20:45:59 +0530566 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530567 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530568 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530569 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530570 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530571
Ankush Menat494bd9e2022-03-28 18:52:46 +0530572 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
573 self.wh_data.valuation_rate
574 )
575 elif sle.batch_no and frappe.db.get_value(
576 "Batch", sle.batch_no, "use_batchwise_valuation", cache=True
577 ):
Ankush Menatce0514c2022-02-15 11:41:41 +0530578 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530579 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530580 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530581 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530582 self.wh_data.valuation_rate = sle.valuation_rate
583 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530584 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
585 self.wh_data.valuation_rate
586 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530587 if self.valuation_method != "Moving Average":
588 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530589 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530590 if self.valuation_method == "Moving Average":
591 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530592 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530593 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
594 self.wh_data.valuation_rate
595 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530596 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530597 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530598
Rushabh Mehta54047782013-12-26 11:07:46 +0530599 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530600 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530601 if not self.wh_data.qty_after_transaction:
602 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530603 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
604 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530605
Nabin Hait902e8602013-01-08 18:29:24 +0530606 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530607 sle.qty_after_transaction = self.wh_data.qty_after_transaction
608 sle.valuation_rate = self.wh_data.valuation_rate
609 sle.stock_value = self.wh_data.stock_value
610 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530611 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530612 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530613
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530614 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530615
Ankush Menat701878f2022-03-01 18:08:29 +0530616 if not self.args.get("sle_id"):
617 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530618
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530619 def reset_actual_qty_for_stock_reco(self, sle):
620 current_qty = frappe.get_cached_value(
621 "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
622 )
623
624 if current_qty:
625 sle.actual_qty = current_qty * -1
626 elif current_qty == 0:
627 sle.is_cancelled = 1
628
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530629 def validate_negative_stock(self, sle):
630 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530631 validate negative stock for entries current datetime onwards
632 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530633 """
s-aga-rf0acb202023-04-12 14:13:54 +0530634 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530635 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530636
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530637 if diff < 0 and abs(diff) > 0.0001:
638 # negative stock!
639 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530640 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530641 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530642 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530643 return True
644
Nabin Haita77b8c92020-12-21 14:45:50 +0530645 def get_dynamic_incoming_outgoing_rate(self, sle):
646 # Get updated incoming/outgoing rate from transaction
647 if sle.recalculate_rate:
648 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
649
650 if flt(sle.actual_qty) >= 0:
651 sle.incoming_rate = rate
652 else:
653 sle.outgoing_rate = rate
654
655 def get_incoming_outgoing_rate_from_transaction(self, sle):
656 rate = 0
657 # Material Transfer, Repack, Manufacturing
658 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530659 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530660 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
661 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530662 elif sle.voucher_type in (
663 "Purchase Receipt",
664 "Purchase Invoice",
665 "Delivery Note",
666 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530667 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530668 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530669 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530670 from erpnext.controllers.sales_and_purchase_return import (
671 get_rate_for_return, # don't move this import to top
672 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530673
674 rate = get_rate_for_return(
675 sle.voucher_type,
676 sle.voucher_no,
677 sle.item_code,
678 voucher_detail_no=sle.voucher_detail_no,
679 sle=sle,
680 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530681
682 elif (
683 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530684 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530685 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530686 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530687 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530688 else:
689 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530690 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530691 elif sle.voucher_type == "Subcontracting Receipt":
692 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530693 else:
694 rate_field = "incoming_rate"
695
696 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530697 item_code, incoming_rate = frappe.db.get_value(
698 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
699 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530700
701 if item_code == sle.item_code:
702 rate = incoming_rate
703 else:
704 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
705 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530706 elif sle == "Subcontracting Receipt":
707 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530708 else:
709 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530710
Ankush Menat494bd9e2022-03-28 18:52:46 +0530711 rate = frappe.db.get_value(
712 ref_doctype,
713 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
714 rate_field,
715 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530716
717 return rate
718
719 def update_outgoing_rate_on_transaction(self, sle):
720 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530721 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
722 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530723 """
724 if sle.actual_qty and sle.voucher_detail_no:
725 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
726
727 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
728 self.update_rate_on_stock_entry(sle, outgoing_rate)
729 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
730 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
731 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
732 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530733 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
734 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
Nabin Haita77b8c92020-12-21 14:45:50 +0530735
736 def update_rate_on_stock_entry(self, sle, outgoing_rate):
737 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
738
Ankush Menat701878f2022-03-01 18:08:29 +0530739 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
740 if not sle.dependant_sle_voucher_detail_no:
741 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530742
743 def recalculate_amounts_in_stock_entry(self, voucher_no):
744 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530745 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
746 stock_entry.db_update()
747 for d in stock_entry.items:
748 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530749
Nabin Haita77b8c92020-12-21 14:45:50 +0530750 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
751 # Update item's incoming rate on transaction
752 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
753 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530754 frappe.db.set_value(
755 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
756 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530757 else:
758 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530759 frappe.db.set_value(
760 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530761 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530762 "incoming_rate",
763 outgoing_rate,
764 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530765
766 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
767 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530768 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
769 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
770 ):
771 frappe.db.set_value(
772 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
773 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530774 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530775 frappe.db.set_value(
776 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
777 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530778
779 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530780 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530781 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530782 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530783 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530784 d.db_update()
785
Sagar Sharma323bdf82022-05-17 15:14:07 +0530786 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530787 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
788 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530789 else:
790 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530791 "Subcontracting Receipt Supplied Item",
792 sle.voucher_detail_no,
793 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530794 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530795
s-aga-ra6cb6c62023-05-03 09:51:58 +0530796 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530797 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530798 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530799 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530800 d.db_update()
801
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530802 def get_serialized_values(self, sle):
803 incoming_rate = flt(sle.incoming_rate)
804 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530805 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530806
807 if incoming_rate < 0:
808 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530809 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530810
Nabin Hait2620bf42016-02-29 11:30:27 +0530811 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530812 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530813 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530814 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530815 # In case of delivery/stock issue, get average purchase rate
816 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530817 if not sle.is_cancelled:
818 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
819 stock_value_change = -1 * outgoing_value
820 else:
821 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530822
Nabin Haita77b8c92020-12-21 14:45:50 +0530823 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530824
Nabin Hait2620bf42016-02-29 11:30:27 +0530825 if new_stock_qty > 0:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530826 new_stock_value = (
827 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
828 ) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530829 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530830 # calculate new valuation rate only if stock value is positive
831 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530832 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530833
Nabin Haita77b8c92020-12-21 14:45:50 +0530834 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530835 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
836 sle.voucher_type, sle.voucher_detail_no
837 )
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530838 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530839 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530840
Nabin Hait328c4f92020-01-02 19:00:32 +0530841 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
842 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530843 all_serial_nos = frappe.get_all(
844 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
845 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530846
Ankush Menat494bd9e2022-03-28 18:52:46 +0530847 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 +0530848
849 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530850 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530851 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530852 incoming_rate = frappe.db.sql(
853 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530854 select incoming_rate
855 from `tabStock Ledger Entry`
856 where
857 company = %s
858 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530859 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530860 and (serial_no = %s
861 or serial_no like %s
862 or serial_no like %s
863 or serial_no like %s
864 )
865 order by posting_date desc
866 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530867 """,
868 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
869 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530870
871 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
872
873 return incoming_values
874
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530875 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530876 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530877 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530878 if new_stock_qty >= 0:
879 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530880 if flt(self.wh_data.qty_after_transaction) <= 0:
881 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530882 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530883 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
884 actual_qty * sle.incoming_rate
885 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530886
Nabin Haita77b8c92020-12-21 14:45:50 +0530887 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530888
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530889 elif sle.outgoing_rate:
890 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530891 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
892 actual_qty * sle.outgoing_rate
893 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530894
Nabin Haita77b8c92020-12-21 14:45:50 +0530895 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530896 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530897 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530898 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530899 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
900 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530901
Nabin Haita77b8c92020-12-21 14:45:50 +0530902 if not self.wh_data.valuation_rate and actual_qty > 0:
903 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530904
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530905 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800906 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530907 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530908 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
909 sle.voucher_type, sle.voucher_detail_no
910 )
Javier Wong9b11d9b2017-04-14 18:24:04 +0800911 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530912 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530913
Ankush Menatf089d392022-02-02 12:51:21 +0530914 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530915 incoming_rate = flt(sle.incoming_rate)
916 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530917 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530918
Ankush Menat494bd9e2022-03-28 18:52:46 +0530919 self.wh_data.qty_after_transaction = round_off_if_near_zero(
920 self.wh_data.qty_after_transaction + actual_qty
921 )
Ankush Menatb534fee2022-02-19 20:58:36 +0530922
Ankush Menat97e18a12022-01-15 17:42:25 +0530923 if self.valuation_method == "LIFO":
924 stock_queue = LIFOValuation(self.wh_data.stock_queue)
925 else:
926 stock_queue = FIFOValuation(self.wh_data.stock_queue)
927
Ankush Menatb534fee2022-02-19 20:58:36 +0530928 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
929
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530930 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530931 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530932 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530933
Ankush Menat4b29fb62021-12-18 18:40:22 +0530934 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530935 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
936 sle.voucher_type, sle.voucher_detail_no
937 )
Ankush Menat4b29fb62021-12-18 18:40:22 +0530938 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530939 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530940 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530941 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530942
Ankush Menat494bd9e2022-03-28 18:52:46 +0530943 stock_queue.remove_stock(
944 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
945 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530946
Ankush Menatb534fee2022-02-19 20:58:36 +0530947 _qty, stock_value = stock_queue.get_total_stock_and_value()
948
949 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530950
Ankush Menat97e18a12022-01-15 17:42:25 +0530951 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +0530952 self.wh_data.stock_value = round_off_if_near_zero(
953 self.wh_data.stock_value + stock_value_difference
954 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530955
Nabin Haita77b8c92020-12-21 14:45:50 +0530956 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530957 self.wh_data.stock_queue.append(
958 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
959 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530960
Ankush Menatb534fee2022-02-19 20:58:36 +0530961 if self.wh_data.qty_after_transaction:
962 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
963
Ankush Menatce0514c2022-02-15 11:41:41 +0530964 def update_batched_values(self, sle):
965 incoming_rate = flt(sle.incoming_rate)
966 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530967
Ankush Menat494bd9e2022-03-28 18:52:46 +0530968 self.wh_data.qty_after_transaction = round_off_if_near_zero(
969 self.wh_data.qty_after_transaction + actual_qty
970 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530971
972 if actual_qty > 0:
973 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530974 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530975 outgoing_rate = get_batch_incoming_rate(
976 item_code=sle.item_code,
977 warehouse=sle.warehouse,
978 batch_no=sle.batch_no,
979 posting_date=sle.posting_date,
980 posting_time=sle.posting_time,
981 creation=sle.creation,
982 )
Ankush Menataba7a7c2022-02-19 19:36:28 +0530983 if outgoing_rate is None:
984 # This can *only* happen if qty available for the batch is zero.
985 # in such case fall back various other rates.
986 # future entries will correct the overall accounting as each
987 # batch individually uses moving average rates.
988 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530989 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530990
Ankush Menat494bd9e2022-03-28 18:52:46 +0530991 self.wh_data.stock_value = round_off_if_near_zero(
992 self.wh_data.stock_value + stock_value_difference
993 )
Ankush Menatce0514c2022-02-15 11:41:41 +0530994 if self.wh_data.qty_after_transaction:
995 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530996
Javier Wong9b11d9b2017-04-14 18:24:04 +0800997 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530998 ref_item_dt = ""
999
1000 if voucher_type == "Stock Entry":
1001 ref_item_dt = voucher_type + " Detail"
1002 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1003 ref_item_dt = voucher_type + " Item"
1004
1005 if ref_item_dt:
1006 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1007 else:
1008 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301009
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301010 def get_fallback_rate(self, sle) -> float:
1011 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301012 This should only get used for negative stock."""
1013 return get_valuation_rate(
1014 sle.item_code,
1015 sle.warehouse,
1016 sle.voucher_type,
1017 sle.voucher_no,
1018 self.allow_zero_rate,
1019 currency=erpnext.get_company_currency(sle.company),
1020 company=sle.company,
1021 batch_no=sle.batch_no,
1022 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301023
Nabin Haita77b8c92020-12-21 14:45:50 +05301024 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301025 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301026 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1027 sle = sle[0] if sle else frappe._dict()
1028 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301029
Nabin Haita77b8c92020-12-21 14:45:50 +05301030 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301031 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301032 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301033
1034 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301035 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301036 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301037 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301038
Ankush Menat494bd9e2022-03-28 18:52:46 +05301039 if (
1040 exceptions[0]["voucher_type"],
1041 exceptions[0]["voucher_no"],
1042 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301043
Nabin Haita77b8c92020-12-21 14:45:50 +05301044 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301045 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301046 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1047 frappe.get_desk_link("Warehouse", warehouse),
1048 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301049 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301050 msg = _(
1051 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1052 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301053 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301054 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1055 frappe.get_desk_link("Warehouse", warehouse),
1056 exceptions[0]["posting_date"],
1057 exceptions[0]["posting_time"],
1058 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1059 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301060
Nabin Haita77b8c92020-12-21 14:45:50 +05301061 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301062 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301063 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
1064 msg = "{0} As {1} units are reserved, you are allowed to consume only {2} units.".format(
1065 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1066 )
s-aga-rf0acb202023-04-12 14:13:54 +05301067
Nabin Haita77b8c92020-12-21 14:45:50 +05301068 msg_list.append(msg)
1069
1070 if msg_list:
1071 message = "\n\n".join(msg_list)
1072 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301073 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301074 else:
1075 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301076
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301077 def update_bin_data(self, sle):
1078 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301079 values_to_update = {
1080 "actual_qty": sle.qty_after_transaction,
1081 "stock_value": sle.stock_value,
1082 }
1083
1084 if sle.valuation_rate is not None:
1085 values_to_update["valuation_rate"] = sle.valuation_rate
1086
1087 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301088
Nabin Haita77b8c92020-12-21 14:45:50 +05301089 def update_bin(self):
1090 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301091 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301092 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301093
Ankush Menat494bd9e2022-03-28 18:52:46 +05301094 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301095 if data.valuation_rate is not None:
1096 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301097 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301098
marination8418c4b2021-06-22 21:35:25 +05301099
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301100def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301101 """get stock ledger entries filtered by specific posting datetime conditions"""
1102
Ankush Menat494bd9e2022-03-28 18:52:46 +05301103 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301104 if not args.get("posting_date"):
1105 args["posting_date"] = "1900-01-01"
1106 if not args.get("posting_time"):
1107 args["posting_time"] = "00:00"
1108
1109 voucher_condition = ""
1110 if exclude_current_voucher:
1111 voucher_no = args.get("voucher_no")
1112 voucher_condition = f"and voucher_no != '{voucher_no}'"
1113
Ankush Menat494bd9e2022-03-28 18:52:46 +05301114 sle = frappe.db.sql(
1115 """
marination8418c4b2021-06-22 21:35:25 +05301116 select *, timestamp(posting_date, posting_time) as "timestamp"
1117 from `tabStock Ledger Entry`
1118 where item_code = %(item_code)s
1119 and warehouse = %(warehouse)s
1120 and is_cancelled = 0
1121 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301122 and (
1123 posting_date < %(posting_date)s or
1124 (
1125 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301126 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301127 )
1128 )
marination8418c4b2021-06-22 21:35:25 +05301129 order by timestamp(posting_date, posting_time) desc, creation desc
1130 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301131 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301132 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301133 ),
1134 args,
1135 as_dict=1,
1136 )
marination8418c4b2021-06-22 21:35:25 +05301137
1138 return sle[0] if sle else frappe._dict()
1139
Ankush Menat494bd9e2022-03-28 18:52:46 +05301140
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301141def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301142 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301143 get the last sle on or before the current time-bucket,
1144 to get actual qty before transaction, this function
1145 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301146
Ankush Menat494bd9e2022-03-28 18:52:46 +05301147 args = {
1148 "item_code": "ABC",
1149 "warehouse": "XYZ",
1150 "posting_date": "2012-12-12",
1151 "posting_time": "12:00",
1152 "sle": "name of reference Stock Ledger Entry"
1153 }
Anand Doshi1b531862013-01-10 19:29:51 +05301154 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301155 args["name"] = args.get("sle", None) or ""
1156 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301157 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301158
Ankush Menat494bd9e2022-03-28 18:52:46 +05301159
1160def get_stock_ledger_entries(
1161 previous_sle,
1162 operator=None,
1163 order="desc",
1164 limit=None,
1165 for_update=False,
1166 debug=False,
1167 check_serial_no=True,
1168):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301169 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301170 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1171 operator
1172 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301173 if previous_sle.get("warehouse"):
1174 conditions += " and warehouse = %(warehouse)s"
1175 elif previous_sle.get("warehouse_condition"):
1176 conditions += " and " + previous_sle.get("warehouse_condition")
1177
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301178 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301179 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1180 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301181 conditions += (
1182 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301183 (
1184 serial_no = {0}
1185 or serial_no like {1}
1186 or serial_no like {2}
1187 or serial_no like {3}
1188 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301189 """
1190 ).format(
1191 frappe.db.escape(serial_no),
1192 frappe.db.escape("{}\n%".format(serial_no)),
1193 frappe.db.escape("%\n{}".format(serial_no)),
1194 frappe.db.escape("%\n{}\n%".format(serial_no)),
1195 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301196
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301197 if not previous_sle.get("posting_date"):
1198 previous_sle["posting_date"] = "1900-01-01"
1199 if not previous_sle.get("posting_time"):
1200 previous_sle["posting_time"] = "00:00"
1201
1202 if operator in (">", "<=") and previous_sle.get("name"):
1203 conditions += " and name!=%(name)s"
1204
Ankush Menat494bd9e2022-03-28 18:52:46 +05301205 return frappe.db.sql(
1206 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301207 select *, timestamp(posting_date, posting_time) as "timestamp"
1208 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301209 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301210 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301211 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301212 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301213 %(limit)s %(for_update)s"""
1214 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301215 "conditions": conditions,
1216 "limit": limit or "",
1217 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301218 "order": order,
1219 },
1220 previous_sle,
1221 as_dict=1,
1222 debug=debug,
1223 )
1224
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301225
Nabin Haita77b8c92020-12-21 14:45:50 +05301226def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301227 return frappe.db.get_value(
1228 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301229 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301230 [
1231 "item_code",
1232 "warehouse",
1233 "posting_date",
1234 "posting_time",
1235 "timestamp(posting_date, posting_time) as timestamp",
1236 ],
1237 as_dict=1,
1238 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301239
Ankush Menatce0514c2022-02-15 11:41:41 +05301240
Ankush Menat494bd9e2022-03-28 18:52:46 +05301241def get_batch_incoming_rate(
1242 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
1243):
1244
Ankush Menat102fff22022-02-19 15:51:04 +05301245 sle = frappe.qb.DocType("Stock Ledger Entry")
1246
Ankush Menate1c16872022-04-21 20:01:48 +05301247 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301248 posting_date, posting_time
1249 )
Ankush Menat102fff22022-02-19 15:51:04 +05301250 if creation:
1251 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301252 CombineDatetime(sle.posting_date, sle.posting_time)
1253 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301254 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301255
1256 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301257 frappe.qb.from_(sle)
1258 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
1259 .where(
1260 (sle.item_code == item_code)
1261 & (sle.warehouse == warehouse)
1262 & (sle.batch_no == batch_no)
1263 & (sle.is_cancelled == 0)
1264 )
1265 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301266 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301267
1268 if batch_details and batch_details[0].batch_qty:
1269 return batch_details[0].batch_value / batch_details[0].batch_qty
1270
1271
Ankush Menat494bd9e2022-03-28 18:52:46 +05301272def get_valuation_rate(
1273 item_code,
1274 warehouse,
1275 voucher_type,
1276 voucher_no,
1277 allow_zero_rate=False,
1278 currency=None,
1279 company=None,
1280 raise_error_if_no_rate=True,
1281 batch_no=None,
1282):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301283
Ankush Menatf7ffe042021-11-01 13:21:14 +05301284 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301285 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301286
Ankush Menat342d09a2022-02-19 14:28:51 +05301287 last_valuation_rate = None
1288
1289 # Get moving average rate of a specific batch number
1290 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301291 last_valuation_rate = frappe.db.sql(
1292 """
Ankush Menat342d09a2022-02-19 14:28:51 +05301293 select sum(stock_value_difference) / sum(actual_qty)
1294 from `tabStock Ledger Entry`
1295 where
1296 item_code = %s
1297 AND warehouse = %s
1298 AND batch_no = %s
1299 AND is_cancelled = 0
1300 AND NOT (voucher_no = %s AND voucher_type = %s)
1301 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301302 (item_code, warehouse, batch_no, voucher_no, voucher_type),
1303 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301304
Ankush Menatf7ffe042021-11-01 13:21:14 +05301305 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301306 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301307 last_valuation_rate = frappe.db.sql(
1308 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301309 from `tabStock Ledger Entry` force index (item_warehouse)
1310 where
1311 item_code = %s
1312 AND warehouse = %s
1313 AND valuation_rate >= 0
1314 AND is_cancelled = 0
1315 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301316 order by posting_date desc, posting_time desc, name desc limit 1""",
1317 (item_code, warehouse, voucher_no, voucher_type),
1318 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301319
Nabin Haita645f362018-03-01 10:31:24 +05301320 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301321 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301322
1323 # If negative stock allowed, and item delivered without any incoming entry,
1324 # system does not found any SLE, then take valuation rate from Item
1325 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301326
1327 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301328 # try Item Standard rate
1329 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301330
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301331 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301332 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301333 valuation_rate = frappe.db.get_value(
1334 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1335 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301336
Ankush Menat494bd9e2022-03-28 18:52:46 +05301337 if (
1338 not allow_zero_rate
1339 and not valuation_rate
1340 and raise_error_if_no_rate
1341 and cint(erpnext.is_perpetual_inventory_enabled(company))
1342 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301343 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301344
Ankush Menat494bd9e2022-03-28 18:52:46 +05301345 message = _(
1346 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1347 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301348 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301349 solutions = (
1350 "<li>"
1351 + _(
1352 "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."
1353 ).format(voucher_type)
1354 + "</li>"
1355 )
1356 solutions += (
1357 "<li>"
1358 + _("If not, you can Cancel / Submit this entry")
1359 + " {0} ".format(frappe.bold("after"))
1360 + _("performing either one below:")
1361 + "</li>"
1362 )
Marica97715f22020-05-11 20:45:37 +05301363 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1364 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1365 msg = message + solutions + sub_solutions + "</li>"
1366
1367 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301368
1369 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301370
Ankush Menat494bd9e2022-03-28 18:52:46 +05301371
Ankush Menate7109c12021-08-26 16:40:45 +05301372def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301373 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301374 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301375 qty_shift = args.actual_qty
1376
Ankush Menat7c839c42022-05-06 12:09:08 +05301377 args["time_format"] = "%H:%i:%s"
1378
marination8418c4b2021-06-22 21:35:25 +05301379 # find difference/shift in qty caused by stock reconciliation
1380 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301381 qty_shift = get_stock_reco_qty_shift(args)
1382
1383 # find the next nearest stock reco so that we only recalculate SLEs till that point
1384 next_stock_reco_detail = get_next_stock_reco(args)
1385 if next_stock_reco_detail:
1386 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301387 if detail.batch_no:
1388 regenerate_sle_for_batch_stock_reco(detail)
1389
marination40389772021-07-02 17:13:45 +05301390 # add condition to update SLEs before this date & time
1391 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301392
Ankush Menat494bd9e2022-03-28 18:52:46 +05301393 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301394 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301395 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301396 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301397 where
1398 item_code = %(item_code)s
1399 and warehouse = %(warehouse)s
1400 and voucher_no != %(voucher_no)s
1401 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301402 and (
1403 posting_date > %(posting_date)s or
1404 (
1405 posting_date = %(posting_date)s and
1406 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1407 )
1408 )
marination40389772021-07-02 17:13:45 +05301409 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301410 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301411 args,
1412 )
Nabin Hait186a0452021-02-18 14:14:21 +05301413
1414 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1415
Ankush Menat494bd9e2022-03-28 18:52:46 +05301416
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301417def regenerate_sle_for_batch_stock_reco(detail):
1418 doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301419 doc.recalculate_current_qty(detail.item_code, detail.batch_no)
Rohit Waghchauref2253dd2023-04-22 11:16:12 +05301420
1421 if not frappe.db.exists(
1422 "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
1423 ):
Rohit Waghchaure2d5ccc02023-05-01 21:17:18 +05301424 doc.repost_future_sle_and_gle(force=True)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301425
1426
marination40389772021-07-02 17:13:45 +05301427def get_stock_reco_qty_shift(args):
1428 stock_reco_qty_shift = 0
1429 if args.get("is_cancelled"):
1430 if args.get("previous_qty_after_transaction"):
1431 # get qty (balance) that was set at submission
1432 last_balance = args.get("previous_qty_after_transaction")
1433 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1434 else:
1435 stock_reco_qty_shift = flt(args.actual_qty)
1436 else:
1437 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301438 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301439 "qty_after_transaction"
1440 )
marination40389772021-07-02 17:13:45 +05301441
1442 if last_balance is not None:
1443 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1444 else:
1445 stock_reco_qty_shift = args.qty_after_transaction
1446
1447 return stock_reco_qty_shift
1448
Ankush Menat494bd9e2022-03-28 18:52:46 +05301449
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301450def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301451 """Returns next nearest stock reconciliaton's details."""
1452
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301453 sle = frappe.qb.DocType("Stock Ledger Entry")
1454
1455 query = (
1456 frappe.qb.from_(sle)
1457 .select(
1458 sle.name,
1459 sle.posting_date,
1460 sle.posting_time,
1461 sle.creation,
1462 sle.voucher_no,
1463 sle.item_code,
1464 sle.batch_no,
1465 sle.actual_qty,
1466 )
1467 .where(
1468 (sle.item_code == kwargs.get("item_code"))
1469 & (sle.warehouse == kwargs.get("warehouse"))
1470 & (sle.voucher_type == "Stock Reconciliation")
1471 & (sle.voucher_no != kwargs.get("voucher_no"))
1472 & (sle.is_cancelled == 0)
1473 & (
1474 (
1475 CombineDatetime(sle.posting_date, sle.posting_time)
1476 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301477 )
1478 | (
1479 (
1480 CombineDatetime(sle.posting_date, sle.posting_time)
1481 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301482 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301483 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301484 )
1485 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301486 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301487 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1488 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301489 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301490 )
1491
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301492 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301493 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301494
1495 return query.run(as_dict=True)
1496
marination40389772021-07-02 17:13:45 +05301497
1498def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301499 return f"""
1500 and
1501 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1502 or (
1503 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1504 and creation < '{detail.creation}'
1505 )
1506 )"""
1507
Ankush Menat494bd9e2022-03-28 18:52:46 +05301508
Ankush Menate7109c12021-08-26 16:40:45 +05301509def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301510 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301511 return
1512 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1513 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301514
Ankush Menat5eba5752021-12-07 23:03:52 +05301515 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301516
1517 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301518 message = _(
1519 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1520 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301521 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301522 frappe.get_desk_link("Item", args.item_code),
1523 frappe.get_desk_link("Warehouse", args.warehouse),
1524 neg_sle[0]["posting_date"],
1525 neg_sle[0]["posting_time"],
1526 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1527 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301528
Ankush Menat494bd9e2022-03-28 18:52:46 +05301529 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301530
1531 if not args.batch_no:
1532 return
1533
1534 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301535 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301536 message = _(
1537 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1538 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301539 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301540 frappe.get_desk_link("Batch", args.batch_no),
1541 frappe.get_desk_link("Warehouse", args.warehouse),
1542 neg_batch_sle[0]["posting_date"],
1543 neg_batch_sle[0]["posting_time"],
1544 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1545 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301546 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301547
Nabin Haita77b8c92020-12-21 14:45:50 +05301548
Maricad6078aa2022-06-17 15:13:13 +05301549def is_negative_with_precision(neg_sle, is_batch=False):
1550 """
1551 Returns whether system precision rounded qty is insufficient.
1552 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1553 """
1554
1555 if not neg_sle:
1556 return False
1557
1558 field = "cumulative_total" if is_batch else "qty_after_transaction"
1559 precision = cint(frappe.db.get_default("float_precision")) or 2
1560 qty_deficit = flt(neg_sle[0][field], precision)
1561
1562 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1563
1564
Nabin Haita77b8c92020-12-21 14:45:50 +05301565def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301566 return frappe.db.sql(
1567 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301568 select
1569 qty_after_transaction, posting_date, posting_time,
1570 voucher_type, voucher_no
1571 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301572 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301573 item_code = %(item_code)s
1574 and warehouse = %(warehouse)s
1575 and voucher_no != %(voucher_no)s
1576 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1577 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301578 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301579 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301580 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301581 """,
1582 args,
1583 as_dict=1,
1584 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301585
Ankush Menat5eba5752021-12-07 23:03:52 +05301586
1587def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301588 return frappe.db.sql(
1589 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301590 with batch_ledger as (
1591 select
1592 posting_date, posting_time, voucher_type, voucher_no,
1593 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1594 from `tabStock Ledger Entry`
1595 where
1596 item_code = %(item_code)s
1597 and warehouse = %(warehouse)s
1598 and batch_no=%(batch_no)s
1599 and is_cancelled = 0
1600 order by posting_date, posting_time, creation
1601 )
1602 select * from batch_ledger
1603 where
1604 cumulative_total < 0.0
1605 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1606 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301607 """,
1608 args,
1609 as_dict=1,
1610 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301611
1612
1613def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1614 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1615 return True
1616 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1617 return True
1618 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301619
1620
1621def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1622 """
1623 For inter company transfer, incoming rate is the average of the outgoing rate
1624 """
1625 rate = 0.0
1626
1627 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1628
1629 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1630
1631 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1632
1633 if reference_name:
1634 rate = frappe.get_cached_value(
1635 doctype,
1636 reference_name,
1637 "incoming_rate",
1638 )
1639
1640 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301641
1642
1643def is_internal_transfer(sle):
1644 data = frappe.get_cached_value(
1645 sle.voucher_type,
1646 sle.voucher_no,
1647 ["is_internal_supplier", "represents_company", "company"],
1648 as_dict=True,
1649 )
1650
1651 if data.is_internal_supplier and data.represents_company == data.company:
1652 return True