blob: 2ae6c197a150f10b26d590c0dcc0b03c19ae4c89 [file] [log] [blame]
Maricad6078aa2022-06-17 15:13:13 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait902e8602013-01-08 18:29:24 +05303
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05304import copy
Akhil Narang21c3d9c2023-10-21 11:19:45 +05305import gzip
Nabin Hait26d46552013-01-09 15:23:05 +05306import json
Ankush Menatecdb4932022-04-17 19:06:13 +05307from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05308
9import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053010from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053011from frappe.model.meta import get_field_precision
Rohit Waghchaured80ca522024-02-07 21:56:21 +053012from frappe.query_builder.functions import Sum
Rohit Waghchaure64cb1152024-01-15 19:39:41 +053013from frappe.utils import (
14 cint,
15 cstr,
16 flt,
17 get_link_to_form,
18 getdate,
19 now,
20 nowdate,
21 nowtime,
22 parse_json,
23)
Achilles Rasquinha361366e2018-02-14 17:08:59 +053024
Chillar Anand915b3432021-09-02 16:44:59 +053025import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053026from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
mergify[bot]27a1e3b2023-10-16 19:15:18 +053027from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
s-aga-re1a87a82023-10-31 18:41:58 +053028from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
29 get_available_batches,
30)
31from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
32 get_sre_reserved_batch_nos_details,
s-aga-rd9e28432023-10-27 16:35:35 +053033 get_sre_reserved_serial_nos_details,
34)
Chillar Anand915b3432021-09-02 16:44:59 +053035from erpnext.stock.utils import (
Rohit Waghchaured80ca522024-02-07 21:56:21 +053036 get_combine_datetime,
Chillar Anand915b3432021-09-02 16:44:59 +053037 get_incoming_outgoing_rate_for_cancel,
mergify[bot]07175362023-12-21 14:40:52 +053038 get_incoming_rate,
Deepesh Garg6f107da2021-10-12 20:15:55 +053039 get_or_make_bin,
s-aga-r73b65ac2023-11-01 18:35:07 +053040 get_stock_balance,
Chillar Anand915b3432021-09-02 16:44:59 +053041 get_valuation_method,
42)
Ankush Menatb534fee2022-02-19 20:58:36 +053043from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053044
Nabin Hait97bce3a2021-07-12 13:24:43 +053045
Ankush Menat494bd9e2022-03-28 18:52:46 +053046class NegativeStockError(frappe.ValidationError):
47 pass
48
49
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053050class SerialNoExistsInFutureTransaction(frappe.ValidationError):
51 pass
Nabin Hait902e8602013-01-08 18:29:24 +053052
Anand Doshi5b004ff2013-09-25 19:55:41 +053053
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053054def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053055 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053056
Ankush Menat494bd9e2022-03-28 18:52:46 +053057 args:
58 - allow_negative_stock: disable negative stock valiations if true
59 - via_landed_cost_voucher: landed cost voucher cancels and reposts
60 entries of purchase document. This flag is used to identify if
61 cancellation and repost is happening via landed cost voucher, in
62 such cases certain validations need to be ignored (like negative
63 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053064 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053065 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053066
Nabin Haitca775742013-09-26 16:16:44 +053067 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053068 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053069 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053070 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053071 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053072
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053073 args = get_args_for_future_sle(sl_entries[0])
74 future_sle_exists(args, sl_entries)
75
Nabin Haitca775742013-09-26 16:16:44 +053076 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053077 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053078 validate_serial_no(sle)
79
Nabin Haita77b8c92020-12-21 14:45:50 +053080 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053081 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053082
Ankush Menat494bd9e2022-03-28 18:52:46 +053083 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
84 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
85 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
86 )
87 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053088
Ankush Menat494bd9e2022-03-28 18:52:46 +053089 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
90 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
91 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
92 )
93 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053094
Ankush Menat494bd9e2022-03-28 18:52:46 +053095 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053096 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053097
Nabin Haita77b8c92020-12-21 14:45:50 +053098 args = sle_doc.as_dict()
Rohit Waghchaured80ca522024-02-07 21:56:21 +053099 args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time)
marination40389772021-07-02 17:13:45 +0530100
101 if sle.get("voucher_type") == "Stock Reconciliation":
102 # preserve previous_qty_after_transaction for qty reposting
103 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
104
Ankush Menat494bd9e2022-03-28 18:52:46 +0530105 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +0530106 if is_stock_item:
107 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
s-aga-r73b65ac2023-11-01 18:35:07 +0530108 args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
Ankush Menatcef84c22021-12-03 12:18:59 +0530109 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +0530110 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +0530111 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530112 frappe.msgprint(
113 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
114 )
115
Ankush Menatcef84c22021-12-03 12:18:59 +0530116
117def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
118 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
119 if not args.get("posting_date"):
120 args["posting_date"] = nowdate()
121
marination7a5fd712022-07-04 17:46:54 +0530122 if not (args.get("is_cancelled") and via_landed_cost_voucher):
123 # Reposts only current voucher SL Entries
124 # Updates valuation rate, stock value, stock queue for current transaction
125 update_entries_after(
126 {
127 "item_code": args.get("item_code"),
128 "warehouse": args.get("warehouse"),
129 "posting_date": args.get("posting_date"),
130 "posting_time": args.get("posting_time"),
131 "voucher_type": args.get("voucher_type"),
132 "voucher_no": args.get("voucher_no"),
133 "sle_id": args.get("name"),
134 "creation": args.get("creation"),
s-aga-r73b65ac2023-11-01 18:35:07 +0530135 "reserved_stock": args.get("reserved_stock"),
marination7a5fd712022-07-04 17:46:54 +0530136 },
137 allow_negative_stock=allow_negative_stock,
138 via_landed_cost_voucher=via_landed_cost_voucher,
139 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530140
141 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530142 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530143 update_qty_in_future_sle(args, allow_negative_stock)
144
Nabin Haitadeb9762014-10-06 11:53:52 +0530145
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530146def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530147 return frappe._dict(
148 {
149 "voucher_type": row.get("voucher_type"),
150 "voucher_no": row.get("voucher_no"),
151 "posting_date": row.get("posting_date"),
152 "posting_time": row.get("posting_time"),
153 }
154 )
155
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530156
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530157def validate_serial_no(sle):
158 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530159
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530160 for sn in get_serial_nos(sle.serial_no):
161 args = copy.deepcopy(sle)
162 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530164
165 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530166 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530167 voucher_type = frappe.bold(row.voucher_type)
168 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530169 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530170
171 if vouchers:
172 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530173 msg = (
174 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
175 The list of the transactions are as below."""
176 + "<br><br><ul><li>"
177 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530178
Ankush Menat494bd9e2022-03-28 18:52:46 +0530179 msg += "</li><li>".join(vouchers)
180 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530181
Ankush Menat494bd9e2022-03-28 18:52:46 +0530182 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530183 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
184
Ankush Menat494bd9e2022-03-28 18:52:46 +0530185
Nabin Hait186a0452021-02-18 14:14:21 +0530186def validate_cancellation(args):
187 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530188 repost_entry = frappe.db.get_value(
189 "Repost Item Valuation",
190 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
191 ["name", "status"],
192 as_dict=1,
193 )
Nabin Hait186a0452021-02-18 14:14:21 +0530194
195 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530196 if repost_entry.status == "In Progress":
197 frappe.throw(
198 _(
199 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
200 )
201 )
202 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530203 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530204 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530205 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530206 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530207
Ankush Menat494bd9e2022-03-28 18:52:46 +0530208
Nabin Hait9653f602013-08-20 15:37:33 +0530209def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530210 frappe.db.sql(
211 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530212 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530213 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530214 (now(), frappe.session.user, voucher_type, voucher_no),
215 )
216
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530217
Nabin Hait54c865e2015-03-27 15:38:31 +0530218def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530219 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530220 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530221 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530222 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530223 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530224 sle.submit()
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530225
226 # Added to handle the case when the stock ledger entry is created from the repostig
227 if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
228 sle.db_set("creation", args.get("creation_time"))
229
Nabin Haita77b8c92020-12-21 14:45:50 +0530230 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530231
Ankush Menat494bd9e2022-03-28 18:52:46 +0530232
233def repost_future_sle(
234 args=None,
235 voucher_type=None,
236 voucher_no=None,
237 allow_negative_stock=None,
238 via_landed_cost_voucher=False,
239 doc=None,
240):
Nabin Haite1fa7232022-07-20 15:19:09 +0530241 if not args:
242 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530243
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530244 reposting_data = {}
245 if doc and doc.reposting_data_file:
246 reposting_data = get_reposting_data(doc.reposting_data_file)
247
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530248 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530249 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530250 )
251 if items_to_be_repost:
252 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530253
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530254 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
255 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530256
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530257 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530258 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530259 validate_item_warehouse(args[i])
260
Ankush Menat494bd9e2022-03-28 18:52:46 +0530261 obj = update_entries_after(
262 {
263 "item_code": args[i].get("item_code"),
264 "warehouse": args[i].get("warehouse"),
265 "posting_date": args[i].get("posting_date"),
266 "posting_time": args[i].get("posting_time"),
267 "creation": args[i].get("creation"),
268 "distinct_item_warehouses": distinct_item_warehouses,
269 },
270 allow_negative_stock=allow_negative_stock,
271 via_landed_cost_voucher=via_landed_cost_voucher,
272 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530273 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530274
Ankush Menat494bd9e2022-03-28 18:52:46 +0530275 distinct_item_warehouses[
276 (args[i].get("item_code"), args[i].get("warehouse"))
277 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530278
Nabin Hait97bce3a2021-07-12 13:24:43 +0530279 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530280 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530281 if ("args_idx" not in data and not data.reposting_status) or (
282 data.sle_changed and data.reposting_status
283 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530284 data.args_idx = len(args)
285 args.append(data.sle)
286 elif data.sle_changed and not data.reposting_status:
287 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530288
Nabin Hait97bce3a2021-07-12 13:24:43 +0530289 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530290 i += 1
291
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530292 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530293 update_args_in_repost_item_valuation(
294 doc, i, args, distinct_item_warehouses, affected_transactions
295 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530296
Ankush Menat494bd9e2022-03-28 18:52:46 +0530297
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530298def get_reposting_data(file_path) -> dict:
299 file_name = frappe.db.get_value(
300 "File",
301 {
302 "file_url": file_path,
303 "attached_to_field": "reposting_data_file",
304 },
305 "name",
306 )
307
308 if not file_name:
309 return frappe._dict()
310
311 attached_file = frappe.get_doc("File", file_name)
312
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530313 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530314 if data := json.loads(data.decode("utf-8")):
315 data = data
316
317 return parse_json(data)
318
319
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530320def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530321 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530322 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530323 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530324 frappe.throw(_(validation_msg))
325
Ankush Menat494bd9e2022-03-28 18:52:46 +0530326
Ankush Menatecdb4932022-04-17 19:06:13 +0530327def update_args_in_repost_item_valuation(
328 doc, index, args, distinct_item_warehouses, affected_transactions
329):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530330 if not doc.items_to_be_repost:
331 file_name = ""
332 if doc.reposting_data_file:
333 file_name = get_reposting_file_name(doc.doctype, doc.name)
334 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
335
336 doc.reposting_data_file = create_json_gz_file(
337 {
338 "items_to_be_repost": args,
339 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
340 "affected_transactions": affected_transactions,
341 },
342 doc,
343 file_name,
344 )
345
346 doc.db_set(
347 {
348 "current_index": index,
349 "total_reposting_count": len(args),
350 "reposting_data_file": doc.reposting_data_file,
351 }
352 )
353
354 else:
355 doc.db_set(
356 {
357 "items_to_be_repost": json.dumps(args, default=str),
358 "distinct_item_and_warehouse": json.dumps(
359 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
360 ),
361 "current_index": index,
362 "affected_transactions": frappe.as_json(affected_transactions),
363 }
364 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530365
Ankush Menatecdb4932022-04-17 19:06:13 +0530366 if not frappe.flags.in_test:
367 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530368
Ankush Menat494bd9e2022-03-28 18:52:46 +0530369 frappe.publish_realtime(
370 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530371 {
372 "name": doc.name,
373 "items_to_be_repost": json.dumps(args, default=str),
374 "current_index": index,
375 "total_reposting_count": len(args),
376 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530377 doctype=doc.doctype,
378 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530379 )
380
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530381
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530382def get_reposting_file_name(dt, dn):
383 return frappe.db.get_value(
384 "File",
385 {
386 "attached_to_doctype": dt,
387 "attached_to_name": dn,
388 "attached_to_field": "reposting_data_file",
389 },
390 "name",
391 )
392
393
394def create_json_gz_file(data, doc, file_name=None) -> str:
395 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530396 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530397
398 if not file_name:
399 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
400 _file = frappe.get_doc(
401 {
402 "doctype": "File",
403 "file_name": json_filename,
404 "attached_to_doctype": doc.doctype,
405 "attached_to_name": doc.name,
406 "attached_to_field": "reposting_data_file",
407 "content": compressed_content,
408 "is_private": 1,
409 }
410 )
411 _file.save(ignore_permissions=True)
412
413 return _file.file_url
414 else:
415 file_doc = frappe.get_doc("File", file_name)
416 path = file_doc.get_full_path()
417
418 with open(path, "wb") as f:
419 f.write(compressed_content)
420
421 return doc.reposting_data_file
422
423
424def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
425 if not reposting_data and doc and doc.reposting_data_file:
426 reposting_data = get_reposting_data(doc.reposting_data_file)
427
428 if reposting_data and reposting_data.items_to_be_repost:
429 return reposting_data.items_to_be_repost
430
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530431 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530432
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530433 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530434 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530435
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530436 if not items_to_be_repost and voucher_type and voucher_no:
437 items_to_be_repost = frappe.db.get_all(
438 "Stock Ledger Entry",
439 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
440 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
441 order_by="creation asc",
442 group_by="item_code, warehouse",
443 )
444
Nabin Haite1fa7232022-07-20 15:19:09 +0530445 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530446
Ankush Menat494bd9e2022-03-28 18:52:46 +0530447
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530448def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
449 if not reposting_data and doc and doc.reposting_data_file:
450 reposting_data = get_reposting_data(doc.reposting_data_file)
451
452 if reposting_data and reposting_data.distinct_item_and_warehouse:
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530453 return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530454
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530455 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530456
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530457 if doc and doc.distinct_item_and_warehouse:
458 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530459 distinct_item_warehouses = {
460 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
461 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530462 else:
463 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530464 distinct_item_warehouses.setdefault(
465 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
466 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530467
468 return distinct_item_warehouses
469
Ankush Menat494bd9e2022-03-28 18:52:46 +0530470
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530471def parse_distinct_items_and_warehouses(distinct_items_and_warehouses):
472 new_dict = frappe._dict({})
473
474 # convert string keys to tuple
475 for k, v in distinct_items_and_warehouses.items():
476 new_dict[frappe.safe_eval(k)] = frappe._dict(v)
477
478 return new_dict
479
480
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530481def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
482 if not reposting_data and doc and doc.reposting_data_file:
483 reposting_data = get_reposting_data(doc.reposting_data_file)
484
485 if reposting_data and reposting_data.affected_transactions:
486 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
487
Ankush Menatecdb4932022-04-17 19:06:13 +0530488 if not doc.affected_transactions:
489 return set()
490
491 transactions = frappe.parse_json(doc.affected_transactions)
492 return {tuple(transaction) for transaction in transactions}
493
494
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530495def get_current_index(doc=None):
496 if doc and doc.current_index:
497 return doc.current_index
498
Ankush Menat494bd9e2022-03-28 18:52:46 +0530499
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530500class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530501 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530502 update valution rate and qty after transaction
503 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530504
Ankush Menat494bd9e2022-03-28 18:52:46 +0530505 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530506
Ankush Menat494bd9e2022-03-28 18:52:46 +0530507 args = {
508 "item_code": "ABC",
509 "warehouse": "XYZ",
510 "posting_date": "2012-12-12",
511 "posting_time": "12:00"
512 }
Nabin Hait902e8602013-01-08 18:29:24 +0530513 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530514
515 def __init__(
516 self,
517 args,
518 allow_zero_rate=False,
519 allow_negative_stock=None,
520 via_landed_cost_voucher=False,
521 verbose=1,
522 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530523 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530524 self.verbose = verbose
525 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530526 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530527 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530528 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
529 item_code=self.item_code
530 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530531
Nabin Haita77b8c92020-12-21 14:45:50 +0530532 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530533 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530534 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530535
Nabin Haita77b8c92020-12-21 14:45:50 +0530536 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530537 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530538 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530539
540 self.new_items_found = False
541 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530542 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-r73b65ac2023-11-01 18:35:07 +0530543 self.reserved_stock = flt(self.args.reserved_stock)
Nabin Haita77b8c92020-12-21 14:45:50 +0530544
545 self.data = frappe._dict()
546 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530547 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530548
Maricad6078aa2022-06-17 15:13:13 +0530549 def set_precision(self):
550 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
551 self.currency_precision = get_field_precision(
552 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530553 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530554
555 def initialize_previous_data(self, args):
556 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530557 Get previous sl entries for current item for each related warehouse
558 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530559
Ankush Menat494bd9e2022-03-28 18:52:46 +0530560 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530561
Ankush Menat494bd9e2022-03-28 18:52:46 +0530562 self.data = {
563 warehouse1: {
564 'previus_sle': {},
565 'qty_after_transaction': 10,
566 'valuation_rate': 100,
567 'stock_value': 1000,
568 'prev_stock_value': 1000,
569 'stock_queue': '[[10, 100]]',
570 'stock_value_difference': 1000
571 }
572 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530573
574 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530575 self.data.setdefault(args.warehouse, frappe._dict())
576 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530577 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530578 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530579
Ankush Menatc1d986a2021-08-31 19:43:42 +0530580 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
581 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
582
Ankush Menat494bd9e2022-03-28 18:52:46 +0530583 warehouse_dict.update(
584 {
585 "prev_stock_value": previous_sle.stock_value or 0.0,
586 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
587 "stock_value_difference": 0.0,
588 }
589 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530590
Nabin Haita77b8c92020-12-21 14:45:50 +0530591 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530592 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530593
Nabin Haita77b8c92020-12-21 14:45:50 +0530594 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530595 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530596 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530597 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530598 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530599 entries_to_fix = self.get_future_entries_to_fix()
600
601 i = 0
602 while i < len(entries_to_fix):
603 sle = entries_to_fix[i]
604 i += 1
605
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530606 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530607 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530608
Nabin Haita77b8c92020-12-21 14:45:50 +0530609 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530610 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530611
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530612 if self.exceptions:
613 self.raise_exceptions()
614
Nabin Hait186a0452021-02-18 14:14:21 +0530615 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530616 sl_entries = self.get_sle_against_current_voucher()
617 for sle in sl_entries:
618 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530619
Nabin Haita77b8c92020-12-21 14:45:50 +0530620 def get_sle_against_current_voucher(self):
Rohit Waghchaured80ca522024-02-07 21:56:21 +0530621 self.args["posting_datetime"] = get_combine_datetime(
622 self.args.posting_date, self.args.posting_time
623 )
Nabin Haitf2be0802021-02-15 19:27:49 +0530624
Ankush Menat494bd9e2022-03-28 18:52:46 +0530625 return frappe.db.sql(
626 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530627 select
Rohit Waghchaured80ca522024-02-07 21:56:21 +0530628 *, posting_datetime as "timestamp"
Nabin Haita77b8c92020-12-21 14:45:50 +0530629 from
630 `tabStock Ledger Entry`
631 where
632 item_code = %(item_code)s
633 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530634 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530635 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +0530636 posting_datetime = %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530637 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530638 order by
Rohit Waghchaurea73ba2c2024-02-21 17:32:02 +0530639 creation ASC
Nabin Haita77b8c92020-12-21 14:45:50 +0530640 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530641 """,
642 self.args,
643 as_dict=1,
644 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530645
Nabin Haita77b8c92020-12-21 14:45:50 +0530646 def get_future_entries_to_fix(self):
647 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530648 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
649 {"item_code": self.item_code, "warehouse": self.args.warehouse}
650 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530651
Nabin Haita77b8c92020-12-21 14:45:50 +0530652 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530653
Nabin Haita77b8c92020-12-21 14:45:50 +0530654 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530655 dependant_sle = get_sle_by_voucher_detail_no(
656 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
657 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530658
Nabin Haita77b8c92020-12-21 14:45:50 +0530659 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530660 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530661 elif (
662 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
663 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530664 return entries_to_fix
665 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530666 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530667 return entries_to_fix
668 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
669 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530670 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530671 self.initialize_previous_data(dependant_sle)
672 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530673 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530674
675 def update_distinct_item_warehouses(self, dependant_sle):
676 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530677 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530678
Nabin Hait97bce3a2021-07-12 13:24:43 +0530679 if key not in self.distinct_item_warehouses:
680 self.distinct_item_warehouses[key] = val
681 self.new_items_found = True
682 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530683 existing_sle_posting_date = (
684 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
685 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530686
687 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
688
Nabin Hait97bce3a2021-07-12 13:24:43 +0530689 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
690 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530691 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
692 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530693 self.distinct_item_warehouses[key] = val
694 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530695 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
696 # Future dependent voucher needs to be repost to get the correct stock value
697 # If dependent voucher has not reposted, then add it to the list
698 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530699 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530700 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
701 self.distinct_item_warehouses[key] = val
702
703 def get_dependent_voucher_detail_nos(self, key):
704 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
705 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
706
707 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530708
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530709 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530710 # previous sle data for this warehouse
711 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530712 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530713
Anand Doshi0dc79f42015-04-06 12:59:34 +0530714 if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530715 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530716 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530717 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530718 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530719 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530720
Nabin Haita77b8c92020-12-21 14:45:50 +0530721 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530722 if not self.args.get("sle_id"):
723 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530724
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530725 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530726 sle.voucher_type == "Stock Reconciliation"
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530727 and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530728 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530729 and not self.args.get("sle_id")
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530730 and sle.is_cancelled == 0
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530731 ):
732 self.reset_actual_qty_for_stock_reco(sle)
733
734 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530735 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
736 and sle.voucher_detail_no
737 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530738 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530739 ):
740 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
741
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530742 dimensions = get_inventory_dimensions()
743 has_dimensions = False
744 if dimensions:
745 for dimension in dimensions:
746 if sle.get(dimension.get("fieldname")):
747 has_dimensions = True
748
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530749 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530750 self.calculate_valuation_for_serial_batch_bundle(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530751 elif sle.serial_no and not self.args.get("sle_id"):
752 # Only run in reposting
753 self.get_serialized_values(sle)
754 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
755 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
756 self.wh_data.qty_after_transaction = sle.qty_after_transaction
757
758 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
759 self.wh_data.valuation_rate
760 )
761 elif (
762 sle.batch_no
763 and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
764 and not self.args.get("sle_id")
765 ):
766 # Only run in reposting
767 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530768 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530769 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530770 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530771 self.wh_data.valuation_rate = sle.valuation_rate
772 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530773 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
774 self.wh_data.valuation_rate
775 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530776 if self.valuation_method != "Moving Average":
777 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530778 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530779 if self.valuation_method == "Moving Average":
780 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530781 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530782 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
783 self.wh_data.valuation_rate
784 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530785 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530786 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530787
Rushabh Mehta54047782013-12-26 11:07:46 +0530788 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530789 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530790 if not self.wh_data.qty_after_transaction:
791 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530792
Nabin Haita77b8c92020-12-21 14:45:50 +0530793 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
794 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530795
Nabin Hait902e8602013-01-08 18:29:24 +0530796 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530797 sle.qty_after_transaction = self.wh_data.qty_after_transaction
798 sle.valuation_rate = self.wh_data.valuation_rate
799 sle.stock_value = self.wh_data.stock_value
800 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530801
rohitwaghchaurea8216b92023-11-09 12:22:26 +0530802 if not sle.is_adjustment_entry or not self.args.get("sle_id"):
803 sle.stock_value_difference = stock_value_difference
804
805 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530806 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530807
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530808 if not self.args.get("sle_id") or (
809 sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
810 ):
Ankush Menat701878f2022-03-01 18:08:29 +0530811 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530812
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530813 def get_serialized_values(self, sle):
814 incoming_rate = flt(sle.incoming_rate)
815 actual_qty = flt(sle.actual_qty)
816 serial_nos = cstr(sle.serial_no).split("\n")
817
818 if incoming_rate < 0:
819 # wrong incoming rate
820 incoming_rate = self.wh_data.valuation_rate
821
822 stock_value_change = 0
823 if actual_qty > 0:
824 stock_value_change = actual_qty * incoming_rate
825 else:
826 # In case of delivery/stock issue, get average purchase rate
827 # of serial nos of current entry
828 if not sle.is_cancelled:
829 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
830 stock_value_change = -1 * outgoing_value
831 else:
832 stock_value_change = actual_qty * sle.outgoing_rate
833
834 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
835
836 if new_stock_qty > 0:
837 new_stock_value = (
838 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
839 ) + stock_value_change
840 if new_stock_value >= 0:
841 # calculate new valuation rate only if stock value is positive
842 # else it remains the same as that of previous entry
843 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
844
845 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
846 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
847 sle.voucher_type, sle.voucher_detail_no
848 )
849 if not allow_zero_rate:
850 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
851
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530852 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530853 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
854 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
855
856 if sle.actual_qty < 0:
857 sle.actual_qty = (
858 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
859 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530860 )
861
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530862 if abs(sle.actual_qty) == 0.0:
863 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530864
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530865 if sle.serial_and_batch_bundle and frappe.get_cached_value(
866 "Item", sle.item_code, "has_serial_no"
867 ):
868 self.update_serial_no_status(sle)
869
870 def update_serial_no_status(self, sle):
871 from erpnext.stock.serial_batch_bundle import get_serial_nos
872
873 serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
874 if not serial_nos:
875 return
876
877 warehouse = None
878 status = "Inactive"
879
880 if sle.actual_qty > 0:
881 warehouse = sle.warehouse
882 status = "Active"
883
884 sn_table = frappe.qb.DocType("Serial No")
885
886 query = (
887 frappe.qb.update(sn_table)
888 .set(sn_table.warehouse, warehouse)
889 .set(sn_table.status, status)
890 .where(sn_table.name.isin(serial_nos))
891 )
892
893 query.run()
894
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530895 def calculate_valuation_for_serial_batch_bundle(self, sle):
896 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
897
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530898 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530899 doc.calculate_qty_and_amount(save=True)
900
901 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
902
s-aga-r1e15a3c2024-02-02 13:07:26 +0530903 precision = doc.precision("total_qty")
904 self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
Rohit Waghchauree8ae4ed2024-02-19 22:18:57 +0530905 if flt(self.wh_data.qty_after_transaction, precision):
s-aga-r1e15a3c2024-02-02 13:07:26 +0530906 self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
907 self.wh_data.qty_after_transaction, precision
908 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530909
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530910 def validate_negative_stock(self, sle):
911 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530912 validate negative stock for entries current datetime onwards
913 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530914 """
s-aga-rf0acb202023-04-12 14:13:54 +0530915 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530916 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530917
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530918 if diff < 0 and abs(diff) > 0.0001:
919 # negative stock!
920 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530921 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530922 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530923 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530924 return True
925
Nabin Haita77b8c92020-12-21 14:45:50 +0530926 def get_dynamic_incoming_outgoing_rate(self, sle):
927 # Get updated incoming/outgoing rate from transaction
928 if sle.recalculate_rate:
929 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
930
931 if flt(sle.actual_qty) >= 0:
932 sle.incoming_rate = rate
933 else:
934 sle.outgoing_rate = rate
935
936 def get_incoming_outgoing_rate_from_transaction(self, sle):
937 rate = 0
938 # Material Transfer, Repack, Manufacturing
939 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530940 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530941 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
942 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530943 elif sle.voucher_type in (
944 "Purchase Receipt",
945 "Purchase Invoice",
946 "Delivery Note",
947 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530948 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530949 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530950 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530951 from erpnext.controllers.sales_and_purchase_return import (
952 get_rate_for_return, # don't move this import to top
953 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530954
mergify[bot]07175362023-12-21 14:40:52 +0530955 if self.valuation_method == "Moving Average":
956 rate = get_incoming_rate(
957 {
958 "item_code": sle.item_code,
959 "warehouse": sle.warehouse,
960 "posting_date": sle.posting_date,
961 "posting_time": sle.posting_time,
962 "qty": sle.actual_qty,
963 "serial_no": sle.get("serial_no"),
964 "batch_no": sle.get("batch_no"),
965 "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
966 "company": sle.company,
967 "voucher_type": sle.voucher_type,
968 "voucher_no": sle.voucher_no,
969 "allow_zero_valuation": self.allow_zero_rate,
970 "sle": sle.name,
971 }
972 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530973
mergify[bot]07175362023-12-21 14:40:52 +0530974 else:
975 rate = get_rate_for_return(
976 sle.voucher_type,
977 sle.voucher_no,
978 sle.item_code,
979 voucher_detail_no=sle.voucher_detail_no,
980 sle=sle,
981 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530982 elif (
983 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530984 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530985 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530986 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530987 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530988 else:
989 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530990 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530991 elif sle.voucher_type == "Subcontracting Receipt":
992 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530993 else:
994 rate_field = "incoming_rate"
995
996 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530997 item_code, incoming_rate = frappe.db.get_value(
998 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
999 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301000
1001 if item_code == sle.item_code:
1002 rate = incoming_rate
1003 else:
1004 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1005 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +05301006 elif sle == "Subcontracting Receipt":
1007 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +05301008 else:
1009 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +05301010
Ankush Menat494bd9e2022-03-28 18:52:46 +05301011 rate = frappe.db.get_value(
1012 ref_doctype,
1013 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
1014 rate_field,
1015 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301016
1017 return rate
1018
1019 def update_outgoing_rate_on_transaction(self, sle):
1020 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301021 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
1022 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +05301023 """
1024 if sle.actual_qty and sle.voucher_detail_no:
1025 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
1026
1027 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
1028 self.update_rate_on_stock_entry(sle, outgoing_rate)
1029 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1030 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
1031 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
1032 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301033 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
1034 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +05301035 elif sle.voucher_type == "Stock Reconciliation":
1036 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +05301037
1038 def update_rate_on_stock_entry(self, sle, outgoing_rate):
1039 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
1040
Ankush Menat701878f2022-03-01 18:08:29 +05301041 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
1042 if not sle.dependant_sle_voucher_detail_no:
1043 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +05301044
1045 def recalculate_amounts_in_stock_entry(self, voucher_no):
1046 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +05301047 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
1048 stock_entry.db_update()
1049 for d in stock_entry.items:
1050 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +05301051
Nabin Haita77b8c92020-12-21 14:45:50 +05301052 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
1053 # Update item's incoming rate on transaction
1054 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
1055 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301056 frappe.db.set_value(
1057 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
1058 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301059 else:
1060 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +05301061 frappe.db.set_value(
1062 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +05301063 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301064 "incoming_rate",
1065 outgoing_rate,
1066 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301067
1068 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
1069 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301070 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
1071 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
1072 ):
1073 frappe.db.set_value(
1074 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
1075 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301076 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301077 frappe.db.set_value(
1078 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
1079 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301080
1081 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +05301082 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +05301083 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +05301084 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301085 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +05301086 d.db_update()
1087
Sagar Sharma323bdf82022-05-17 15:14:07 +05301088 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +05301089 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
1090 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301091 else:
1092 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301093 "Subcontracting Receipt Supplied Item",
1094 sle.voucher_detail_no,
1095 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +05301096 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301097
s-aga-ra6cb6c62023-05-03 09:51:58 +05301098 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301099 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +05301100 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301101 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +05301102 d.db_update()
1103
s-aga-r88a3f652023-05-30 16:54:28 +05301104 def update_rate_on_stock_reconciliation(self, sle):
1105 if not sle.serial_no and not sle.batch_no:
1106 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
1107
1108 for item in sr.items:
1109 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +05301110 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +05301111 continue
1112
1113 previous_sle = get_previous_sle(
1114 {
1115 "item_code": item.item_code,
1116 "warehouse": item.warehouse,
1117 "posting_date": sr.posting_date,
1118 "posting_time": sr.posting_time,
1119 "sle": sle.name,
1120 }
1121 )
1122
1123 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
1124 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
1125 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
1126
1127 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +05301128 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +05301129 item.amount_difference = item.amount - item.current_amount
1130 else:
1131 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1132 sr.db_update()
1133
1134 for item in sr.items:
1135 item.db_update()
1136
Nabin Hait328c4f92020-01-02 19:00:32 +05301137 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1138 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301139 all_serial_nos = frappe.get_all(
1140 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1141 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301142
Ankush Menat494bd9e2022-03-28 18:52:46 +05301143 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 +05301144
1145 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301146 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301147 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301148 incoming_rate = frappe.db.sql(
1149 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301150 select incoming_rate
1151 from `tabStock Ledger Entry`
1152 where
1153 company = %s
1154 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301155 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301156 and (serial_no = %s
1157 or serial_no like %s
1158 or serial_no like %s
1159 or serial_no like %s
1160 )
1161 order by posting_date desc
1162 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301163 """,
1164 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1165 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301166
1167 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1168
1169 return incoming_values
1170
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301171 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301172 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301173 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301174 if new_stock_qty >= 0:
1175 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301176 if flt(self.wh_data.qty_after_transaction) <= 0:
1177 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301178 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301179 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1180 actual_qty * sle.incoming_rate
1181 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301182
Nabin Haita77b8c92020-12-21 14:45:50 +05301183 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301184
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301185 elif sle.outgoing_rate:
1186 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301187 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1188 actual_qty * sle.outgoing_rate
1189 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301190
Nabin Haita77b8c92020-12-21 14:45:50 +05301191 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301192 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301193 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301194 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301195 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1196 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301197
Nabin Haita77b8c92020-12-21 14:45:50 +05301198 if not self.wh_data.valuation_rate and actual_qty > 0:
1199 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301200
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301201 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001202 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301203 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301204 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1205 sle.voucher_type, sle.voucher_detail_no
1206 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001207 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301208 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301209
Ankush Menatf089d392022-02-02 12:51:21 +05301210 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301211 incoming_rate = flt(sle.incoming_rate)
1212 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301213 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301214
Ankush Menat494bd9e2022-03-28 18:52:46 +05301215 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1216 self.wh_data.qty_after_transaction + actual_qty
1217 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301218
Ankush Menat97e18a12022-01-15 17:42:25 +05301219 if self.valuation_method == "LIFO":
1220 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1221 else:
1222 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1223
Ankush Menatb534fee2022-02-19 20:58:36 +05301224 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1225
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301226 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301227 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301228 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301229
Ankush Menat4b29fb62021-12-18 18:40:22 +05301230 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301231 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1232 sle.voucher_type, sle.voucher_detail_no
1233 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301234 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301235 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301236 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301237 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301238
Ankush Menat494bd9e2022-03-28 18:52:46 +05301239 stock_queue.remove_stock(
1240 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1241 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301242
Ankush Menatb534fee2022-02-19 20:58:36 +05301243 _qty, stock_value = stock_queue.get_total_stock_and_value()
1244
1245 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301246
Ankush Menat97e18a12022-01-15 17:42:25 +05301247 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301248 self.wh_data.stock_value = round_off_if_near_zero(
1249 self.wh_data.stock_value + stock_value_difference
1250 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301251
Nabin Haita77b8c92020-12-21 14:45:50 +05301252 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301253 self.wh_data.stock_queue.append(
1254 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1255 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301256
Ankush Menatb534fee2022-02-19 20:58:36 +05301257 if self.wh_data.qty_after_transaction:
1258 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1259
Ankush Menatce0514c2022-02-15 11:41:41 +05301260 def update_batched_values(self, sle):
1261 incoming_rate = flt(sle.incoming_rate)
1262 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301263
Ankush Menat494bd9e2022-03-28 18:52:46 +05301264 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1265 self.wh_data.qty_after_transaction + actual_qty
1266 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301267
1268 if actual_qty > 0:
1269 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301270 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301271 outgoing_rate = get_batch_incoming_rate(
1272 item_code=sle.item_code,
1273 warehouse=sle.warehouse,
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301274 batch_no=sle.batch_no,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301275 posting_date=sle.posting_date,
1276 posting_time=sle.posting_time,
1277 creation=sle.creation,
1278 )
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301279
Ankush Menataba7a7c2022-02-19 19:36:28 +05301280 if outgoing_rate is None:
1281 # This can *only* happen if qty available for the batch is zero.
1282 # in such case fall back various other rates.
1283 # future entries will correct the overall accounting as each
1284 # batch individually uses moving average rates.
1285 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301286 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301287
Ankush Menat494bd9e2022-03-28 18:52:46 +05301288 self.wh_data.stock_value = round_off_if_near_zero(
1289 self.wh_data.stock_value + stock_value_difference
1290 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301291 if self.wh_data.qty_after_transaction:
1292 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301293
Javier Wong9b11d9b2017-04-14 18:24:04 +08001294 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301295 ref_item_dt = ""
1296
1297 if voucher_type == "Stock Entry":
1298 ref_item_dt = voucher_type + " Detail"
1299 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1300 ref_item_dt = voucher_type + " Item"
1301
1302 if ref_item_dt:
1303 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1304 else:
1305 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301306
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301307 def get_fallback_rate(self, sle) -> float:
1308 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301309 This should only get used for negative stock."""
1310 return get_valuation_rate(
1311 sle.item_code,
1312 sle.warehouse,
1313 sle.voucher_type,
1314 sle.voucher_no,
1315 self.allow_zero_rate,
1316 currency=erpnext.get_company_currency(sle.company),
1317 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301318 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301319
Nabin Haita77b8c92020-12-21 14:45:50 +05301320 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301321 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301322 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1323 sle = sle[0] if sle else frappe._dict()
1324 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301325
Nabin Haita77b8c92020-12-21 14:45:50 +05301326 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301327 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301328 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301329
1330 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301331 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301332 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301333 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301334
Ankush Menat494bd9e2022-03-28 18:52:46 +05301335 if (
1336 exceptions[0]["voucher_type"],
1337 exceptions[0]["voucher_no"],
1338 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301339
Nabin Haita77b8c92020-12-21 14:45:50 +05301340 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301341 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301342 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1343 frappe.get_desk_link("Warehouse", warehouse),
1344 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301345 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301346 msg = _(
1347 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1348 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301349 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301350 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1351 frappe.get_desk_link("Warehouse", warehouse),
1352 exceptions[0]["posting_date"],
1353 exceptions[0]["posting_time"],
1354 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1355 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301356
Nabin Haita77b8c92020-12-21 14:45:50 +05301357 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301358 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301359 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301360
1361 if allowed_qty > 0:
1362 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1363 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1364 )
1365 else:
1366 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1367 msg,
1368 )
s-aga-rf0acb202023-04-12 14:13:54 +05301369
Nabin Haita77b8c92020-12-21 14:45:50 +05301370 msg_list.append(msg)
1371
1372 if msg_list:
1373 message = "\n\n".join(msg_list)
1374 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301375 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301376 else:
1377 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301378
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301379 def update_bin_data(self, sle):
1380 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301381 values_to_update = {
1382 "actual_qty": sle.qty_after_transaction,
1383 "stock_value": sle.stock_value,
1384 }
1385
1386 if sle.valuation_rate is not None:
1387 values_to_update["valuation_rate"] = sle.valuation_rate
1388
1389 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301390
Nabin Haita77b8c92020-12-21 14:45:50 +05301391 def update_bin(self):
1392 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301393 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301394 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301395
Ankush Menat494bd9e2022-03-28 18:52:46 +05301396 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301397 if data.valuation_rate is not None:
1398 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301399 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301400
marination8418c4b2021-06-22 21:35:25 +05301401
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301402def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301403 """get stock ledger entries filtered by specific posting datetime conditions"""
1404
marination8418c4b2021-06-22 21:35:25 +05301405 if not args.get("posting_date"):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301406 args["posting_datetime"] = "1900-01-01 00:00:00"
1407
1408 if not args.get("posting_datetime"):
1409 args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
1410
marination8418c4b2021-06-22 21:35:25 +05301411 voucher_condition = ""
1412 if exclude_current_voucher:
1413 voucher_no = args.get("voucher_no")
1414 voucher_condition = f"and voucher_no != '{voucher_no}'"
1415
Ankush Menat494bd9e2022-03-28 18:52:46 +05301416 sle = frappe.db.sql(
1417 """
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301418 select *, posting_datetime as "timestamp"
marination8418c4b2021-06-22 21:35:25 +05301419 from `tabStock Ledger Entry`
1420 where item_code = %(item_code)s
1421 and warehouse = %(warehouse)s
1422 and is_cancelled = 0
1423 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301424 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301425 posting_datetime {operator} %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301426 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301427 order by posting_datetime desc, creation desc
marination8418c4b2021-06-22 21:35:25 +05301428 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301429 for update""".format(
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301430 operator=operator,
1431 voucher_condition=voucher_condition,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301432 ),
1433 args,
1434 as_dict=1,
1435 )
marination8418c4b2021-06-22 21:35:25 +05301436
1437 return sle[0] if sle else frappe._dict()
1438
Ankush Menat494bd9e2022-03-28 18:52:46 +05301439
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301440def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301441 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301442 get the last sle on or before the current time-bucket,
1443 to get actual qty before transaction, this function
1444 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301445
Ankush Menat494bd9e2022-03-28 18:52:46 +05301446 args = {
1447 "item_code": "ABC",
1448 "warehouse": "XYZ",
1449 "posting_date": "2012-12-12",
1450 "posting_time": "12:00",
1451 "sle": "name of reference Stock Ledger Entry"
1452 }
Anand Doshi1b531862013-01-10 19:29:51 +05301453 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301454 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301455 sle = get_stock_ledger_entries(
1456 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1457 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301458 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301459
Ankush Menat494bd9e2022-03-28 18:52:46 +05301460
1461def get_stock_ledger_entries(
1462 previous_sle,
1463 operator=None,
1464 order="desc",
1465 limit=None,
1466 for_update=False,
1467 debug=False,
1468 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301469 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301470):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301471 """get stock ledger entries filtered by specific posting datetime conditions"""
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301472 conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator)
Nabin Haitb9ce1042018-02-01 14:58:50 +05301473 if previous_sle.get("warehouse"):
1474 conditions += " and warehouse = %(warehouse)s"
1475 elif previous_sle.get("warehouse_condition"):
1476 conditions += " and " + previous_sle.get("warehouse_condition")
1477
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301478 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301479 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1480 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301481 conditions += (
1482 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301483 (
1484 serial_no = {0}
1485 or serial_no like {1}
1486 or serial_no like {2}
1487 or serial_no like {3}
1488 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301489 """
1490 ).format(
1491 frappe.db.escape(serial_no),
1492 frappe.db.escape("{}\n%".format(serial_no)),
1493 frappe.db.escape("%\n{}".format(serial_no)),
1494 frappe.db.escape("%\n{}\n%".format(serial_no)),
1495 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301496
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301497 if not previous_sle.get("posting_date"):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301498 previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
1499 else:
1500 previous_sle["posting_datetime"] = get_combine_datetime(
1501 previous_sle["posting_date"], previous_sle["posting_time"]
1502 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301503
1504 if operator in (">", "<=") and previous_sle.get("name"):
1505 conditions += " and name!=%(name)s"
1506
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301507 if extra_cond:
1508 conditions += f"{extra_cond}"
1509
Ankush Menat494bd9e2022-03-28 18:52:46 +05301510 return frappe.db.sql(
1511 """
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301512 select *, posting_datetime as "timestamp"
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301513 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301514 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301515 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301516 %(conditions)s
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301517 order by posting_datetime %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301518 %(limit)s %(for_update)s"""
1519 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301520 "conditions": conditions,
1521 "limit": limit or "",
1522 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301523 "order": order,
1524 },
1525 previous_sle,
1526 as_dict=1,
1527 debug=debug,
1528 )
1529
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301530
Nabin Haita77b8c92020-12-21 14:45:50 +05301531def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301532 return frappe.db.get_value(
1533 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301534 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301535 [
1536 "item_code",
1537 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301538 "actual_qty",
1539 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301540 "posting_date",
1541 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301542 "voucher_detail_no",
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301543 "posting_datetime as timestamp",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301544 ],
1545 as_dict=1,
1546 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301547
Ankush Menatce0514c2022-02-15 11:41:41 +05301548
Ankush Menat494bd9e2022-03-28 18:52:46 +05301549def get_batch_incoming_rate(
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301550 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301551):
1552
Ankush Menat102fff22022-02-19 15:51:04 +05301553 sle = frappe.qb.DocType("Stock Ledger Entry")
1554
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301555 timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time)
Ankush Menat102fff22022-02-19 15:51:04 +05301556 if creation:
1557 timestamp_condition |= (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301558 sle.posting_datetime == get_combine_datetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301559 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301560
1561 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301562 frappe.qb.from_(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301563 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301564 .where(
1565 (sle.item_code == item_code)
1566 & (sle.warehouse == warehouse)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301567 & (sle.batch_no == batch_no)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301568 & (sle.is_cancelled == 0)
1569 )
1570 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301571 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301572
1573 if batch_details and batch_details[0].batch_qty:
1574 return batch_details[0].batch_value / batch_details[0].batch_qty
1575
1576
Ankush Menat494bd9e2022-03-28 18:52:46 +05301577def get_valuation_rate(
1578 item_code,
1579 warehouse,
1580 voucher_type,
1581 voucher_no,
1582 allow_zero_rate=False,
1583 currency=None,
1584 company=None,
1585 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301586 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301587 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301588):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301589
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301590 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1591
Ankush Menatf7ffe042021-11-01 13:21:14 +05301592 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301593 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301594
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301595 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1596 table = frappe.qb.DocType("Stock Ledger Entry")
1597 query = (
1598 frappe.qb.from_(table)
1599 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1600 .where(
1601 (table.item_code == item_code)
1602 & (table.warehouse == warehouse)
1603 & (table.batch_no == batch_no)
1604 & (table.is_cancelled == 0)
1605 & (table.voucher_no != voucher_no)
1606 & (table.voucher_type != voucher_type)
1607 )
1608 )
1609
1610 last_valuation_rate = query.run()
1611 if last_valuation_rate:
1612 return flt(last_valuation_rate[0][0])
1613
Ankush Menat342d09a2022-02-19 14:28:51 +05301614 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301615 if warehouse and serial_and_batch_bundle:
1616 batch_obj = BatchNoValuation(
1617 sle=frappe._dict(
1618 {
1619 "item_code": item_code,
1620 "warehouse": warehouse,
1621 "actual_qty": -1,
1622 "serial_and_batch_bundle": serial_and_batch_bundle,
1623 }
1624 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301625 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301626
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301627 return batch_obj.get_incoming_rate()
1628
Ankush Menatf7ffe042021-11-01 13:21:14 +05301629 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301630 if last_valuation_rate := frappe.db.sql(
1631 """select valuation_rate
1632 from `tabStock Ledger Entry` force index (item_warehouse)
1633 where
1634 item_code = %s
1635 AND warehouse = %s
1636 AND valuation_rate >= 0
1637 AND is_cancelled = 0
1638 AND NOT (voucher_no = %s AND voucher_type = %s)
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301639 order by posting_datetime desc, name desc limit 1""",
Akhil Narangdd911aa2023-09-26 13:45:39 +05301640 (item_code, warehouse, voucher_no, voucher_type),
1641 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301642 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301643
1644 # If negative stock allowed, and item delivered without any incoming entry,
1645 # system does not found any SLE, then take valuation rate from Item
1646 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301647
1648 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301649 # try Item Standard rate
1650 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301651
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301652 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301653 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301654 valuation_rate = frappe.db.get_value(
1655 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1656 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301657
Ankush Menat494bd9e2022-03-28 18:52:46 +05301658 if (
1659 not allow_zero_rate
1660 and not valuation_rate
1661 and raise_error_if_no_rate
1662 and cint(erpnext.is_perpetual_inventory_enabled(company))
1663 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301664 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301665
Ankush Menat494bd9e2022-03-28 18:52:46 +05301666 message = _(
1667 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1668 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301669 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301670 solutions = (
1671 "<li>"
1672 + _(
1673 "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."
1674 ).format(voucher_type)
1675 + "</li>"
1676 )
1677 solutions += (
1678 "<li>"
1679 + _("If not, you can Cancel / Submit this entry")
1680 + " {0} ".format(frappe.bold("after"))
1681 + _("performing either one below:")
1682 + "</li>"
1683 )
Marica97715f22020-05-11 20:45:37 +05301684 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1685 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1686 msg = message + solutions + sub_solutions + "</li>"
1687
1688 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301689
1690 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301691
Ankush Menat494bd9e2022-03-28 18:52:46 +05301692
Ankush Menate7109c12021-08-26 16:40:45 +05301693def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301694 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301695 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301696 qty_shift = args.actual_qty
1697
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301698 args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
Ankush Menat7c839c42022-05-06 12:09:08 +05301699
marination8418c4b2021-06-22 21:35:25 +05301700 # find difference/shift in qty caused by stock reconciliation
1701 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301702 qty_shift = get_stock_reco_qty_shift(args)
1703
1704 # find the next nearest stock reco so that we only recalculate SLEs till that point
1705 next_stock_reco_detail = get_next_stock_reco(args)
1706 if next_stock_reco_detail:
1707 detail = next_stock_reco_detail[0]
marination40389772021-07-02 17:13:45 +05301708 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301709
Ankush Menat494bd9e2022-03-28 18:52:46 +05301710 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301711 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301712 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301713 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301714 where
1715 item_code = %(item_code)s
1716 and warehouse = %(warehouse)s
1717 and voucher_no != %(voucher_no)s
1718 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301719 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301720 posting_datetime > %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301721 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301722 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301723 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301724 args,
1725 )
Nabin Hait186a0452021-02-18 14:14:21 +05301726
1727 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1728
Ankush Menat494bd9e2022-03-28 18:52:46 +05301729
marination40389772021-07-02 17:13:45 +05301730def get_stock_reco_qty_shift(args):
1731 stock_reco_qty_shift = 0
1732 if args.get("is_cancelled"):
1733 if args.get("previous_qty_after_transaction"):
1734 # get qty (balance) that was set at submission
1735 last_balance = args.get("previous_qty_after_transaction")
1736 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1737 else:
1738 stock_reco_qty_shift = flt(args.actual_qty)
1739 else:
1740 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301741 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301742 "qty_after_transaction"
1743 )
marination40389772021-07-02 17:13:45 +05301744
1745 if last_balance is not None:
1746 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1747 else:
1748 stock_reco_qty_shift = args.qty_after_transaction
1749
1750 return stock_reco_qty_shift
1751
Ankush Menat494bd9e2022-03-28 18:52:46 +05301752
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301753def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301754 """Returns next nearest stock reconciliaton's details."""
1755
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301756 sle = frappe.qb.DocType("Stock Ledger Entry")
1757
1758 query = (
1759 frappe.qb.from_(sle)
1760 .select(
1761 sle.name,
1762 sle.posting_date,
1763 sle.posting_time,
1764 sle.creation,
1765 sle.voucher_no,
1766 sle.item_code,
1767 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301768 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301769 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301770 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301771 )
1772 .where(
1773 (sle.item_code == kwargs.get("item_code"))
1774 & (sle.warehouse == kwargs.get("warehouse"))
1775 & (sle.voucher_type == "Stock Reconciliation")
1776 & (sle.voucher_no != kwargs.get("voucher_no"))
1777 & (sle.is_cancelled == 0)
1778 & (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301779 sle.posting_datetime
1780 >= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
marination40389772021-07-02 17:13:45 +05301781 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301782 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301783 .orderby(sle.posting_datetime)
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301784 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301785 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301786 )
1787
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301788 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301789 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301790
1791 return query.run(as_dict=True)
1792
marination40389772021-07-02 17:13:45 +05301793
1794def get_datetime_limit_condition(detail):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301795 posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time)
1796
marination40389772021-07-02 17:13:45 +05301797 return f"""
1798 and
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301799 (posting_datetime < '{posting_datetime}'
marination40389772021-07-02 17:13:45 +05301800 or (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301801 posting_datetime = '{posting_datetime}'
marination40389772021-07-02 17:13:45 +05301802 and creation < '{detail.creation}'
1803 )
1804 )"""
1805
Ankush Menat494bd9e2022-03-28 18:52:46 +05301806
Ankush Menate7109c12021-08-26 16:40:45 +05301807def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301808 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301809 return
barredterraeb9ee3f2023-12-05 11:22:55 +01001810 if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
Ankush Menat5eba5752021-12-07 23:03:52 +05301811 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301812
Ankush Menat5eba5752021-12-07 23:03:52 +05301813 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301814
1815 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301816 message = _(
1817 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1818 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301819 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301820 frappe.get_desk_link("Item", args.item_code),
1821 frappe.get_desk_link("Warehouse", args.warehouse),
1822 neg_sle[0]["posting_date"],
1823 neg_sle[0]["posting_time"],
1824 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1825 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301826
Ankush Menat494bd9e2022-03-28 18:52:46 +05301827 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301828
s-aga-rd9e28432023-10-27 16:35:35 +05301829 if args.batch_no:
1830 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1831 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1832 message = _(
1833 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1834 ).format(
1835 abs(neg_batch_sle[0]["cumulative_total"]),
1836 frappe.get_desk_link("Batch", args.batch_no),
1837 frappe.get_desk_link("Warehouse", args.warehouse),
1838 neg_batch_sle[0]["posting_date"],
1839 neg_batch_sle[0]["posting_time"],
1840 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1841 )
1842 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301843
s-aga-r73b65ac2023-11-01 18:35:07 +05301844 if args.reserved_stock:
1845 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301846
Nabin Haita77b8c92020-12-21 14:45:50 +05301847
Maricad6078aa2022-06-17 15:13:13 +05301848def is_negative_with_precision(neg_sle, is_batch=False):
1849 """
1850 Returns whether system precision rounded qty is insufficient.
1851 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1852 """
1853
1854 if not neg_sle:
1855 return False
1856
1857 field = "cumulative_total" if is_batch else "qty_after_transaction"
1858 precision = cint(frappe.db.get_default("float_precision")) or 2
1859 qty_deficit = flt(neg_sle[0][field], precision)
1860
1861 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1862
1863
Nabin Haita77b8c92020-12-21 14:45:50 +05301864def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301865 return frappe.db.sql(
1866 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301867 select
1868 qty_after_transaction, posting_date, posting_time,
1869 voucher_type, voucher_no
1870 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301871 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301872 item_code = %(item_code)s
1873 and warehouse = %(warehouse)s
1874 and voucher_no != %(voucher_no)s
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301875 and posting_datetime >= %(posting_datetime)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301876 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301877 and qty_after_transaction < 0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301878 order by posting_datetime asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301879 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301880 """,
1881 args,
1882 as_dict=1,
1883 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301884
Ankush Menat5eba5752021-12-07 23:03:52 +05301885
1886def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301887 return frappe.db.sql(
1888 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301889 with batch_ledger as (
1890 select
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301891 posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
1892 sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
Ankush Menat5eba5752021-12-07 23:03:52 +05301893 from `tabStock Ledger Entry`
1894 where
1895 item_code = %(item_code)s
1896 and warehouse = %(warehouse)s
1897 and batch_no=%(batch_no)s
1898 and is_cancelled = 0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301899 order by posting_datetime, creation
Ankush Menat5eba5752021-12-07 23:03:52 +05301900 )
1901 select * from batch_ledger
1902 where
1903 cumulative_total < 0.0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301904 and posting_datetime >= %(posting_datetime)s
Ankush Menat5eba5752021-12-07 23:03:52 +05301905 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301906 """,
1907 args,
1908 as_dict=1,
1909 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301910
1911
s-aga-rd9e28432023-10-27 16:35:35 +05301912def validate_reserved_stock(kwargs):
1913 if kwargs.serial_no:
1914 serial_nos = kwargs.serial_no.split("\n")
1915 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1916
s-aga-re1a87a82023-10-31 18:41:58 +05301917 elif kwargs.batch_no:
1918 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1919
s-aga-rd9e28432023-10-27 16:35:35 +05301920 elif kwargs.serial_and_batch_bundle:
1921 sbb_entries = frappe.db.get_all(
1922 "Serial and Batch Entry",
1923 {
1924 "parenttype": "Serial and Batch Bundle",
1925 "parent": kwargs.serial_and_batch_bundle,
1926 "docstatus": 1,
1927 },
s-aga-re1a87a82023-10-31 18:41:58 +05301928 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301929 )
s-aga-rd9e28432023-10-27 16:35:35 +05301930
s-aga-re1a87a82023-10-31 18:41:58 +05301931 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301932 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301933 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1934 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301935
s-aga-r92317062023-11-02 10:36:00 +05301936 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1937 precision = cint(frappe.db.get_default("float_precision")) or 2
1938 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301939
s-aga-r92317062023-11-02 10:36:00 +05301940 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1941 if diff < 0 and abs(diff) > 0.0001:
1942 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1943 abs(diff),
1944 frappe.get_desk_link("Item", kwargs.item_code),
1945 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1946 nowdate(),
1947 nowtime(),
1948 )
1949 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301950
s-aga-rd9e28432023-10-27 16:35:35 +05301951
1952def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1953 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1954 item_code, warehouse, serial_nos
1955 ):
1956 if common_serial_nos := list(
1957 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1958 ):
1959 msg = _(
1960 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1961 )
1962 msg += "<br />"
1963 msg += _("Example: Serial No {0} reserved in {1}.").format(
1964 frappe.bold(common_serial_nos[0]),
1965 frappe.get_desk_link(
1966 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1967 ),
1968 )
1969 frappe.throw(msg, title=_("Reserved Serial No."))
1970
1971
s-aga-re1a87a82023-10-31 18:41:58 +05301972def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1973 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1974 available_batches = get_available_batches(
1975 frappe._dict(
1976 {
1977 "item_code": item_code,
1978 "warehouse": warehouse,
1979 "posting_date": nowdate(),
1980 "posting_time": nowtime(),
1981 }
1982 )
1983 )
1984 available_batches_map = {row.batch_no: row.qty for row in available_batches}
1985 precision = cint(frappe.db.get_default("float_precision")) or 2
1986
1987 for batch_no in batch_nos:
1988 diff = flt(
1989 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
1990 )
1991 if diff < 0 and abs(diff) > 0.0001:
1992 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1993 abs(diff),
1994 frappe.get_desk_link("Batch", batch_no),
1995 frappe.get_desk_link("Warehouse", warehouse),
1996 nowdate(),
1997 nowtime(),
1998 )
1999 frappe.throw(msg, title=_("Reserved Stock for Batch"))
2000
2001
Ankush Menateb8b4242022-02-12 13:08:28 +05302002def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
2003 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
2004 return True
2005 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
2006 return True
2007 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05302008
2009
2010def get_incoming_rate_for_inter_company_transfer(sle) -> float:
2011 """
2012 For inter company transfer, incoming rate is the average of the outgoing rate
2013 """
2014 rate = 0.0
2015
2016 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
2017
2018 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
2019
2020 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
2021
2022 if reference_name:
2023 rate = frappe.get_cached_value(
2024 doctype,
2025 reference_name,
2026 "incoming_rate",
2027 )
2028
2029 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05302030
2031
2032def is_internal_transfer(sle):
2033 data = frappe.get_cached_value(
2034 sle.voucher_type,
2035 sle.voucher_no,
2036 ["is_internal_supplier", "represents_company", "company"],
2037 as_dict=True,
2038 )
2039
2040 if data.is_internal_supplier and data.represents_company == data.company:
2041 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302042
2043
2044def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
2045 table = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaured80ca522024-02-07 21:56:21 +05302046 posting_datetime = get_combine_datetime(posting_date, posting_time)
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302047
2048 query = (
2049 frappe.qb.from_(table)
2050 .select(Sum(table.stock_value_difference).as_("value"))
2051 .where(
2052 (table.is_cancelled == 0)
2053 & (table.item_code == item_code)
2054 & (table.warehouse == warehouse)
Rohit Waghchaured80ca522024-02-07 21:56:21 +05302055 & (table.posting_datetime <= posting_datetime)
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302056 )
2057 )
2058
2059 if voucher_no:
2060 query = query.where(table.voucher_no != voucher_no)
2061
2062 difference_amount = query.run()
2063 return flt(difference_amount[0][0]) if difference_amount else 0