blob: 60053aae03c077e94e25bb60e3ec54e39a148b68 [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):
rohitwaghchaure01856a62024-03-07 14:14:19 +0530896 if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
897 return
898
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530899 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
900
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530901 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530902 doc.calculate_qty_and_amount(save=True)
903
904 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
905
s-aga-r1e15a3c2024-02-02 13:07:26 +0530906 precision = doc.precision("total_qty")
907 self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
Rohit Waghchauree8ae4ed2024-02-19 22:18:57 +0530908 if flt(self.wh_data.qty_after_transaction, precision):
s-aga-r1e15a3c2024-02-02 13:07:26 +0530909 self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
910 self.wh_data.qty_after_transaction, precision
911 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530912
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530913 def validate_negative_stock(self, sle):
914 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530915 validate negative stock for entries current datetime onwards
916 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530917 """
s-aga-rf0acb202023-04-12 14:13:54 +0530918 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530919 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530920
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530921 if diff < 0 and abs(diff) > 0.0001:
922 # negative stock!
923 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530924 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530925 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530926 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530927 return True
928
Nabin Haita77b8c92020-12-21 14:45:50 +0530929 def get_dynamic_incoming_outgoing_rate(self, sle):
930 # Get updated incoming/outgoing rate from transaction
931 if sle.recalculate_rate:
932 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
933
934 if flt(sle.actual_qty) >= 0:
935 sle.incoming_rate = rate
936 else:
937 sle.outgoing_rate = rate
938
939 def get_incoming_outgoing_rate_from_transaction(self, sle):
940 rate = 0
941 # Material Transfer, Repack, Manufacturing
942 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530943 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530944 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
945 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530946 elif sle.voucher_type in (
947 "Purchase Receipt",
948 "Purchase Invoice",
949 "Delivery Note",
950 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530951 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530952 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530953 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530954 from erpnext.controllers.sales_and_purchase_return import (
955 get_rate_for_return, # don't move this import to top
956 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530957
rohitwaghchaure63792382024-03-04 12:04:41 +0530958 if (
959 self.valuation_method == "Moving Average"
960 and not sle.get("serial_no")
961 and not sle.get("batch_no")
962 and not sle.get("serial_and_batch_bundle")
963 ):
mergify[bot]07175362023-12-21 14:40:52 +0530964 rate = get_incoming_rate(
965 {
966 "item_code": sle.item_code,
967 "warehouse": sle.warehouse,
968 "posting_date": sle.posting_date,
969 "posting_time": sle.posting_time,
970 "qty": sle.actual_qty,
971 "serial_no": sle.get("serial_no"),
972 "batch_no": sle.get("batch_no"),
973 "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
974 "company": sle.company,
975 "voucher_type": sle.voucher_type,
976 "voucher_no": sle.voucher_no,
977 "allow_zero_valuation": self.allow_zero_rate,
978 "sle": sle.name,
979 }
980 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530981
mergify[bot]07175362023-12-21 14:40:52 +0530982 else:
983 rate = get_rate_for_return(
984 sle.voucher_type,
985 sle.voucher_no,
986 sle.item_code,
987 voucher_detail_no=sle.voucher_detail_no,
988 sle=sle,
989 )
rohitwaghchaure63792382024-03-04 12:04:41 +0530990
991 if (
992 sle.get("serial_and_batch_bundle")
993 and rate > 0
994 and sle.voucher_type in ["Delivery Note", "Sales Invoice"]
995 ):
996 frappe.db.set_value(
997 sle.voucher_type + " Item",
998 sle.voucher_detail_no,
999 "incoming_rate",
1000 rate,
1001 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +05301002 elif (
1003 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +05301004 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301005 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +05301006 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301007 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +05301008 else:
1009 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +05301010 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +05301011 elif sle.voucher_type == "Subcontracting Receipt":
1012 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +05301013 else:
1014 rate_field = "incoming_rate"
1015
1016 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +05301017 item_code, incoming_rate = frappe.db.get_value(
1018 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
1019 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301020
1021 if item_code == sle.item_code:
1022 rate = incoming_rate
1023 else:
1024 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1025 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +05301026 elif sle == "Subcontracting Receipt":
1027 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +05301028 else:
1029 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +05301030
Ankush Menat494bd9e2022-03-28 18:52:46 +05301031 rate = frappe.db.get_value(
1032 ref_doctype,
1033 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
1034 rate_field,
1035 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301036
1037 return rate
1038
1039 def update_outgoing_rate_on_transaction(self, sle):
1040 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301041 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
1042 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +05301043 """
1044 if sle.actual_qty and sle.voucher_detail_no:
1045 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
1046
1047 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
1048 self.update_rate_on_stock_entry(sle, outgoing_rate)
1049 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1050 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
1051 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
1052 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301053 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
1054 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +05301055 elif sle.voucher_type == "Stock Reconciliation":
1056 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +05301057
1058 def update_rate_on_stock_entry(self, sle, outgoing_rate):
1059 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
1060
Ankush Menat701878f2022-03-01 18:08:29 +05301061 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
1062 if not sle.dependant_sle_voucher_detail_no:
1063 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +05301064
1065 def recalculate_amounts_in_stock_entry(self, voucher_no):
1066 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +05301067 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
1068 stock_entry.db_update()
1069 for d in stock_entry.items:
1070 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +05301071
Nabin Haita77b8c92020-12-21 14:45:50 +05301072 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
1073 # Update item's incoming rate on transaction
1074 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
1075 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301076 frappe.db.set_value(
1077 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
1078 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301079 else:
1080 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +05301081 frappe.db.set_value(
1082 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +05301083 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301084 "incoming_rate",
1085 outgoing_rate,
1086 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301087
1088 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
1089 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301090 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
1091 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
1092 ):
1093 frappe.db.set_value(
1094 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
1095 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301096 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301097 frappe.db.set_value(
1098 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
1099 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301100
1101 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +05301102 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +05301103 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +05301104 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301105 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +05301106 d.db_update()
1107
Sagar Sharma323bdf82022-05-17 15:14:07 +05301108 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +05301109 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
1110 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301111 else:
1112 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301113 "Subcontracting Receipt Supplied Item",
1114 sle.voucher_detail_no,
1115 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +05301116 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301117
s-aga-ra6cb6c62023-05-03 09:51:58 +05301118 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301119 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +05301120 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301121 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +05301122 d.db_update()
1123
s-aga-r88a3f652023-05-30 16:54:28 +05301124 def update_rate_on_stock_reconciliation(self, sle):
1125 if not sle.serial_no and not sle.batch_no:
1126 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
1127
1128 for item in sr.items:
1129 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +05301130 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +05301131 continue
1132
1133 previous_sle = get_previous_sle(
1134 {
1135 "item_code": item.item_code,
1136 "warehouse": item.warehouse,
1137 "posting_date": sr.posting_date,
1138 "posting_time": sr.posting_time,
1139 "sle": sle.name,
1140 }
1141 )
1142
1143 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
1144 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
1145 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
1146
1147 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +05301148 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +05301149 item.amount_difference = item.amount - item.current_amount
1150 else:
1151 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1152 sr.db_update()
1153
1154 for item in sr.items:
1155 item.db_update()
1156
Nabin Hait328c4f92020-01-02 19:00:32 +05301157 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1158 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301159 all_serial_nos = frappe.get_all(
1160 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1161 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301162
Ankush Menat494bd9e2022-03-28 18:52:46 +05301163 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 +05301164
1165 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301166 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301167 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301168 incoming_rate = frappe.db.sql(
1169 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301170 select incoming_rate
1171 from `tabStock Ledger Entry`
1172 where
1173 company = %s
1174 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301175 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301176 and (serial_no = %s
1177 or serial_no like %s
1178 or serial_no like %s
1179 or serial_no like %s
1180 )
1181 order by posting_date desc
1182 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301183 """,
1184 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1185 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301186
1187 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1188
1189 return incoming_values
1190
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301191 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301192 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301193 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301194 if new_stock_qty >= 0:
1195 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301196 if flt(self.wh_data.qty_after_transaction) <= 0:
1197 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301198 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301199 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1200 actual_qty * sle.incoming_rate
1201 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301202
Nabin Haita77b8c92020-12-21 14:45:50 +05301203 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301204
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301205 elif sle.outgoing_rate:
1206 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301207 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1208 actual_qty * sle.outgoing_rate
1209 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301210
Nabin Haita77b8c92020-12-21 14:45:50 +05301211 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301212 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301213 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301214 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301215 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1216 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301217
Nabin Haita77b8c92020-12-21 14:45:50 +05301218 if not self.wh_data.valuation_rate and actual_qty > 0:
1219 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301220
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301221 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001222 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301223 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301224 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1225 sle.voucher_type, sle.voucher_detail_no
1226 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001227 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301228 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301229
Ankush Menatf089d392022-02-02 12:51:21 +05301230 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301231 incoming_rate = flt(sle.incoming_rate)
1232 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301233 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301234
Ankush Menat494bd9e2022-03-28 18:52:46 +05301235 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1236 self.wh_data.qty_after_transaction + actual_qty
1237 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301238
Ankush Menat97e18a12022-01-15 17:42:25 +05301239 if self.valuation_method == "LIFO":
1240 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1241 else:
1242 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1243
Ankush Menatb534fee2022-02-19 20:58:36 +05301244 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1245
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301246 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301247 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301248 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301249
Ankush Menat4b29fb62021-12-18 18:40:22 +05301250 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301251 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1252 sle.voucher_type, sle.voucher_detail_no
1253 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301254 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301255 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301256 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301257 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301258
Ankush Menat494bd9e2022-03-28 18:52:46 +05301259 stock_queue.remove_stock(
1260 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1261 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301262
Ankush Menatb534fee2022-02-19 20:58:36 +05301263 _qty, stock_value = stock_queue.get_total_stock_and_value()
1264
1265 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301266
Ankush Menat97e18a12022-01-15 17:42:25 +05301267 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301268 self.wh_data.stock_value = round_off_if_near_zero(
1269 self.wh_data.stock_value + stock_value_difference
1270 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301271
Nabin Haita77b8c92020-12-21 14:45:50 +05301272 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301273 self.wh_data.stock_queue.append(
1274 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1275 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301276
Ankush Menatb534fee2022-02-19 20:58:36 +05301277 if self.wh_data.qty_after_transaction:
1278 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1279
Ankush Menatce0514c2022-02-15 11:41:41 +05301280 def update_batched_values(self, sle):
1281 incoming_rate = flt(sle.incoming_rate)
1282 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301283
Ankush Menat494bd9e2022-03-28 18:52:46 +05301284 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1285 self.wh_data.qty_after_transaction + actual_qty
1286 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301287
1288 if actual_qty > 0:
1289 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301290 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301291 outgoing_rate = get_batch_incoming_rate(
1292 item_code=sle.item_code,
1293 warehouse=sle.warehouse,
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301294 batch_no=sle.batch_no,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301295 posting_date=sle.posting_date,
1296 posting_time=sle.posting_time,
1297 creation=sle.creation,
1298 )
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301299
Ankush Menataba7a7c2022-02-19 19:36:28 +05301300 if outgoing_rate is None:
1301 # This can *only* happen if qty available for the batch is zero.
1302 # in such case fall back various other rates.
1303 # future entries will correct the overall accounting as each
1304 # batch individually uses moving average rates.
1305 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301306 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301307
Ankush Menat494bd9e2022-03-28 18:52:46 +05301308 self.wh_data.stock_value = round_off_if_near_zero(
1309 self.wh_data.stock_value + stock_value_difference
1310 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301311 if self.wh_data.qty_after_transaction:
1312 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301313
Javier Wong9b11d9b2017-04-14 18:24:04 +08001314 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301315 ref_item_dt = ""
1316
1317 if voucher_type == "Stock Entry":
1318 ref_item_dt = voucher_type + " Detail"
1319 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1320 ref_item_dt = voucher_type + " Item"
1321
1322 if ref_item_dt:
1323 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1324 else:
1325 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301326
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301327 def get_fallback_rate(self, sle) -> float:
1328 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301329 This should only get used for negative stock."""
1330 return get_valuation_rate(
1331 sle.item_code,
1332 sle.warehouse,
1333 sle.voucher_type,
1334 sle.voucher_no,
1335 self.allow_zero_rate,
1336 currency=erpnext.get_company_currency(sle.company),
1337 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301338 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301339
Nabin Haita77b8c92020-12-21 14:45:50 +05301340 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301341 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301342 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1343 sle = sle[0] if sle else frappe._dict()
1344 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301345
Nabin Haita77b8c92020-12-21 14:45:50 +05301346 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301347 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301348 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301349
1350 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301351 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301352 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301353 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301354
Ankush Menat494bd9e2022-03-28 18:52:46 +05301355 if (
1356 exceptions[0]["voucher_type"],
1357 exceptions[0]["voucher_no"],
1358 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301359
Nabin Haita77b8c92020-12-21 14:45:50 +05301360 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301361 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301362 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1363 frappe.get_desk_link("Warehouse", warehouse),
1364 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301365 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301366 msg = _(
1367 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1368 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301369 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301370 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1371 frappe.get_desk_link("Warehouse", warehouse),
1372 exceptions[0]["posting_date"],
1373 exceptions[0]["posting_time"],
1374 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1375 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301376
Nabin Haita77b8c92020-12-21 14:45:50 +05301377 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301378 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301379 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301380
1381 if allowed_qty > 0:
1382 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1383 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1384 )
1385 else:
1386 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1387 msg,
1388 )
s-aga-rf0acb202023-04-12 14:13:54 +05301389
Nabin Haita77b8c92020-12-21 14:45:50 +05301390 msg_list.append(msg)
1391
1392 if msg_list:
1393 message = "\n\n".join(msg_list)
1394 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301395 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301396 else:
1397 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301398
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301399 def update_bin_data(self, sle):
1400 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301401 values_to_update = {
1402 "actual_qty": sle.qty_after_transaction,
1403 "stock_value": sle.stock_value,
1404 }
1405
1406 if sle.valuation_rate is not None:
1407 values_to_update["valuation_rate"] = sle.valuation_rate
1408
1409 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301410
Nabin Haita77b8c92020-12-21 14:45:50 +05301411 def update_bin(self):
1412 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301413 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301414 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301415
Ankush Menat494bd9e2022-03-28 18:52:46 +05301416 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301417 if data.valuation_rate is not None:
1418 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301419 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301420
marination8418c4b2021-06-22 21:35:25 +05301421
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301422def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301423 """get stock ledger entries filtered by specific posting datetime conditions"""
1424
marination8418c4b2021-06-22 21:35:25 +05301425 if not args.get("posting_date"):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301426 args["posting_datetime"] = "1900-01-01 00:00:00"
1427
1428 if not args.get("posting_datetime"):
1429 args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
1430
marination8418c4b2021-06-22 21:35:25 +05301431 voucher_condition = ""
1432 if exclude_current_voucher:
1433 voucher_no = args.get("voucher_no")
1434 voucher_condition = f"and voucher_no != '{voucher_no}'"
1435
Ankush Menat494bd9e2022-03-28 18:52:46 +05301436 sle = frappe.db.sql(
1437 """
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301438 select *, posting_datetime as "timestamp"
marination8418c4b2021-06-22 21:35:25 +05301439 from `tabStock Ledger Entry`
1440 where item_code = %(item_code)s
1441 and warehouse = %(warehouse)s
1442 and is_cancelled = 0
1443 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301444 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301445 posting_datetime {operator} %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301446 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301447 order by posting_datetime desc, creation desc
marination8418c4b2021-06-22 21:35:25 +05301448 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301449 for update""".format(
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301450 operator=operator,
1451 voucher_condition=voucher_condition,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301452 ),
1453 args,
1454 as_dict=1,
1455 )
marination8418c4b2021-06-22 21:35:25 +05301456
1457 return sle[0] if sle else frappe._dict()
1458
Ankush Menat494bd9e2022-03-28 18:52:46 +05301459
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301460def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301461 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301462 get the last sle on or before the current time-bucket,
1463 to get actual qty before transaction, this function
1464 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301465
Ankush Menat494bd9e2022-03-28 18:52:46 +05301466 args = {
1467 "item_code": "ABC",
1468 "warehouse": "XYZ",
1469 "posting_date": "2012-12-12",
1470 "posting_time": "12:00",
1471 "sle": "name of reference Stock Ledger Entry"
1472 }
Anand Doshi1b531862013-01-10 19:29:51 +05301473 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301474 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301475 sle = get_stock_ledger_entries(
1476 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1477 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301478 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301479
Ankush Menat494bd9e2022-03-28 18:52:46 +05301480
1481def get_stock_ledger_entries(
1482 previous_sle,
1483 operator=None,
1484 order="desc",
1485 limit=None,
1486 for_update=False,
1487 debug=False,
1488 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301489 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301490):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301491 """get stock ledger entries filtered by specific posting datetime conditions"""
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301492 conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator)
Nabin Haitb9ce1042018-02-01 14:58:50 +05301493 if previous_sle.get("warehouse"):
1494 conditions += " and warehouse = %(warehouse)s"
1495 elif previous_sle.get("warehouse_condition"):
1496 conditions += " and " + previous_sle.get("warehouse_condition")
1497
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301498 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301499 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1500 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301501 conditions += (
1502 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301503 (
1504 serial_no = {0}
1505 or serial_no like {1}
1506 or serial_no like {2}
1507 or serial_no like {3}
1508 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301509 """
1510 ).format(
1511 frappe.db.escape(serial_no),
1512 frappe.db.escape("{}\n%".format(serial_no)),
1513 frappe.db.escape("%\n{}".format(serial_no)),
1514 frappe.db.escape("%\n{}\n%".format(serial_no)),
1515 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301516
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301517 if not previous_sle.get("posting_date"):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301518 previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
1519 else:
1520 previous_sle["posting_datetime"] = get_combine_datetime(
1521 previous_sle["posting_date"], previous_sle["posting_time"]
1522 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301523
1524 if operator in (">", "<=") and previous_sle.get("name"):
1525 conditions += " and name!=%(name)s"
1526
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301527 if extra_cond:
1528 conditions += f"{extra_cond}"
1529
Ankush Menat494bd9e2022-03-28 18:52:46 +05301530 return frappe.db.sql(
1531 """
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301532 select *, posting_datetime as "timestamp"
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301533 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301534 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301535 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301536 %(conditions)s
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301537 order by posting_datetime %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301538 %(limit)s %(for_update)s"""
1539 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301540 "conditions": conditions,
1541 "limit": limit or "",
1542 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301543 "order": order,
1544 },
1545 previous_sle,
1546 as_dict=1,
1547 debug=debug,
1548 )
1549
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301550
Nabin Haita77b8c92020-12-21 14:45:50 +05301551def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301552 return frappe.db.get_value(
1553 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301554 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301555 [
1556 "item_code",
1557 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301558 "actual_qty",
1559 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301560 "posting_date",
1561 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301562 "voucher_detail_no",
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301563 "posting_datetime as timestamp",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301564 ],
1565 as_dict=1,
1566 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301567
Ankush Menatce0514c2022-02-15 11:41:41 +05301568
Ankush Menat494bd9e2022-03-28 18:52:46 +05301569def get_batch_incoming_rate(
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301570 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301571):
1572
Ankush Menat102fff22022-02-19 15:51:04 +05301573 sle = frappe.qb.DocType("Stock Ledger Entry")
1574
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301575 timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time)
Ankush Menat102fff22022-02-19 15:51:04 +05301576 if creation:
1577 timestamp_condition |= (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301578 sle.posting_datetime == get_combine_datetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301579 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301580
1581 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301582 frappe.qb.from_(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301583 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301584 .where(
1585 (sle.item_code == item_code)
1586 & (sle.warehouse == warehouse)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301587 & (sle.batch_no == batch_no)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301588 & (sle.is_cancelled == 0)
1589 )
1590 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301591 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301592
1593 if batch_details and batch_details[0].batch_qty:
1594 return batch_details[0].batch_value / batch_details[0].batch_qty
1595
1596
Ankush Menat494bd9e2022-03-28 18:52:46 +05301597def get_valuation_rate(
1598 item_code,
1599 warehouse,
1600 voucher_type,
1601 voucher_no,
1602 allow_zero_rate=False,
1603 currency=None,
1604 company=None,
1605 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301606 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301607 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301608):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301609
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301610 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1611
Ankush Menatf7ffe042021-11-01 13:21:14 +05301612 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301613 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301614
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301615 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1616 table = frappe.qb.DocType("Stock Ledger Entry")
1617 query = (
1618 frappe.qb.from_(table)
1619 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1620 .where(
1621 (table.item_code == item_code)
1622 & (table.warehouse == warehouse)
1623 & (table.batch_no == batch_no)
1624 & (table.is_cancelled == 0)
1625 & (table.voucher_no != voucher_no)
1626 & (table.voucher_type != voucher_type)
1627 )
1628 )
1629
1630 last_valuation_rate = query.run()
1631 if last_valuation_rate:
1632 return flt(last_valuation_rate[0][0])
1633
Ankush Menat342d09a2022-02-19 14:28:51 +05301634 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301635 if warehouse and serial_and_batch_bundle:
1636 batch_obj = BatchNoValuation(
1637 sle=frappe._dict(
1638 {
1639 "item_code": item_code,
1640 "warehouse": warehouse,
1641 "actual_qty": -1,
1642 "serial_and_batch_bundle": serial_and_batch_bundle,
1643 }
1644 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301645 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301646
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301647 return batch_obj.get_incoming_rate()
1648
Ankush Menatf7ffe042021-11-01 13:21:14 +05301649 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301650 if last_valuation_rate := frappe.db.sql(
1651 """select valuation_rate
1652 from `tabStock Ledger Entry` force index (item_warehouse)
1653 where
1654 item_code = %s
1655 AND warehouse = %s
1656 AND valuation_rate >= 0
1657 AND is_cancelled = 0
1658 AND NOT (voucher_no = %s AND voucher_type = %s)
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301659 order by posting_datetime desc, name desc limit 1""",
Akhil Narangdd911aa2023-09-26 13:45:39 +05301660 (item_code, warehouse, voucher_no, voucher_type),
1661 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301662 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301663
1664 # If negative stock allowed, and item delivered without any incoming entry,
1665 # system does not found any SLE, then take valuation rate from Item
1666 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301667
1668 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301669 # try Item Standard rate
1670 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301671
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301672 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301673 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301674 valuation_rate = frappe.db.get_value(
1675 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1676 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301677
Ankush Menat494bd9e2022-03-28 18:52:46 +05301678 if (
1679 not allow_zero_rate
1680 and not valuation_rate
1681 and raise_error_if_no_rate
1682 and cint(erpnext.is_perpetual_inventory_enabled(company))
1683 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301684 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301685
Ankush Menat494bd9e2022-03-28 18:52:46 +05301686 message = _(
1687 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1688 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301689 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301690 solutions = (
1691 "<li>"
1692 + _(
1693 "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."
1694 ).format(voucher_type)
1695 + "</li>"
1696 )
1697 solutions += (
1698 "<li>"
1699 + _("If not, you can Cancel / Submit this entry")
1700 + " {0} ".format(frappe.bold("after"))
1701 + _("performing either one below:")
1702 + "</li>"
1703 )
Marica97715f22020-05-11 20:45:37 +05301704 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1705 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1706 msg = message + solutions + sub_solutions + "</li>"
1707
1708 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301709
1710 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301711
Ankush Menat494bd9e2022-03-28 18:52:46 +05301712
Ankush Menate7109c12021-08-26 16:40:45 +05301713def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301714 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301715 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301716 qty_shift = args.actual_qty
1717
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301718 args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
Ankush Menat7c839c42022-05-06 12:09:08 +05301719
marination8418c4b2021-06-22 21:35:25 +05301720 # find difference/shift in qty caused by stock reconciliation
1721 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301722 qty_shift = get_stock_reco_qty_shift(args)
1723
1724 # find the next nearest stock reco so that we only recalculate SLEs till that point
1725 next_stock_reco_detail = get_next_stock_reco(args)
1726 if next_stock_reco_detail:
1727 detail = next_stock_reco_detail[0]
marination40389772021-07-02 17:13:45 +05301728 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301729
Ankush Menat494bd9e2022-03-28 18:52:46 +05301730 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301731 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301732 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301733 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301734 where
1735 item_code = %(item_code)s
1736 and warehouse = %(warehouse)s
1737 and voucher_no != %(voucher_no)s
1738 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301739 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301740 posting_datetime > %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301741 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301742 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301743 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301744 args,
1745 )
Nabin Hait186a0452021-02-18 14:14:21 +05301746
1747 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1748
Ankush Menat494bd9e2022-03-28 18:52:46 +05301749
marination40389772021-07-02 17:13:45 +05301750def get_stock_reco_qty_shift(args):
1751 stock_reco_qty_shift = 0
1752 if args.get("is_cancelled"):
1753 if args.get("previous_qty_after_transaction"):
1754 # get qty (balance) that was set at submission
1755 last_balance = args.get("previous_qty_after_transaction")
1756 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1757 else:
1758 stock_reco_qty_shift = flt(args.actual_qty)
1759 else:
1760 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301761 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301762 "qty_after_transaction"
1763 )
marination40389772021-07-02 17:13:45 +05301764
1765 if last_balance is not None:
1766 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1767 else:
1768 stock_reco_qty_shift = args.qty_after_transaction
1769
1770 return stock_reco_qty_shift
1771
Ankush Menat494bd9e2022-03-28 18:52:46 +05301772
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301773def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301774 """Returns next nearest stock reconciliaton's details."""
1775
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301776 sle = frappe.qb.DocType("Stock Ledger Entry")
1777
1778 query = (
1779 frappe.qb.from_(sle)
1780 .select(
1781 sle.name,
1782 sle.posting_date,
1783 sle.posting_time,
1784 sle.creation,
1785 sle.voucher_no,
1786 sle.item_code,
1787 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301788 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301789 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301790 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301791 )
1792 .where(
1793 (sle.item_code == kwargs.get("item_code"))
1794 & (sle.warehouse == kwargs.get("warehouse"))
1795 & (sle.voucher_type == "Stock Reconciliation")
1796 & (sle.voucher_no != kwargs.get("voucher_no"))
1797 & (sle.is_cancelled == 0)
1798 & (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301799 sle.posting_datetime
1800 >= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
marination40389772021-07-02 17:13:45 +05301801 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301802 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301803 .orderby(sle.posting_datetime)
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301804 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301805 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301806 )
1807
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301808 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301809 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301810
1811 return query.run(as_dict=True)
1812
marination40389772021-07-02 17:13:45 +05301813
1814def get_datetime_limit_condition(detail):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301815 posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time)
1816
marination40389772021-07-02 17:13:45 +05301817 return f"""
1818 and
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301819 (posting_datetime < '{posting_datetime}'
marination40389772021-07-02 17:13:45 +05301820 or (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301821 posting_datetime = '{posting_datetime}'
marination40389772021-07-02 17:13:45 +05301822 and creation < '{detail.creation}'
1823 )
1824 )"""
1825
Ankush Menat494bd9e2022-03-28 18:52:46 +05301826
Ankush Menate7109c12021-08-26 16:40:45 +05301827def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301828 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301829 return
barredterraeb9ee3f2023-12-05 11:22:55 +01001830 if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
Ankush Menat5eba5752021-12-07 23:03:52 +05301831 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301832
Ankush Menat5eba5752021-12-07 23:03:52 +05301833 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301834
1835 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301836 message = _(
1837 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1838 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301839 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301840 frappe.get_desk_link("Item", args.item_code),
1841 frappe.get_desk_link("Warehouse", args.warehouse),
1842 neg_sle[0]["posting_date"],
1843 neg_sle[0]["posting_time"],
1844 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1845 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301846
Ankush Menat494bd9e2022-03-28 18:52:46 +05301847 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301848
s-aga-rd9e28432023-10-27 16:35:35 +05301849 if args.batch_no:
1850 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1851 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1852 message = _(
1853 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1854 ).format(
1855 abs(neg_batch_sle[0]["cumulative_total"]),
1856 frappe.get_desk_link("Batch", args.batch_no),
1857 frappe.get_desk_link("Warehouse", args.warehouse),
1858 neg_batch_sle[0]["posting_date"],
1859 neg_batch_sle[0]["posting_time"],
1860 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1861 )
1862 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301863
s-aga-r73b65ac2023-11-01 18:35:07 +05301864 if args.reserved_stock:
1865 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301866
Nabin Haita77b8c92020-12-21 14:45:50 +05301867
Maricad6078aa2022-06-17 15:13:13 +05301868def is_negative_with_precision(neg_sle, is_batch=False):
1869 """
1870 Returns whether system precision rounded qty is insufficient.
1871 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1872 """
1873
1874 if not neg_sle:
1875 return False
1876
1877 field = "cumulative_total" if is_batch else "qty_after_transaction"
1878 precision = cint(frappe.db.get_default("float_precision")) or 2
1879 qty_deficit = flt(neg_sle[0][field], precision)
1880
1881 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1882
1883
Nabin Haita77b8c92020-12-21 14:45:50 +05301884def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301885 return frappe.db.sql(
1886 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301887 select
1888 qty_after_transaction, posting_date, posting_time,
1889 voucher_type, voucher_no
1890 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301891 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301892 item_code = %(item_code)s
1893 and warehouse = %(warehouse)s
1894 and voucher_no != %(voucher_no)s
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301895 and posting_datetime >= %(posting_datetime)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301896 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301897 and qty_after_transaction < 0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301898 order by posting_datetime asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301899 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301900 """,
1901 args,
1902 as_dict=1,
1903 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301904
Ankush Menat5eba5752021-12-07 23:03:52 +05301905
1906def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301907 return frappe.db.sql(
1908 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301909 with batch_ledger as (
1910 select
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301911 posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
1912 sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
Ankush Menat5eba5752021-12-07 23:03:52 +05301913 from `tabStock Ledger Entry`
1914 where
1915 item_code = %(item_code)s
1916 and warehouse = %(warehouse)s
1917 and batch_no=%(batch_no)s
1918 and is_cancelled = 0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301919 order by posting_datetime, creation
Ankush Menat5eba5752021-12-07 23:03:52 +05301920 )
1921 select * from batch_ledger
1922 where
1923 cumulative_total < 0.0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301924 and posting_datetime >= %(posting_datetime)s
Ankush Menat5eba5752021-12-07 23:03:52 +05301925 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301926 """,
1927 args,
1928 as_dict=1,
1929 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301930
1931
s-aga-rd9e28432023-10-27 16:35:35 +05301932def validate_reserved_stock(kwargs):
1933 if kwargs.serial_no:
1934 serial_nos = kwargs.serial_no.split("\n")
1935 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1936
s-aga-re1a87a82023-10-31 18:41:58 +05301937 elif kwargs.batch_no:
1938 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1939
s-aga-rd9e28432023-10-27 16:35:35 +05301940 elif kwargs.serial_and_batch_bundle:
1941 sbb_entries = frappe.db.get_all(
1942 "Serial and Batch Entry",
1943 {
1944 "parenttype": "Serial and Batch Bundle",
1945 "parent": kwargs.serial_and_batch_bundle,
1946 "docstatus": 1,
1947 },
s-aga-re1a87a82023-10-31 18:41:58 +05301948 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301949 )
s-aga-rd9e28432023-10-27 16:35:35 +05301950
s-aga-re1a87a82023-10-31 18:41:58 +05301951 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301952 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301953 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1954 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301955
s-aga-r92317062023-11-02 10:36:00 +05301956 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1957 precision = cint(frappe.db.get_default("float_precision")) or 2
1958 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301959
s-aga-r92317062023-11-02 10:36:00 +05301960 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1961 if diff < 0 and abs(diff) > 0.0001:
1962 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1963 abs(diff),
1964 frappe.get_desk_link("Item", kwargs.item_code),
1965 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1966 nowdate(),
1967 nowtime(),
1968 )
1969 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301970
s-aga-rd9e28432023-10-27 16:35:35 +05301971
1972def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1973 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1974 item_code, warehouse, serial_nos
1975 ):
1976 if common_serial_nos := list(
1977 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1978 ):
1979 msg = _(
1980 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1981 )
1982 msg += "<br />"
1983 msg += _("Example: Serial No {0} reserved in {1}.").format(
1984 frappe.bold(common_serial_nos[0]),
1985 frappe.get_desk_link(
1986 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1987 ),
1988 )
1989 frappe.throw(msg, title=_("Reserved Serial No."))
1990
1991
s-aga-re1a87a82023-10-31 18:41:58 +05301992def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1993 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1994 available_batches = get_available_batches(
1995 frappe._dict(
1996 {
1997 "item_code": item_code,
1998 "warehouse": warehouse,
1999 "posting_date": nowdate(),
2000 "posting_time": nowtime(),
2001 }
2002 )
2003 )
2004 available_batches_map = {row.batch_no: row.qty for row in available_batches}
2005 precision = cint(frappe.db.get_default("float_precision")) or 2
2006
2007 for batch_no in batch_nos:
2008 diff = flt(
2009 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
2010 )
2011 if diff < 0 and abs(diff) > 0.0001:
2012 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
2013 abs(diff),
2014 frappe.get_desk_link("Batch", batch_no),
2015 frappe.get_desk_link("Warehouse", warehouse),
2016 nowdate(),
2017 nowtime(),
2018 )
2019 frappe.throw(msg, title=_("Reserved Stock for Batch"))
2020
2021
Ankush Menateb8b4242022-02-12 13:08:28 +05302022def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
2023 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
2024 return True
2025 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
2026 return True
2027 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05302028
2029
2030def get_incoming_rate_for_inter_company_transfer(sle) -> float:
2031 """
2032 For inter company transfer, incoming rate is the average of the outgoing rate
2033 """
2034 rate = 0.0
2035
2036 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
2037
2038 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
2039
2040 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
2041
2042 if reference_name:
2043 rate = frappe.get_cached_value(
2044 doctype,
2045 reference_name,
2046 "incoming_rate",
2047 )
2048
2049 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05302050
2051
2052def is_internal_transfer(sle):
2053 data = frappe.get_cached_value(
2054 sle.voucher_type,
2055 sle.voucher_no,
2056 ["is_internal_supplier", "represents_company", "company"],
2057 as_dict=True,
2058 )
2059
2060 if data.is_internal_supplier and data.represents_company == data.company:
2061 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302062
2063
2064def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
2065 table = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaured80ca522024-02-07 21:56:21 +05302066 posting_datetime = get_combine_datetime(posting_date, posting_time)
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302067
2068 query = (
2069 frappe.qb.from_(table)
2070 .select(Sum(table.stock_value_difference).as_("value"))
2071 .where(
2072 (table.is_cancelled == 0)
2073 & (table.item_code == item_code)
2074 & (table.warehouse == warehouse)
Rohit Waghchaured80ca522024-02-07 21:56:21 +05302075 & (table.posting_datetime <= posting_datetime)
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302076 )
2077 )
2078
2079 if voucher_no:
2080 query = query.where(table.voucher_no != voucher_no)
2081
2082 difference_amount = query.run()
2083 return flt(difference_amount[0][0]) if difference_amount else 0