blob: 2b62baf42b3d9fa027241dabc26a84a8d8107bf2 [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
Akhil Narang21c3d9c2023-10-21 11:19:45 +05305import gzip
Nabin Hait26d46552013-01-09 15:23:05 +05306import json
Ankush Menatecdb4932022-04-17 19:06:13 +05307from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05308
9import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053010from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053011from frappe.model.meta import get_field_precision
Rohit Waghchaured80ca522024-02-07 21:56:21 +053012from frappe.query_builder.functions import Sum
Rohit Waghchaure64cb1152024-01-15 19:39:41 +053013from frappe.utils import (
14 cint,
15 cstr,
16 flt,
17 get_link_to_form,
18 getdate,
19 now,
20 nowdate,
21 nowtime,
22 parse_json,
23)
Achilles Rasquinha361366e2018-02-14 17:08:59 +053024
Chillar Anand915b3432021-09-02 16:44:59 +053025import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053026from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
mergify[bot]27a1e3b2023-10-16 19:15:18 +053027from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
s-aga-re1a87a82023-10-31 18:41:58 +053028from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
29 get_available_batches,
30)
31from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
32 get_sre_reserved_batch_nos_details,
s-aga-rd9e28432023-10-27 16:35:35 +053033 get_sre_reserved_serial_nos_details,
34)
Chillar Anand915b3432021-09-02 16:44:59 +053035from erpnext.stock.utils import (
Rohit Waghchaured80ca522024-02-07 21:56:21 +053036 get_combine_datetime,
Chillar Anand915b3432021-09-02 16:44:59 +053037 get_incoming_outgoing_rate_for_cancel,
mergify[bot]07175362023-12-21 14:40:52 +053038 get_incoming_rate,
Deepesh Garg6f107da2021-10-12 20:15:55 +053039 get_or_make_bin,
s-aga-r73b65ac2023-11-01 18:35:07 +053040 get_stock_balance,
Chillar Anand915b3432021-09-02 16:44:59 +053041 get_valuation_method,
42)
Ankush Menatb534fee2022-02-19 20:58:36 +053043from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053044
Nabin Hait97bce3a2021-07-12 13:24:43 +053045
Ankush Menat494bd9e2022-03-28 18:52:46 +053046class NegativeStockError(frappe.ValidationError):
47 pass
48
49
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053050class SerialNoExistsInFutureTransaction(frappe.ValidationError):
51 pass
Nabin Hait902e8602013-01-08 18:29:24 +053052
Anand Doshi5b004ff2013-09-25 19:55:41 +053053
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053054def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053055 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053056
Ankush Menat494bd9e2022-03-28 18:52:46 +053057 args:
58 - allow_negative_stock: disable negative stock valiations if true
59 - via_landed_cost_voucher: landed cost voucher cancels and reposts
60 entries of purchase document. This flag is used to identify if
61 cancellation and repost is happening via landed cost voucher, in
62 such cases certain validations need to be ignored (like negative
63 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053064 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053065 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053066
Nabin Haitca775742013-09-26 16:16:44 +053067 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053068 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053069 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053070 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053071 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053072
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053073 args = get_args_for_future_sle(sl_entries[0])
74 future_sle_exists(args, sl_entries)
75
Nabin Haitca775742013-09-26 16:16:44 +053076 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053077 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053078 validate_serial_no(sle)
79
Nabin Haita77b8c92020-12-21 14:45:50 +053080 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053081 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053082
Ankush Menat494bd9e2022-03-28 18:52:46 +053083 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
84 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
85 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
86 )
87 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053088
Ankush Menat494bd9e2022-03-28 18:52:46 +053089 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
90 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
91 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
92 )
93 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053094
Ankush Menat494bd9e2022-03-28 18:52:46 +053095 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053096 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053097
Nabin Haita77b8c92020-12-21 14:45:50 +053098 args = sle_doc.as_dict()
Rohit Waghchaured80ca522024-02-07 21:56:21 +053099 args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time)
marination40389772021-07-02 17:13:45 +0530100
101 if sle.get("voucher_type") == "Stock Reconciliation":
102 # preserve previous_qty_after_transaction for qty reposting
103 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
104
Ankush Menat494bd9e2022-03-28 18:52:46 +0530105 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +0530106 if is_stock_item:
107 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
s-aga-r73b65ac2023-11-01 18:35:07 +0530108 args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
Ankush Menatcef84c22021-12-03 12:18:59 +0530109 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +0530110 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +0530111 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530112 frappe.msgprint(
113 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
114 )
115
Ankush Menatcef84c22021-12-03 12:18:59 +0530116
117def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
118 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
119 if not args.get("posting_date"):
120 args["posting_date"] = nowdate()
121
marination7a5fd712022-07-04 17:46:54 +0530122 if not (args.get("is_cancelled") and via_landed_cost_voucher):
123 # Reposts only current voucher SL Entries
124 # Updates valuation rate, stock value, stock queue for current transaction
125 update_entries_after(
126 {
127 "item_code": args.get("item_code"),
128 "warehouse": args.get("warehouse"),
129 "posting_date": args.get("posting_date"),
130 "posting_time": args.get("posting_time"),
131 "voucher_type": args.get("voucher_type"),
132 "voucher_no": args.get("voucher_no"),
133 "sle_id": args.get("name"),
134 "creation": args.get("creation"),
s-aga-r73b65ac2023-11-01 18:35:07 +0530135 "reserved_stock": args.get("reserved_stock"),
marination7a5fd712022-07-04 17:46:54 +0530136 },
137 allow_negative_stock=allow_negative_stock,
138 via_landed_cost_voucher=via_landed_cost_voucher,
139 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530140
141 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530142 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530143 update_qty_in_future_sle(args, allow_negative_stock)
144
Nabin Haitadeb9762014-10-06 11:53:52 +0530145
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530146def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530147 return frappe._dict(
148 {
149 "voucher_type": row.get("voucher_type"),
150 "voucher_no": row.get("voucher_no"),
151 "posting_date": row.get("posting_date"),
152 "posting_time": row.get("posting_time"),
153 }
154 )
155
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530156
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530157def validate_serial_no(sle):
158 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530159
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530160 for sn in get_serial_nos(sle.serial_no):
161 args = copy.deepcopy(sle)
162 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530164
165 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530166 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530167 voucher_type = frappe.bold(row.voucher_type)
168 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530169 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530170
171 if vouchers:
172 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530173 msg = (
174 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
175 The list of the transactions are as below."""
176 + "<br><br><ul><li>"
177 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530178
Ankush Menat494bd9e2022-03-28 18:52:46 +0530179 msg += "</li><li>".join(vouchers)
180 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530181
Ankush Menat494bd9e2022-03-28 18:52:46 +0530182 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530183 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
184
Ankush Menat494bd9e2022-03-28 18:52:46 +0530185
Nabin Hait186a0452021-02-18 14:14:21 +0530186def validate_cancellation(args):
187 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530188 repost_entry = frappe.db.get_value(
189 "Repost Item Valuation",
190 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
191 ["name", "status"],
192 as_dict=1,
193 )
Nabin Hait186a0452021-02-18 14:14:21 +0530194
195 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530196 if repost_entry.status == "In Progress":
197 frappe.throw(
198 _(
199 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
200 )
201 )
202 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530203 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530204 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530205 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530206 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530207
Ankush Menat494bd9e2022-03-28 18:52:46 +0530208
Nabin Hait9653f602013-08-20 15:37:33 +0530209def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530210 frappe.db.sql(
211 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530212 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530213 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530214 (now(), frappe.session.user, voucher_type, voucher_no),
215 )
216
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530217
Nabin Hait54c865e2015-03-27 15:38:31 +0530218def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530219 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530220 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530221 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530222 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530223 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530224 sle.submit()
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530225
226 # Added to handle the case when the stock ledger entry is created from the repostig
227 if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
228 sle.db_set("creation", args.get("creation_time"))
229
Nabin Haita77b8c92020-12-21 14:45:50 +0530230 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530231
Ankush Menat494bd9e2022-03-28 18:52:46 +0530232
233def repost_future_sle(
234 args=None,
235 voucher_type=None,
236 voucher_no=None,
237 allow_negative_stock=None,
238 via_landed_cost_voucher=False,
239 doc=None,
240):
Nabin Haite1fa7232022-07-20 15:19:09 +0530241 if not args:
242 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530243
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530244 reposting_data = {}
245 if doc and doc.reposting_data_file:
246 reposting_data = get_reposting_data(doc.reposting_data_file)
247
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530248 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530249 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530250 )
251 if items_to_be_repost:
252 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530253
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530254 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
255 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530256
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530257 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530258 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530259 validate_item_warehouse(args[i])
260
Ankush Menat494bd9e2022-03-28 18:52:46 +0530261 obj = update_entries_after(
262 {
263 "item_code": args[i].get("item_code"),
264 "warehouse": args[i].get("warehouse"),
265 "posting_date": args[i].get("posting_date"),
266 "posting_time": args[i].get("posting_time"),
267 "creation": args[i].get("creation"),
268 "distinct_item_warehouses": distinct_item_warehouses,
269 },
270 allow_negative_stock=allow_negative_stock,
271 via_landed_cost_voucher=via_landed_cost_voucher,
272 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530273 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530274
Ankush Menat494bd9e2022-03-28 18:52:46 +0530275 distinct_item_warehouses[
276 (args[i].get("item_code"), args[i].get("warehouse"))
277 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530278
Nabin Hait97bce3a2021-07-12 13:24:43 +0530279 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530280 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530281 if ("args_idx" not in data and not data.reposting_status) or (
282 data.sle_changed and data.reposting_status
283 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530284 data.args_idx = len(args)
285 args.append(data.sle)
286 elif data.sle_changed and not data.reposting_status:
287 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530288
Nabin Hait97bce3a2021-07-12 13:24:43 +0530289 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530290 i += 1
291
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530292 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530293 update_args_in_repost_item_valuation(
294 doc, i, args, distinct_item_warehouses, affected_transactions
295 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296
Ankush Menat494bd9e2022-03-28 18:52:46 +0530297
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530298def get_reposting_data(file_path) -> dict:
299 file_name = frappe.db.get_value(
300 "File",
301 {
302 "file_url": file_path,
303 "attached_to_field": "reposting_data_file",
304 },
305 "name",
306 )
307
308 if not file_name:
309 return frappe._dict()
310
311 attached_file = frappe.get_doc("File", file_name)
312
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530313 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530314 if data := json.loads(data.decode("utf-8")):
315 data = data
316
317 return parse_json(data)
318
319
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530320def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530321 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530322 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530323 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530324 frappe.throw(_(validation_msg))
325
Ankush Menat494bd9e2022-03-28 18:52:46 +0530326
Ankush Menatecdb4932022-04-17 19:06:13 +0530327def update_args_in_repost_item_valuation(
328 doc, index, args, distinct_item_warehouses, affected_transactions
329):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530330 if not doc.items_to_be_repost:
331 file_name = ""
332 if doc.reposting_data_file:
333 file_name = get_reposting_file_name(doc.doctype, doc.name)
334 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
335
336 doc.reposting_data_file = create_json_gz_file(
337 {
338 "items_to_be_repost": args,
339 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
340 "affected_transactions": affected_transactions,
341 },
342 doc,
343 file_name,
344 )
345
346 doc.db_set(
347 {
348 "current_index": index,
349 "total_reposting_count": len(args),
350 "reposting_data_file": doc.reposting_data_file,
351 }
352 )
353
354 else:
355 doc.db_set(
356 {
357 "items_to_be_repost": json.dumps(args, default=str),
358 "distinct_item_and_warehouse": json.dumps(
359 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
360 ),
361 "current_index": index,
362 "affected_transactions": frappe.as_json(affected_transactions),
363 }
364 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530365
Ankush Menatecdb4932022-04-17 19:06:13 +0530366 if not frappe.flags.in_test:
367 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530368
Ankush Menat494bd9e2022-03-28 18:52:46 +0530369 frappe.publish_realtime(
370 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530371 {
372 "name": doc.name,
373 "items_to_be_repost": json.dumps(args, default=str),
374 "current_index": index,
375 "total_reposting_count": len(args),
376 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530377 doctype=doc.doctype,
378 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530379 )
380
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530381
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530382def get_reposting_file_name(dt, dn):
383 return frappe.db.get_value(
384 "File",
385 {
386 "attached_to_doctype": dt,
387 "attached_to_name": dn,
388 "attached_to_field": "reposting_data_file",
389 },
390 "name",
391 )
392
393
394def create_json_gz_file(data, doc, file_name=None) -> str:
395 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530396 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530397
398 if not file_name:
399 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
400 _file = frappe.get_doc(
401 {
402 "doctype": "File",
403 "file_name": json_filename,
404 "attached_to_doctype": doc.doctype,
405 "attached_to_name": doc.name,
406 "attached_to_field": "reposting_data_file",
407 "content": compressed_content,
408 "is_private": 1,
409 }
410 )
411 _file.save(ignore_permissions=True)
412
413 return _file.file_url
414 else:
415 file_doc = frappe.get_doc("File", file_name)
416 path = file_doc.get_full_path()
417
418 with open(path, "wb") as f:
419 f.write(compressed_content)
420
421 return doc.reposting_data_file
422
423
424def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
425 if not reposting_data and doc and doc.reposting_data_file:
426 reposting_data = get_reposting_data(doc.reposting_data_file)
427
428 if reposting_data and reposting_data.items_to_be_repost:
429 return reposting_data.items_to_be_repost
430
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530431 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530432
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530433 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530434 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530435
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530436 if not items_to_be_repost and voucher_type and voucher_no:
437 items_to_be_repost = frappe.db.get_all(
438 "Stock Ledger Entry",
439 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
440 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
441 order_by="creation asc",
442 group_by="item_code, warehouse",
443 )
444
Nabin Haite1fa7232022-07-20 15:19:09 +0530445 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530446
Ankush Menat494bd9e2022-03-28 18:52:46 +0530447
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530448def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
449 if not reposting_data and doc and doc.reposting_data_file:
450 reposting_data = get_reposting_data(doc.reposting_data_file)
451
452 if reposting_data and reposting_data.distinct_item_and_warehouse:
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530453 return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530454
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530455 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530456
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530457 if doc and doc.distinct_item_and_warehouse:
458 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530459 distinct_item_warehouses = {
460 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
461 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530462 else:
463 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530464 distinct_item_warehouses.setdefault(
465 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
466 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530467
468 return distinct_item_warehouses
469
Ankush Menat494bd9e2022-03-28 18:52:46 +0530470
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530471def parse_distinct_items_and_warehouses(distinct_items_and_warehouses):
472 new_dict = frappe._dict({})
473
474 # convert string keys to tuple
475 for k, v in distinct_items_and_warehouses.items():
476 new_dict[frappe.safe_eval(k)] = frappe._dict(v)
477
478 return new_dict
479
480
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530481def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
482 if not reposting_data and doc and doc.reposting_data_file:
483 reposting_data = get_reposting_data(doc.reposting_data_file)
484
485 if reposting_data and reposting_data.affected_transactions:
486 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
487
Ankush Menatecdb4932022-04-17 19:06:13 +0530488 if not doc.affected_transactions:
489 return set()
490
491 transactions = frappe.parse_json(doc.affected_transactions)
492 return {tuple(transaction) for transaction in transactions}
493
494
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530495def get_current_index(doc=None):
496 if doc and doc.current_index:
497 return doc.current_index
498
Ankush Menat494bd9e2022-03-28 18:52:46 +0530499
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530500class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530501 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530502 update valution rate and qty after transaction
503 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530504
Ankush Menat494bd9e2022-03-28 18:52:46 +0530505 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530506
Ankush Menat494bd9e2022-03-28 18:52:46 +0530507 args = {
508 "item_code": "ABC",
509 "warehouse": "XYZ",
510 "posting_date": "2012-12-12",
511 "posting_time": "12:00"
512 }
Nabin Hait902e8602013-01-08 18:29:24 +0530513 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530514
515 def __init__(
516 self,
517 args,
518 allow_zero_rate=False,
519 allow_negative_stock=None,
520 via_landed_cost_voucher=False,
521 verbose=1,
522 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530523 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530524 self.verbose = verbose
525 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530526 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530527 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530528 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
529 item_code=self.item_code
530 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530531
Nabin Haita77b8c92020-12-21 14:45:50 +0530532 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530533 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530534 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530535
Nabin Haita77b8c92020-12-21 14:45:50 +0530536 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530537 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530538 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530539
540 self.new_items_found = False
541 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530542 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-r73b65ac2023-11-01 18:35:07 +0530543 self.reserved_stock = flt(self.args.reserved_stock)
Nabin Haita77b8c92020-12-21 14:45:50 +0530544
545 self.data = frappe._dict()
546 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530547 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530548
Maricad6078aa2022-06-17 15:13:13 +0530549 def set_precision(self):
550 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
551 self.currency_precision = get_field_precision(
552 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530553 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530554
555 def initialize_previous_data(self, args):
556 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530557 Get previous sl entries for current item for each related warehouse
558 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530559
Ankush Menat494bd9e2022-03-28 18:52:46 +0530560 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530561
Ankush Menat494bd9e2022-03-28 18:52:46 +0530562 self.data = {
563 warehouse1: {
564 'previus_sle': {},
565 'qty_after_transaction': 10,
566 'valuation_rate': 100,
567 'stock_value': 1000,
568 'prev_stock_value': 1000,
569 'stock_queue': '[[10, 100]]',
570 'stock_value_difference': 1000
571 }
572 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530573
574 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530575 self.data.setdefault(args.warehouse, frappe._dict())
576 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530577 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530578 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530579
Ankush Menatc1d986a2021-08-31 19:43:42 +0530580 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
581 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
582
Ankush Menat494bd9e2022-03-28 18:52:46 +0530583 warehouse_dict.update(
584 {
585 "prev_stock_value": previous_sle.stock_value or 0.0,
586 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
587 "stock_value_difference": 0.0,
588 }
589 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530590
Nabin Haita77b8c92020-12-21 14:45:50 +0530591 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530592 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530593
Nabin Haita77b8c92020-12-21 14:45:50 +0530594 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530595 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530596 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530597 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530598 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530599 entries_to_fix = self.get_future_entries_to_fix()
600
601 i = 0
602 while i < len(entries_to_fix):
603 sle = entries_to_fix[i]
604 i += 1
605
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530606 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530607 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530608
Nabin Haita77b8c92020-12-21 14:45:50 +0530609 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530610 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530611
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530612 if self.exceptions:
613 self.raise_exceptions()
614
Nabin Hait186a0452021-02-18 14:14:21 +0530615 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530616 sl_entries = self.get_sle_against_current_voucher()
617 for sle in sl_entries:
618 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530619
Nabin Haita77b8c92020-12-21 14:45:50 +0530620 def get_sle_against_current_voucher(self):
Rohit Waghchaured80ca522024-02-07 21:56:21 +0530621 self.args["posting_datetime"] = get_combine_datetime(
622 self.args.posting_date, self.args.posting_time
623 )
Nabin Haitf2be0802021-02-15 19:27:49 +0530624
Ankush Menat494bd9e2022-03-28 18:52:46 +0530625 return frappe.db.sql(
626 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530627 select
Rohit Waghchaured80ca522024-02-07 21:56:21 +0530628 *, posting_datetime as "timestamp"
Nabin Haita77b8c92020-12-21 14:45:50 +0530629 from
630 `tabStock Ledger Entry`
631 where
632 item_code = %(item_code)s
633 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530634 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530635 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +0530636 posting_datetime = %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530637 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530638 order by
Rohit Waghchaurea73ba2c2024-02-21 17:32:02 +0530639 creation ASC
Nabin Haita77b8c92020-12-21 14:45:50 +0530640 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530641 """,
642 self.args,
643 as_dict=1,
644 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530645
Nabin Haita77b8c92020-12-21 14:45:50 +0530646 def get_future_entries_to_fix(self):
647 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530648 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
649 {"item_code": self.item_code, "warehouse": self.args.warehouse}
650 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530651
Nabin Haita77b8c92020-12-21 14:45:50 +0530652 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530653
Nabin Haita77b8c92020-12-21 14:45:50 +0530654 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530655 dependant_sle = get_sle_by_voucher_detail_no(
656 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
657 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530658
Nabin Haita77b8c92020-12-21 14:45:50 +0530659 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530660 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530661 elif (
662 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
663 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530664 return entries_to_fix
665 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530666 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530667 return entries_to_fix
668 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
669 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530670 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530671 self.initialize_previous_data(dependant_sle)
672 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530673 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530674
675 def update_distinct_item_warehouses(self, dependant_sle):
676 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530677 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530678
Nabin Hait97bce3a2021-07-12 13:24:43 +0530679 if key not in self.distinct_item_warehouses:
680 self.distinct_item_warehouses[key] = val
681 self.new_items_found = True
682 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530683 existing_sle_posting_date = (
684 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
685 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530686
687 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
688
Nabin Hait97bce3a2021-07-12 13:24:43 +0530689 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
690 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530691 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
692 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530693 self.distinct_item_warehouses[key] = val
694 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530695 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
696 # Future dependent voucher needs to be repost to get the correct stock value
697 # If dependent voucher has not reposted, then add it to the list
698 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530699 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530700 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
701 self.distinct_item_warehouses[key] = val
702
703 def get_dependent_voucher_detail_nos(self, key):
704 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
705 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
706
707 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530708
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530709 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530710 # previous sle data for this warehouse
711 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530712 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530713
Anand Doshi0dc79f42015-04-06 12:59:34 +0530714 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 +0530715 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530716 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530717 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530718 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530719 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530720
Nabin Haita77b8c92020-12-21 14:45:50 +0530721 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530722 if not self.args.get("sle_id"):
723 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530724
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530725 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530726 sle.voucher_type == "Stock Reconciliation"
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530727 and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530728 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530729 and not self.args.get("sle_id")
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530730 and sle.is_cancelled == 0
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530731 ):
732 self.reset_actual_qty_for_stock_reco(sle)
733
734 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530735 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
736 and sle.voucher_detail_no
737 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530738 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530739 ):
740 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
741
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530742 dimensions = get_inventory_dimensions()
743 has_dimensions = False
744 if dimensions:
745 for dimension in dimensions:
746 if sle.get(dimension.get("fieldname")):
747 has_dimensions = True
748
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530749 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530750 self.calculate_valuation_for_serial_batch_bundle(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530751 elif sle.serial_no and not self.args.get("sle_id"):
752 # Only run in reposting
753 self.get_serialized_values(sle)
754 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
755 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
756 self.wh_data.qty_after_transaction = sle.qty_after_transaction
757
758 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
759 self.wh_data.valuation_rate
760 )
761 elif (
762 sle.batch_no
763 and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
764 and not self.args.get("sle_id")
765 ):
766 # Only run in reposting
767 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530768 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530769 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530770 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530771 self.wh_data.valuation_rate = sle.valuation_rate
772 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530773 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
774 self.wh_data.valuation_rate
775 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530776 if self.valuation_method != "Moving Average":
777 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530778 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530779 if self.valuation_method == "Moving Average":
780 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530781 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530782 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
783 self.wh_data.valuation_rate
784 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530785 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530786 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530787
Rushabh Mehta54047782013-12-26 11:07:46 +0530788 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530789 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530790 if not self.wh_data.qty_after_transaction:
791 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530792
Nabin Haita77b8c92020-12-21 14:45:50 +0530793 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
794 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530795
Nabin Hait902e8602013-01-08 18:29:24 +0530796 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530797 sle.qty_after_transaction = self.wh_data.qty_after_transaction
798 sle.valuation_rate = self.wh_data.valuation_rate
799 sle.stock_value = self.wh_data.stock_value
800 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530801
rohitwaghchaurea8216b92023-11-09 12:22:26 +0530802 if not sle.is_adjustment_entry or not self.args.get("sle_id"):
803 sle.stock_value_difference = stock_value_difference
804
805 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530806 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530807
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530808 if not self.args.get("sle_id") or (
809 sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
810 ):
Ankush Menat701878f2022-03-01 18:08:29 +0530811 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530812
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530813 def get_serialized_values(self, sle):
814 incoming_rate = flt(sle.incoming_rate)
815 actual_qty = flt(sle.actual_qty)
816 serial_nos = cstr(sle.serial_no).split("\n")
817
818 if incoming_rate < 0:
819 # wrong incoming rate
820 incoming_rate = self.wh_data.valuation_rate
821
822 stock_value_change = 0
823 if actual_qty > 0:
824 stock_value_change = actual_qty * incoming_rate
825 else:
826 # In case of delivery/stock issue, get average purchase rate
827 # of serial nos of current entry
828 if not sle.is_cancelled:
829 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
830 stock_value_change = -1 * outgoing_value
831 else:
832 stock_value_change = actual_qty * sle.outgoing_rate
833
834 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
835
836 if new_stock_qty > 0:
837 new_stock_value = (
838 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
839 ) + stock_value_change
840 if new_stock_value >= 0:
841 # calculate new valuation rate only if stock value is positive
842 # else it remains the same as that of previous entry
843 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
844
845 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
846 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
847 sle.voucher_type, sle.voucher_detail_no
848 )
849 if not allow_zero_rate:
850 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
851
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530852 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530853 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
854 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
855
856 if sle.actual_qty < 0:
857 sle.actual_qty = (
858 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
859 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530860 )
861
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530862 if abs(sle.actual_qty) == 0.0:
863 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530864
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530865 if sle.serial_and_batch_bundle and frappe.get_cached_value(
866 "Item", sle.item_code, "has_serial_no"
867 ):
868 self.update_serial_no_status(sle)
869
870 def update_serial_no_status(self, sle):
871 from erpnext.stock.serial_batch_bundle import get_serial_nos
872
873 serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
874 if not serial_nos:
875 return
876
877 warehouse = None
878 status = "Inactive"
879
880 if sle.actual_qty > 0:
881 warehouse = sle.warehouse
882 status = "Active"
883
884 sn_table = frappe.qb.DocType("Serial No")
885
886 query = (
887 frappe.qb.update(sn_table)
888 .set(sn_table.warehouse, warehouse)
889 .set(sn_table.status, status)
890 .where(sn_table.name.isin(serial_nos))
891 )
892
893 query.run()
894
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530895 def calculate_valuation_for_serial_batch_bundle(self, sle):
896 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
897
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530898 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530899 doc.calculate_qty_and_amount(save=True)
900
901 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
902
s-aga-r1e15a3c2024-02-02 13:07:26 +0530903 precision = doc.precision("total_qty")
904 self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
Rohit Waghchauree8ae4ed2024-02-19 22:18:57 +0530905 if flt(self.wh_data.qty_after_transaction, precision):
s-aga-r1e15a3c2024-02-02 13:07:26 +0530906 self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
907 self.wh_data.qty_after_transaction, precision
908 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530909
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530910 def validate_negative_stock(self, sle):
911 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530912 validate negative stock for entries current datetime onwards
913 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530914 """
s-aga-rf0acb202023-04-12 14:13:54 +0530915 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530916 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530917
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530918 if diff < 0 and abs(diff) > 0.0001:
919 # negative stock!
920 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530921 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530922 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530923 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530924 return True
925
Nabin Haita77b8c92020-12-21 14:45:50 +0530926 def get_dynamic_incoming_outgoing_rate(self, sle):
927 # Get updated incoming/outgoing rate from transaction
928 if sle.recalculate_rate:
929 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
930
931 if flt(sle.actual_qty) >= 0:
932 sle.incoming_rate = rate
933 else:
934 sle.outgoing_rate = rate
935
936 def get_incoming_outgoing_rate_from_transaction(self, sle):
937 rate = 0
938 # Material Transfer, Repack, Manufacturing
939 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530940 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530941 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
942 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530943 elif sle.voucher_type in (
944 "Purchase Receipt",
945 "Purchase Invoice",
946 "Delivery Note",
947 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530948 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530949 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530950 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530951 from erpnext.controllers.sales_and_purchase_return import (
952 get_rate_for_return, # don't move this import to top
953 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530954
rohitwaghchaure63792382024-03-04 12:04:41 +0530955 if (
956 self.valuation_method == "Moving Average"
957 and not sle.get("serial_no")
958 and not sle.get("batch_no")
959 and not sle.get("serial_and_batch_bundle")
960 ):
mergify[bot]07175362023-12-21 14:40:52 +0530961 rate = get_incoming_rate(
962 {
963 "item_code": sle.item_code,
964 "warehouse": sle.warehouse,
965 "posting_date": sle.posting_date,
966 "posting_time": sle.posting_time,
967 "qty": sle.actual_qty,
968 "serial_no": sle.get("serial_no"),
969 "batch_no": sle.get("batch_no"),
970 "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
971 "company": sle.company,
972 "voucher_type": sle.voucher_type,
973 "voucher_no": sle.voucher_no,
974 "allow_zero_valuation": self.allow_zero_rate,
975 "sle": sle.name,
976 }
977 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530978
mergify[bot]07175362023-12-21 14:40:52 +0530979 else:
980 rate = get_rate_for_return(
981 sle.voucher_type,
982 sle.voucher_no,
983 sle.item_code,
984 voucher_detail_no=sle.voucher_detail_no,
985 sle=sle,
986 )
rohitwaghchaure63792382024-03-04 12:04:41 +0530987
988 if (
989 sle.get("serial_and_batch_bundle")
990 and rate > 0
991 and sle.voucher_type in ["Delivery Note", "Sales Invoice"]
992 ):
993 frappe.db.set_value(
994 sle.voucher_type + " Item",
995 sle.voucher_detail_no,
996 "incoming_rate",
997 rate,
998 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530999 elif (
1000 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +05301001 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301002 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +05301003 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301004 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +05301005 else:
1006 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +05301007 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +05301008 elif sle.voucher_type == "Subcontracting Receipt":
1009 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +05301010 else:
1011 rate_field = "incoming_rate"
1012
1013 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +05301014 item_code, incoming_rate = frappe.db.get_value(
1015 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
1016 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301017
1018 if item_code == sle.item_code:
1019 rate = incoming_rate
1020 else:
1021 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1022 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +05301023 elif sle == "Subcontracting Receipt":
1024 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +05301025 else:
1026 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +05301027
Ankush Menat494bd9e2022-03-28 18:52:46 +05301028 rate = frappe.db.get_value(
1029 ref_doctype,
1030 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
1031 rate_field,
1032 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301033
1034 return rate
1035
1036 def update_outgoing_rate_on_transaction(self, sle):
1037 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301038 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
1039 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +05301040 """
1041 if sle.actual_qty and sle.voucher_detail_no:
1042 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
1043
1044 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
1045 self.update_rate_on_stock_entry(sle, outgoing_rate)
1046 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1047 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
1048 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
1049 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301050 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
1051 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +05301052 elif sle.voucher_type == "Stock Reconciliation":
1053 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +05301054
1055 def update_rate_on_stock_entry(self, sle, outgoing_rate):
1056 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
1057
Ankush Menat701878f2022-03-01 18:08:29 +05301058 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
1059 if not sle.dependant_sle_voucher_detail_no:
1060 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +05301061
1062 def recalculate_amounts_in_stock_entry(self, voucher_no):
1063 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +05301064 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
1065 stock_entry.db_update()
1066 for d in stock_entry.items:
1067 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +05301068
Nabin Haita77b8c92020-12-21 14:45:50 +05301069 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
1070 # Update item's incoming rate on transaction
1071 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
1072 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301073 frappe.db.set_value(
1074 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
1075 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301076 else:
1077 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +05301078 frappe.db.set_value(
1079 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +05301080 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301081 "incoming_rate",
1082 outgoing_rate,
1083 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301084
1085 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
1086 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301087 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
1088 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
1089 ):
1090 frappe.db.set_value(
1091 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
1092 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301093 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301094 frappe.db.set_value(
1095 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
1096 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301097
1098 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +05301099 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +05301100 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +05301101 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301102 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +05301103 d.db_update()
1104
Sagar Sharma323bdf82022-05-17 15:14:07 +05301105 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +05301106 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
1107 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301108 else:
1109 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301110 "Subcontracting Receipt Supplied Item",
1111 sle.voucher_detail_no,
1112 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +05301113 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301114
s-aga-ra6cb6c62023-05-03 09:51:58 +05301115 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301116 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +05301117 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301118 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +05301119 d.db_update()
1120
s-aga-r88a3f652023-05-30 16:54:28 +05301121 def update_rate_on_stock_reconciliation(self, sle):
1122 if not sle.serial_no and not sle.batch_no:
1123 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
1124
1125 for item in sr.items:
1126 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +05301127 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +05301128 continue
1129
1130 previous_sle = get_previous_sle(
1131 {
1132 "item_code": item.item_code,
1133 "warehouse": item.warehouse,
1134 "posting_date": sr.posting_date,
1135 "posting_time": sr.posting_time,
1136 "sle": sle.name,
1137 }
1138 )
1139
1140 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
1141 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
1142 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
1143
1144 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +05301145 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +05301146 item.amount_difference = item.amount - item.current_amount
1147 else:
1148 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1149 sr.db_update()
1150
1151 for item in sr.items:
1152 item.db_update()
1153
Nabin Hait328c4f92020-01-02 19:00:32 +05301154 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1155 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301156 all_serial_nos = frappe.get_all(
1157 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1158 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301159
Ankush Menat494bd9e2022-03-28 18:52:46 +05301160 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 +05301161
1162 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301163 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301164 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301165 incoming_rate = frappe.db.sql(
1166 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301167 select incoming_rate
1168 from `tabStock Ledger Entry`
1169 where
1170 company = %s
1171 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301172 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301173 and (serial_no = %s
1174 or serial_no like %s
1175 or serial_no like %s
1176 or serial_no like %s
1177 )
1178 order by posting_date desc
1179 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301180 """,
1181 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1182 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301183
1184 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1185
1186 return incoming_values
1187
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301188 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301189 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301190 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301191 if new_stock_qty >= 0:
1192 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301193 if flt(self.wh_data.qty_after_transaction) <= 0:
1194 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301195 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301196 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1197 actual_qty * sle.incoming_rate
1198 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301199
Nabin Haita77b8c92020-12-21 14:45:50 +05301200 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301201
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301202 elif sle.outgoing_rate:
1203 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301204 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1205 actual_qty * sle.outgoing_rate
1206 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301207
Nabin Haita77b8c92020-12-21 14:45:50 +05301208 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301209 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301210 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301211 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301212 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1213 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301214
Nabin Haita77b8c92020-12-21 14:45:50 +05301215 if not self.wh_data.valuation_rate and actual_qty > 0:
1216 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301217
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301218 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001219 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301220 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301221 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1222 sle.voucher_type, sle.voucher_detail_no
1223 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001224 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301225 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301226
Ankush Menatf089d392022-02-02 12:51:21 +05301227 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301228 incoming_rate = flt(sle.incoming_rate)
1229 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301230 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301231
Ankush Menat494bd9e2022-03-28 18:52:46 +05301232 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1233 self.wh_data.qty_after_transaction + actual_qty
1234 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301235
Ankush Menat97e18a12022-01-15 17:42:25 +05301236 if self.valuation_method == "LIFO":
1237 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1238 else:
1239 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1240
Ankush Menatb534fee2022-02-19 20:58:36 +05301241 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1242
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301243 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301244 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301245 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301246
Ankush Menat4b29fb62021-12-18 18:40:22 +05301247 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301248 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1249 sle.voucher_type, sle.voucher_detail_no
1250 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301251 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301252 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301253 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301254 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301255
Ankush Menat494bd9e2022-03-28 18:52:46 +05301256 stock_queue.remove_stock(
1257 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1258 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301259
Ankush Menatb534fee2022-02-19 20:58:36 +05301260 _qty, stock_value = stock_queue.get_total_stock_and_value()
1261
1262 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301263
Ankush Menat97e18a12022-01-15 17:42:25 +05301264 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301265 self.wh_data.stock_value = round_off_if_near_zero(
1266 self.wh_data.stock_value + stock_value_difference
1267 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301268
Nabin Haita77b8c92020-12-21 14:45:50 +05301269 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301270 self.wh_data.stock_queue.append(
1271 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1272 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301273
Ankush Menatb534fee2022-02-19 20:58:36 +05301274 if self.wh_data.qty_after_transaction:
1275 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1276
Ankush Menatce0514c2022-02-15 11:41:41 +05301277 def update_batched_values(self, sle):
1278 incoming_rate = flt(sle.incoming_rate)
1279 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301280
Ankush Menat494bd9e2022-03-28 18:52:46 +05301281 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1282 self.wh_data.qty_after_transaction + actual_qty
1283 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301284
1285 if actual_qty > 0:
1286 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301287 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301288 outgoing_rate = get_batch_incoming_rate(
1289 item_code=sle.item_code,
1290 warehouse=sle.warehouse,
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301291 batch_no=sle.batch_no,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301292 posting_date=sle.posting_date,
1293 posting_time=sle.posting_time,
1294 creation=sle.creation,
1295 )
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301296
Ankush Menataba7a7c2022-02-19 19:36:28 +05301297 if outgoing_rate is None:
1298 # This can *only* happen if qty available for the batch is zero.
1299 # in such case fall back various other rates.
1300 # future entries will correct the overall accounting as each
1301 # batch individually uses moving average rates.
1302 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301303 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301304
Ankush Menat494bd9e2022-03-28 18:52:46 +05301305 self.wh_data.stock_value = round_off_if_near_zero(
1306 self.wh_data.stock_value + stock_value_difference
1307 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301308 if self.wh_data.qty_after_transaction:
1309 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301310
Javier Wong9b11d9b2017-04-14 18:24:04 +08001311 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301312 ref_item_dt = ""
1313
1314 if voucher_type == "Stock Entry":
1315 ref_item_dt = voucher_type + " Detail"
1316 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1317 ref_item_dt = voucher_type + " Item"
1318
1319 if ref_item_dt:
1320 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1321 else:
1322 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301323
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301324 def get_fallback_rate(self, sle) -> float:
1325 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301326 This should only get used for negative stock."""
1327 return get_valuation_rate(
1328 sle.item_code,
1329 sle.warehouse,
1330 sle.voucher_type,
1331 sle.voucher_no,
1332 self.allow_zero_rate,
1333 currency=erpnext.get_company_currency(sle.company),
1334 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301335 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301336
Nabin Haita77b8c92020-12-21 14:45:50 +05301337 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301338 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301339 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1340 sle = sle[0] if sle else frappe._dict()
1341 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301342
Nabin Haita77b8c92020-12-21 14:45:50 +05301343 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301344 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301345 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301346
1347 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301348 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301349 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301350 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301351
Ankush Menat494bd9e2022-03-28 18:52:46 +05301352 if (
1353 exceptions[0]["voucher_type"],
1354 exceptions[0]["voucher_no"],
1355 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301356
Nabin Haita77b8c92020-12-21 14:45:50 +05301357 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301358 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301359 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1360 frappe.get_desk_link("Warehouse", warehouse),
1361 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301362 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301363 msg = _(
1364 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1365 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301366 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301367 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1368 frappe.get_desk_link("Warehouse", warehouse),
1369 exceptions[0]["posting_date"],
1370 exceptions[0]["posting_time"],
1371 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1372 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301373
Nabin Haita77b8c92020-12-21 14:45:50 +05301374 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301375 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301376 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301377
1378 if allowed_qty > 0:
1379 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1380 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1381 )
1382 else:
1383 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1384 msg,
1385 )
s-aga-rf0acb202023-04-12 14:13:54 +05301386
Nabin Haita77b8c92020-12-21 14:45:50 +05301387 msg_list.append(msg)
1388
1389 if msg_list:
1390 message = "\n\n".join(msg_list)
1391 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301392 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301393 else:
1394 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301395
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301396 def update_bin_data(self, sle):
1397 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301398 values_to_update = {
1399 "actual_qty": sle.qty_after_transaction,
1400 "stock_value": sle.stock_value,
1401 }
1402
1403 if sle.valuation_rate is not None:
1404 values_to_update["valuation_rate"] = sle.valuation_rate
1405
1406 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301407
Nabin Haita77b8c92020-12-21 14:45:50 +05301408 def update_bin(self):
1409 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301410 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301411 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301412
Ankush Menat494bd9e2022-03-28 18:52:46 +05301413 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301414 if data.valuation_rate is not None:
1415 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301416 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301417
marination8418c4b2021-06-22 21:35:25 +05301418
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301419def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301420 """get stock ledger entries filtered by specific posting datetime conditions"""
1421
marination8418c4b2021-06-22 21:35:25 +05301422 if not args.get("posting_date"):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301423 args["posting_datetime"] = "1900-01-01 00:00:00"
1424
1425 if not args.get("posting_datetime"):
1426 args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
1427
marination8418c4b2021-06-22 21:35:25 +05301428 voucher_condition = ""
1429 if exclude_current_voucher:
1430 voucher_no = args.get("voucher_no")
1431 voucher_condition = f"and voucher_no != '{voucher_no}'"
1432
Ankush Menat494bd9e2022-03-28 18:52:46 +05301433 sle = frappe.db.sql(
1434 """
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301435 select *, posting_datetime as "timestamp"
marination8418c4b2021-06-22 21:35:25 +05301436 from `tabStock Ledger Entry`
1437 where item_code = %(item_code)s
1438 and warehouse = %(warehouse)s
1439 and is_cancelled = 0
1440 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301441 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301442 posting_datetime {operator} %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301443 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301444 order by posting_datetime desc, creation desc
marination8418c4b2021-06-22 21:35:25 +05301445 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301446 for update""".format(
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301447 operator=operator,
1448 voucher_condition=voucher_condition,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301449 ),
1450 args,
1451 as_dict=1,
1452 )
marination8418c4b2021-06-22 21:35:25 +05301453
1454 return sle[0] if sle else frappe._dict()
1455
Ankush Menat494bd9e2022-03-28 18:52:46 +05301456
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301457def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301458 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301459 get the last sle on or before the current time-bucket,
1460 to get actual qty before transaction, this function
1461 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301462
Ankush Menat494bd9e2022-03-28 18:52:46 +05301463 args = {
1464 "item_code": "ABC",
1465 "warehouse": "XYZ",
1466 "posting_date": "2012-12-12",
1467 "posting_time": "12:00",
1468 "sle": "name of reference Stock Ledger Entry"
1469 }
Anand Doshi1b531862013-01-10 19:29:51 +05301470 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301471 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301472 sle = get_stock_ledger_entries(
1473 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1474 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301475 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301476
Ankush Menat494bd9e2022-03-28 18:52:46 +05301477
1478def get_stock_ledger_entries(
1479 previous_sle,
1480 operator=None,
1481 order="desc",
1482 limit=None,
1483 for_update=False,
1484 debug=False,
1485 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301486 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301487):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301488 """get stock ledger entries filtered by specific posting datetime conditions"""
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301489 conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator)
Nabin Haitb9ce1042018-02-01 14:58:50 +05301490 if previous_sle.get("warehouse"):
1491 conditions += " and warehouse = %(warehouse)s"
1492 elif previous_sle.get("warehouse_condition"):
1493 conditions += " and " + previous_sle.get("warehouse_condition")
1494
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301495 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301496 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1497 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301498 conditions += (
1499 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301500 (
1501 serial_no = {0}
1502 or serial_no like {1}
1503 or serial_no like {2}
1504 or serial_no like {3}
1505 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301506 """
1507 ).format(
1508 frappe.db.escape(serial_no),
1509 frappe.db.escape("{}\n%".format(serial_no)),
1510 frappe.db.escape("%\n{}".format(serial_no)),
1511 frappe.db.escape("%\n{}\n%".format(serial_no)),
1512 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301513
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301514 if not previous_sle.get("posting_date"):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301515 previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
1516 else:
1517 previous_sle["posting_datetime"] = get_combine_datetime(
1518 previous_sle["posting_date"], previous_sle["posting_time"]
1519 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301520
1521 if operator in (">", "<=") and previous_sle.get("name"):
1522 conditions += " and name!=%(name)s"
1523
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301524 if extra_cond:
1525 conditions += f"{extra_cond}"
1526
Ankush Menat494bd9e2022-03-28 18:52:46 +05301527 return frappe.db.sql(
1528 """
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301529 select *, posting_datetime as "timestamp"
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301530 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301531 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301532 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301533 %(conditions)s
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301534 order by posting_datetime %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301535 %(limit)s %(for_update)s"""
1536 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301537 "conditions": conditions,
1538 "limit": limit or "",
1539 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301540 "order": order,
1541 },
1542 previous_sle,
1543 as_dict=1,
1544 debug=debug,
1545 )
1546
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301547
Nabin Haita77b8c92020-12-21 14:45:50 +05301548def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301549 return frappe.db.get_value(
1550 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301551 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301552 [
1553 "item_code",
1554 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301555 "actual_qty",
1556 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301557 "posting_date",
1558 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301559 "voucher_detail_no",
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301560 "posting_datetime as timestamp",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301561 ],
1562 as_dict=1,
1563 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301564
Ankush Menatce0514c2022-02-15 11:41:41 +05301565
Ankush Menat494bd9e2022-03-28 18:52:46 +05301566def get_batch_incoming_rate(
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301567 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301568):
1569
Ankush Menat102fff22022-02-19 15:51:04 +05301570 sle = frappe.qb.DocType("Stock Ledger Entry")
1571
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301572 timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time)
Ankush Menat102fff22022-02-19 15:51:04 +05301573 if creation:
1574 timestamp_condition |= (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301575 sle.posting_datetime == get_combine_datetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301576 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301577
1578 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301579 frappe.qb.from_(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301580 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301581 .where(
1582 (sle.item_code == item_code)
1583 & (sle.warehouse == warehouse)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301584 & (sle.batch_no == batch_no)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301585 & (sle.is_cancelled == 0)
1586 )
1587 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301588 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301589
1590 if batch_details and batch_details[0].batch_qty:
1591 return batch_details[0].batch_value / batch_details[0].batch_qty
1592
1593
Ankush Menat494bd9e2022-03-28 18:52:46 +05301594def get_valuation_rate(
1595 item_code,
1596 warehouse,
1597 voucher_type,
1598 voucher_no,
1599 allow_zero_rate=False,
1600 currency=None,
1601 company=None,
1602 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301603 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301604 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301605):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301606
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301607 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1608
Ankush Menatf7ffe042021-11-01 13:21:14 +05301609 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301610 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301611
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301612 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1613 table = frappe.qb.DocType("Stock Ledger Entry")
1614 query = (
1615 frappe.qb.from_(table)
1616 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1617 .where(
1618 (table.item_code == item_code)
1619 & (table.warehouse == warehouse)
1620 & (table.batch_no == batch_no)
1621 & (table.is_cancelled == 0)
1622 & (table.voucher_no != voucher_no)
1623 & (table.voucher_type != voucher_type)
1624 )
1625 )
1626
1627 last_valuation_rate = query.run()
1628 if last_valuation_rate:
1629 return flt(last_valuation_rate[0][0])
1630
Ankush Menat342d09a2022-02-19 14:28:51 +05301631 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301632 if warehouse and serial_and_batch_bundle:
1633 batch_obj = BatchNoValuation(
1634 sle=frappe._dict(
1635 {
1636 "item_code": item_code,
1637 "warehouse": warehouse,
1638 "actual_qty": -1,
1639 "serial_and_batch_bundle": serial_and_batch_bundle,
1640 }
1641 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301642 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301643
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301644 return batch_obj.get_incoming_rate()
1645
Ankush Menatf7ffe042021-11-01 13:21:14 +05301646 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301647 if last_valuation_rate := frappe.db.sql(
1648 """select valuation_rate
1649 from `tabStock Ledger Entry` force index (item_warehouse)
1650 where
1651 item_code = %s
1652 AND warehouse = %s
1653 AND valuation_rate >= 0
1654 AND is_cancelled = 0
1655 AND NOT (voucher_no = %s AND voucher_type = %s)
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301656 order by posting_datetime desc, name desc limit 1""",
Akhil Narangdd911aa2023-09-26 13:45:39 +05301657 (item_code, warehouse, voucher_no, voucher_type),
1658 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301659 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301660
1661 # If negative stock allowed, and item delivered without any incoming entry,
1662 # system does not found any SLE, then take valuation rate from Item
1663 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301664
1665 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301666 # try Item Standard rate
1667 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301668
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301669 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301670 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301671 valuation_rate = frappe.db.get_value(
1672 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1673 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301674
Ankush Menat494bd9e2022-03-28 18:52:46 +05301675 if (
1676 not allow_zero_rate
1677 and not valuation_rate
1678 and raise_error_if_no_rate
1679 and cint(erpnext.is_perpetual_inventory_enabled(company))
1680 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301681 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301682
Ankush Menat494bd9e2022-03-28 18:52:46 +05301683 message = _(
1684 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1685 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301686 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301687 solutions = (
1688 "<li>"
1689 + _(
1690 "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."
1691 ).format(voucher_type)
1692 + "</li>"
1693 )
1694 solutions += (
1695 "<li>"
1696 + _("If not, you can Cancel / Submit this entry")
1697 + " {0} ".format(frappe.bold("after"))
1698 + _("performing either one below:")
1699 + "</li>"
1700 )
Marica97715f22020-05-11 20:45:37 +05301701 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1702 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1703 msg = message + solutions + sub_solutions + "</li>"
1704
1705 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301706
1707 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301708
Ankush Menat494bd9e2022-03-28 18:52:46 +05301709
Ankush Menate7109c12021-08-26 16:40:45 +05301710def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301711 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301712 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301713 qty_shift = args.actual_qty
1714
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301715 args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
Ankush Menat7c839c42022-05-06 12:09:08 +05301716
marination8418c4b2021-06-22 21:35:25 +05301717 # find difference/shift in qty caused by stock reconciliation
1718 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301719 qty_shift = get_stock_reco_qty_shift(args)
1720
1721 # find the next nearest stock reco so that we only recalculate SLEs till that point
1722 next_stock_reco_detail = get_next_stock_reco(args)
1723 if next_stock_reco_detail:
1724 detail = next_stock_reco_detail[0]
marination40389772021-07-02 17:13:45 +05301725 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301726
Ankush Menat494bd9e2022-03-28 18:52:46 +05301727 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301728 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301729 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301730 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301731 where
1732 item_code = %(item_code)s
1733 and warehouse = %(warehouse)s
1734 and voucher_no != %(voucher_no)s
1735 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301736 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301737 posting_datetime > %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301738 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301739 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301740 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301741 args,
1742 )
Nabin Hait186a0452021-02-18 14:14:21 +05301743
1744 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1745
Ankush Menat494bd9e2022-03-28 18:52:46 +05301746
marination40389772021-07-02 17:13:45 +05301747def get_stock_reco_qty_shift(args):
1748 stock_reco_qty_shift = 0
1749 if args.get("is_cancelled"):
1750 if args.get("previous_qty_after_transaction"):
1751 # get qty (balance) that was set at submission
1752 last_balance = args.get("previous_qty_after_transaction")
1753 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1754 else:
1755 stock_reco_qty_shift = flt(args.actual_qty)
1756 else:
1757 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301758 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301759 "qty_after_transaction"
1760 )
marination40389772021-07-02 17:13:45 +05301761
1762 if last_balance is not None:
1763 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1764 else:
1765 stock_reco_qty_shift = args.qty_after_transaction
1766
1767 return stock_reco_qty_shift
1768
Ankush Menat494bd9e2022-03-28 18:52:46 +05301769
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301770def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301771 """Returns next nearest stock reconciliaton's details."""
1772
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301773 sle = frappe.qb.DocType("Stock Ledger Entry")
1774
1775 query = (
1776 frappe.qb.from_(sle)
1777 .select(
1778 sle.name,
1779 sle.posting_date,
1780 sle.posting_time,
1781 sle.creation,
1782 sle.voucher_no,
1783 sle.item_code,
1784 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301785 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301786 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301787 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301788 )
1789 .where(
1790 (sle.item_code == kwargs.get("item_code"))
1791 & (sle.warehouse == kwargs.get("warehouse"))
1792 & (sle.voucher_type == "Stock Reconciliation")
1793 & (sle.voucher_no != kwargs.get("voucher_no"))
1794 & (sle.is_cancelled == 0)
1795 & (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301796 sle.posting_datetime
1797 >= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
marination40389772021-07-02 17:13:45 +05301798 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301799 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301800 .orderby(sle.posting_datetime)
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301801 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301802 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301803 )
1804
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301805 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301806 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301807
1808 return query.run(as_dict=True)
1809
marination40389772021-07-02 17:13:45 +05301810
1811def get_datetime_limit_condition(detail):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301812 posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time)
1813
marination40389772021-07-02 17:13:45 +05301814 return f"""
1815 and
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301816 (posting_datetime < '{posting_datetime}'
marination40389772021-07-02 17:13:45 +05301817 or (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301818 posting_datetime = '{posting_datetime}'
marination40389772021-07-02 17:13:45 +05301819 and creation < '{detail.creation}'
1820 )
1821 )"""
1822
Ankush Menat494bd9e2022-03-28 18:52:46 +05301823
Ankush Menate7109c12021-08-26 16:40:45 +05301824def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301825 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301826 return
barredterraeb9ee3f2023-12-05 11:22:55 +01001827 if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
Ankush Menat5eba5752021-12-07 23:03:52 +05301828 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301829
Ankush Menat5eba5752021-12-07 23:03:52 +05301830 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301831
1832 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301833 message = _(
1834 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1835 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301836 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301837 frappe.get_desk_link("Item", args.item_code),
1838 frappe.get_desk_link("Warehouse", args.warehouse),
1839 neg_sle[0]["posting_date"],
1840 neg_sle[0]["posting_time"],
1841 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1842 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301843
Ankush Menat494bd9e2022-03-28 18:52:46 +05301844 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301845
s-aga-rd9e28432023-10-27 16:35:35 +05301846 if args.batch_no:
1847 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1848 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1849 message = _(
1850 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1851 ).format(
1852 abs(neg_batch_sle[0]["cumulative_total"]),
1853 frappe.get_desk_link("Batch", args.batch_no),
1854 frappe.get_desk_link("Warehouse", args.warehouse),
1855 neg_batch_sle[0]["posting_date"],
1856 neg_batch_sle[0]["posting_time"],
1857 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1858 )
1859 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301860
s-aga-r73b65ac2023-11-01 18:35:07 +05301861 if args.reserved_stock:
1862 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301863
Nabin Haita77b8c92020-12-21 14:45:50 +05301864
Maricad6078aa2022-06-17 15:13:13 +05301865def is_negative_with_precision(neg_sle, is_batch=False):
1866 """
1867 Returns whether system precision rounded qty is insufficient.
1868 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1869 """
1870
1871 if not neg_sle:
1872 return False
1873
1874 field = "cumulative_total" if is_batch else "qty_after_transaction"
1875 precision = cint(frappe.db.get_default("float_precision")) or 2
1876 qty_deficit = flt(neg_sle[0][field], precision)
1877
1878 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1879
1880
Nabin Haita77b8c92020-12-21 14:45:50 +05301881def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301882 return frappe.db.sql(
1883 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301884 select
1885 qty_after_transaction, posting_date, posting_time,
1886 voucher_type, voucher_no
1887 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301888 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301889 item_code = %(item_code)s
1890 and warehouse = %(warehouse)s
1891 and voucher_no != %(voucher_no)s
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301892 and posting_datetime >= %(posting_datetime)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301893 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301894 and qty_after_transaction < 0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301895 order by posting_datetime asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301896 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301897 """,
1898 args,
1899 as_dict=1,
1900 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301901
Ankush Menat5eba5752021-12-07 23:03:52 +05301902
1903def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301904 return frappe.db.sql(
1905 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301906 with batch_ledger as (
1907 select
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301908 posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
1909 sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
Ankush Menat5eba5752021-12-07 23:03:52 +05301910 from `tabStock Ledger Entry`
1911 where
1912 item_code = %(item_code)s
1913 and warehouse = %(warehouse)s
1914 and batch_no=%(batch_no)s
1915 and is_cancelled = 0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301916 order by posting_datetime, creation
Ankush Menat5eba5752021-12-07 23:03:52 +05301917 )
1918 select * from batch_ledger
1919 where
1920 cumulative_total < 0.0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301921 and posting_datetime >= %(posting_datetime)s
Ankush Menat5eba5752021-12-07 23:03:52 +05301922 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301923 """,
1924 args,
1925 as_dict=1,
1926 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301927
1928
s-aga-rd9e28432023-10-27 16:35:35 +05301929def validate_reserved_stock(kwargs):
1930 if kwargs.serial_no:
1931 serial_nos = kwargs.serial_no.split("\n")
1932 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1933
s-aga-re1a87a82023-10-31 18:41:58 +05301934 elif kwargs.batch_no:
1935 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1936
s-aga-rd9e28432023-10-27 16:35:35 +05301937 elif kwargs.serial_and_batch_bundle:
1938 sbb_entries = frappe.db.get_all(
1939 "Serial and Batch Entry",
1940 {
1941 "parenttype": "Serial and Batch Bundle",
1942 "parent": kwargs.serial_and_batch_bundle,
1943 "docstatus": 1,
1944 },
s-aga-re1a87a82023-10-31 18:41:58 +05301945 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301946 )
s-aga-rd9e28432023-10-27 16:35:35 +05301947
s-aga-re1a87a82023-10-31 18:41:58 +05301948 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301949 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301950 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1951 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301952
s-aga-r92317062023-11-02 10:36:00 +05301953 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1954 precision = cint(frappe.db.get_default("float_precision")) or 2
1955 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301956
s-aga-r92317062023-11-02 10:36:00 +05301957 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1958 if diff < 0 and abs(diff) > 0.0001:
1959 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1960 abs(diff),
1961 frappe.get_desk_link("Item", kwargs.item_code),
1962 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1963 nowdate(),
1964 nowtime(),
1965 )
1966 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301967
s-aga-rd9e28432023-10-27 16:35:35 +05301968
1969def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1970 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1971 item_code, warehouse, serial_nos
1972 ):
1973 if common_serial_nos := list(
1974 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1975 ):
1976 msg = _(
1977 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1978 )
1979 msg += "<br />"
1980 msg += _("Example: Serial No {0} reserved in {1}.").format(
1981 frappe.bold(common_serial_nos[0]),
1982 frappe.get_desk_link(
1983 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1984 ),
1985 )
1986 frappe.throw(msg, title=_("Reserved Serial No."))
1987
1988
s-aga-re1a87a82023-10-31 18:41:58 +05301989def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1990 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1991 available_batches = get_available_batches(
1992 frappe._dict(
1993 {
1994 "item_code": item_code,
1995 "warehouse": warehouse,
1996 "posting_date": nowdate(),
1997 "posting_time": nowtime(),
1998 }
1999 )
2000 )
2001 available_batches_map = {row.batch_no: row.qty for row in available_batches}
2002 precision = cint(frappe.db.get_default("float_precision")) or 2
2003
2004 for batch_no in batch_nos:
2005 diff = flt(
2006 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
2007 )
2008 if diff < 0 and abs(diff) > 0.0001:
2009 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
2010 abs(diff),
2011 frappe.get_desk_link("Batch", batch_no),
2012 frappe.get_desk_link("Warehouse", warehouse),
2013 nowdate(),
2014 nowtime(),
2015 )
2016 frappe.throw(msg, title=_("Reserved Stock for Batch"))
2017
2018
Ankush Menateb8b4242022-02-12 13:08:28 +05302019def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
2020 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
2021 return True
2022 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
2023 return True
2024 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05302025
2026
2027def get_incoming_rate_for_inter_company_transfer(sle) -> float:
2028 """
2029 For inter company transfer, incoming rate is the average of the outgoing rate
2030 """
2031 rate = 0.0
2032
2033 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
2034
2035 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
2036
2037 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
2038
2039 if reference_name:
2040 rate = frappe.get_cached_value(
2041 doctype,
2042 reference_name,
2043 "incoming_rate",
2044 )
2045
2046 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05302047
2048
2049def is_internal_transfer(sle):
2050 data = frappe.get_cached_value(
2051 sle.voucher_type,
2052 sle.voucher_no,
2053 ["is_internal_supplier", "represents_company", "company"],
2054 as_dict=True,
2055 )
2056
2057 if data.is_internal_supplier and data.represents_company == data.company:
2058 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302059
2060
2061def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
2062 table = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaured80ca522024-02-07 21:56:21 +05302063 posting_datetime = get_combine_datetime(posting_date, posting_time)
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302064
2065 query = (
2066 frappe.qb.from_(table)
2067 .select(Sum(table.stock_value_difference).as_("value"))
2068 .where(
2069 (table.is_cancelled == 0)
2070 & (table.item_code == item_code)
2071 & (table.warehouse == warehouse)
Rohit Waghchaured80ca522024-02-07 21:56:21 +05302072 & (table.posting_datetime <= posting_datetime)
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302073 )
2074 )
2075
2076 if voucher_no:
2077 query = query.where(table.voucher_no != voucher_no)
2078
2079 difference_amount = query.run()
2080 return flt(difference_amount[0][0]) if difference_amount else 0