blob: edb4df6af84d4e0e1908b3ad87fabf27bb6305bb [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
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +05309from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053010from frappe.model.meta import get_field_precision
Rohit Waghchaured80ca522024-02-07 21:56:21 +053011from frappe.query_builder.functions import Sum
Rohit Waghchaure64cb1152024-01-15 19:39:41 +053012from frappe.utils import (
13 cint,
14 cstr,
15 flt,
16 get_link_to_form,
17 getdate,
18 now,
19 nowdate,
20 nowtime,
21 parse_json,
22)
Achilles Rasquinha361366e2018-02-14 17:08:59 +053023
Chillar Anand915b3432021-09-02 16:44:59 +053024import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053025from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
mergify[bot]27a1e3b2023-10-16 19:15:18 +053026from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
s-aga-re1a87a82023-10-31 18:41:58 +053027from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
28 get_available_batches,
29)
30from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
31 get_sre_reserved_batch_nos_details,
s-aga-rd9e28432023-10-27 16:35:35 +053032 get_sre_reserved_serial_nos_details,
33)
Chillar Anand915b3432021-09-02 16:44:59 +053034from erpnext.stock.utils import (
Rohit Waghchaured80ca522024-02-07 21:56:21 +053035 get_combine_datetime,
Chillar Anand915b3432021-09-02 16:44:59 +053036 get_incoming_outgoing_rate_for_cancel,
mergify[bot]07175362023-12-21 14:40:52 +053037 get_incoming_rate,
Deepesh Garg6f107da2021-10-12 20:15:55 +053038 get_or_make_bin,
s-aga-r73b65ac2023-11-01 18:35:07 +053039 get_stock_balance,
Chillar Anand915b3432021-09-02 16:44:59 +053040 get_valuation_method,
41)
Ankush Menatb534fee2022-02-19 20:58:36 +053042from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053043
Nabin Hait97bce3a2021-07-12 13:24:43 +053044
Ankush Menat494bd9e2022-03-28 18:52:46 +053045class NegativeStockError(frappe.ValidationError):
46 pass
47
48
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053049class SerialNoExistsInFutureTransaction(frappe.ValidationError):
50 pass
Nabin Hait902e8602013-01-08 18:29:24 +053051
Anand Doshi5b004ff2013-09-25 19:55:41 +053052
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053053def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053054 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053055
Ankush Menat494bd9e2022-03-28 18:52:46 +053056 args:
57 - allow_negative_stock: disable negative stock valiations if true
58 - via_landed_cost_voucher: landed cost voucher cancels and reposts
59 entries of purchase document. This flag is used to identify if
60 cancellation and repost is happening via landed cost voucher, in
61 such cases certain validations need to be ignored (like negative
62 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053063 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053064 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053065
Nabin Haitca775742013-09-26 16:16:44 +053066 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053067 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053068 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053069 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053070 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053071
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053072 args = get_args_for_future_sle(sl_entries[0])
73 future_sle_exists(args, sl_entries)
74
Nabin Haitca775742013-09-26 16:16:44 +053075 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053076 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053077 validate_serial_no(sle)
78
Nabin Haita77b8c92020-12-21 14:45:50 +053079 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053080 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053081
Ankush Menat494bd9e2022-03-28 18:52:46 +053082 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
83 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
84 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
85 )
86 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053087
Ankush Menat494bd9e2022-03-28 18:52:46 +053088 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
89 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
90 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
91 )
92 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053093
Ankush Menat494bd9e2022-03-28 18:52:46 +053094 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053095 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053096
Nabin Haita77b8c92020-12-21 14:45:50 +053097 args = sle_doc.as_dict()
Rohit Waghchaured80ca522024-02-07 21:56:21 +053098 args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time)
marination40389772021-07-02 17:13:45 +053099
100 if sle.get("voucher_type") == "Stock Reconciliation":
101 # preserve previous_qty_after_transaction for qty reposting
102 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
103
Ankush Menat494bd9e2022-03-28 18:52:46 +0530104 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +0530105 if is_stock_item:
106 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
s-aga-r73b65ac2023-11-01 18:35:07 +0530107 args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
Ankush Menatcef84c22021-12-03 12:18:59 +0530108 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +0530109 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +0530110 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530111 frappe.msgprint(
112 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
113 )
114
Ankush Menatcef84c22021-12-03 12:18:59 +0530115
116def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
117 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
118 if not args.get("posting_date"):
119 args["posting_date"] = nowdate()
120
marination7a5fd712022-07-04 17:46:54 +0530121 if not (args.get("is_cancelled") and via_landed_cost_voucher):
122 # Reposts only current voucher SL Entries
123 # Updates valuation rate, stock value, stock queue for current transaction
124 update_entries_after(
125 {
126 "item_code": args.get("item_code"),
127 "warehouse": args.get("warehouse"),
128 "posting_date": args.get("posting_date"),
129 "posting_time": args.get("posting_time"),
130 "voucher_type": args.get("voucher_type"),
131 "voucher_no": args.get("voucher_no"),
132 "sle_id": args.get("name"),
133 "creation": args.get("creation"),
s-aga-r73b65ac2023-11-01 18:35:07 +0530134 "reserved_stock": args.get("reserved_stock"),
marination7a5fd712022-07-04 17:46:54 +0530135 },
136 allow_negative_stock=allow_negative_stock,
137 via_landed_cost_voucher=via_landed_cost_voucher,
138 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530139
140 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530141 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530142 update_qty_in_future_sle(args, allow_negative_stock)
143
Nabin Haitadeb9762014-10-06 11:53:52 +0530144
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530145def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530146 return frappe._dict(
147 {
148 "voucher_type": row.get("voucher_type"),
149 "voucher_no": row.get("voucher_no"),
150 "posting_date": row.get("posting_date"),
151 "posting_time": row.get("posting_time"),
152 }
153 )
154
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530155
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530156def validate_serial_no(sle):
157 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530158
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530159 for sn in get_serial_nos(sle.serial_no):
160 args = copy.deepcopy(sle)
161 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530162 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530163
164 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530165 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530166 voucher_type = frappe.bold(row.voucher_type)
167 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530168 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530169
170 if vouchers:
171 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530172 msg = (
173 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
174 The list of the transactions are as below."""
175 + "<br><br><ul><li>"
176 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530177
Ankush Menat494bd9e2022-03-28 18:52:46 +0530178 msg += "</li><li>".join(vouchers)
179 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530180
Ankush Menat494bd9e2022-03-28 18:52:46 +0530181 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530182 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
183
Ankush Menat494bd9e2022-03-28 18:52:46 +0530184
Nabin Hait186a0452021-02-18 14:14:21 +0530185def validate_cancellation(args):
186 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530187 repost_entry = frappe.db.get_value(
188 "Repost Item Valuation",
189 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
190 ["name", "status"],
191 as_dict=1,
192 )
Nabin Hait186a0452021-02-18 14:14:21 +0530193
194 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530195 if repost_entry.status == "In Progress":
196 frappe.throw(
197 _(
198 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
199 )
200 )
201 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530202 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530203 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530204 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530205 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530206
Ankush Menat494bd9e2022-03-28 18:52:46 +0530207
Nabin Hait9653f602013-08-20 15:37:33 +0530208def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530209 frappe.db.sql(
210 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530211 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530212 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530213 (now(), frappe.session.user, voucher_type, voucher_no),
214 )
215
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530216
Nabin Hait54c865e2015-03-27 15:38:31 +0530217def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530218 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530219 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530220 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530221 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530222 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530223 sle.submit()
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530224
225 # Added to handle the case when the stock ledger entry is created from the repostig
226 if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
227 sle.db_set("creation", args.get("creation_time"))
228
Nabin Haita77b8c92020-12-21 14:45:50 +0530229 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530230
Ankush Menat494bd9e2022-03-28 18:52:46 +0530231
232def repost_future_sle(
233 args=None,
234 voucher_type=None,
235 voucher_no=None,
236 allow_negative_stock=None,
237 via_landed_cost_voucher=False,
238 doc=None,
239):
Nabin Haite1fa7232022-07-20 15:19:09 +0530240 if not args:
241 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530242
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530243 reposting_data = {}
244 if doc and doc.reposting_data_file:
245 reposting_data = get_reposting_data(doc.reposting_data_file)
246
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530247 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530248 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530249 )
250 if items_to_be_repost:
251 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530252
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530253 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
254 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530255
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530256 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530257 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530258 validate_item_warehouse(args[i])
259
Ankush Menat494bd9e2022-03-28 18:52:46 +0530260 obj = update_entries_after(
261 {
262 "item_code": args[i].get("item_code"),
263 "warehouse": args[i].get("warehouse"),
264 "posting_date": args[i].get("posting_date"),
265 "posting_time": args[i].get("posting_time"),
266 "creation": args[i].get("creation"),
267 "distinct_item_warehouses": distinct_item_warehouses,
268 },
269 allow_negative_stock=allow_negative_stock,
270 via_landed_cost_voucher=via_landed_cost_voucher,
271 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530272 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530273
Akhil Narang3effaf22024-03-27 11:37:26 +0530274 distinct_item_warehouses[(args[i].get("item_code"), args[i].get("warehouse"))].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530275
Nabin Hait97bce3a2021-07-12 13:24:43 +0530276 if obj.new_items_found:
Akhil Narang3effaf22024-03-27 11:37:26 +0530277 for _item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530278 if ("args_idx" not in data and not data.reposting_status) or (
279 data.sle_changed and data.reposting_status
280 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530281 data.args_idx = len(args)
282 args.append(data.sle)
283 elif data.sle_changed and not data.reposting_status:
284 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530285
Nabin Hait97bce3a2021-07-12 13:24:43 +0530286 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530287 i += 1
288
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530289 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530290 update_args_in_repost_item_valuation(
291 doc, i, args, distinct_item_warehouses, affected_transactions
292 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530293
Ankush Menat494bd9e2022-03-28 18:52:46 +0530294
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530295def get_reposting_data(file_path) -> dict:
296 file_name = frappe.db.get_value(
297 "File",
298 {
299 "file_url": file_path,
300 "attached_to_field": "reposting_data_file",
301 },
302 "name",
303 )
304
305 if not file_name:
306 return frappe._dict()
307
308 attached_file = frappe.get_doc("File", file_name)
309
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530310 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530311 if data := json.loads(data.decode("utf-8")):
312 data = data
313
314 return parse_json(data)
315
316
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530317def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530318 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530319 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530320 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530321 frappe.throw(_(validation_msg))
322
Ankush Menat494bd9e2022-03-28 18:52:46 +0530323
Akhil Narang3effaf22024-03-27 11:37:26 +0530324def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses, affected_transactions):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530325 if not doc.items_to_be_repost:
326 file_name = ""
327 if doc.reposting_data_file:
328 file_name = get_reposting_file_name(doc.doctype, doc.name)
329 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
330
331 doc.reposting_data_file = create_json_gz_file(
332 {
333 "items_to_be_repost": args,
334 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
335 "affected_transactions": affected_transactions,
336 },
337 doc,
338 file_name,
339 )
340
341 doc.db_set(
342 {
343 "current_index": index,
344 "total_reposting_count": len(args),
345 "reposting_data_file": doc.reposting_data_file,
346 }
347 )
348
349 else:
350 doc.db_set(
351 {
352 "items_to_be_repost": json.dumps(args, default=str),
353 "distinct_item_and_warehouse": json.dumps(
354 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
355 ),
356 "current_index": index,
357 "affected_transactions": frappe.as_json(affected_transactions),
358 }
359 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530360
Ankush Menatecdb4932022-04-17 19:06:13 +0530361 if not frappe.flags.in_test:
362 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530363
Ankush Menat494bd9e2022-03-28 18:52:46 +0530364 frappe.publish_realtime(
365 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530366 {
367 "name": doc.name,
368 "items_to_be_repost": json.dumps(args, default=str),
369 "current_index": index,
370 "total_reposting_count": len(args),
371 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530372 doctype=doc.doctype,
373 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530374 )
375
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530376
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530377def get_reposting_file_name(dt, dn):
378 return frappe.db.get_value(
379 "File",
380 {
381 "attached_to_doctype": dt,
382 "attached_to_name": dn,
383 "attached_to_field": "reposting_data_file",
384 },
385 "name",
386 )
387
388
389def create_json_gz_file(data, doc, file_name=None) -> str:
390 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530391 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530392
393 if not file_name:
394 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
395 _file = frappe.get_doc(
396 {
397 "doctype": "File",
398 "file_name": json_filename,
399 "attached_to_doctype": doc.doctype,
400 "attached_to_name": doc.name,
401 "attached_to_field": "reposting_data_file",
402 "content": compressed_content,
403 "is_private": 1,
404 }
405 )
406 _file.save(ignore_permissions=True)
407
408 return _file.file_url
409 else:
410 file_doc = frappe.get_doc("File", file_name)
411 path = file_doc.get_full_path()
412
413 with open(path, "wb") as f:
414 f.write(compressed_content)
415
416 return doc.reposting_data_file
417
418
419def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
420 if not reposting_data and doc and doc.reposting_data_file:
421 reposting_data = get_reposting_data(doc.reposting_data_file)
422
423 if reposting_data and reposting_data.items_to_be_repost:
424 return reposting_data.items_to_be_repost
425
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530426 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530427
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530428 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530429 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530430
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530431 if not items_to_be_repost and voucher_type and voucher_no:
432 items_to_be_repost = frappe.db.get_all(
433 "Stock Ledger Entry",
434 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
435 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
436 order_by="creation asc",
437 group_by="item_code, warehouse",
438 )
439
Nabin Haite1fa7232022-07-20 15:19:09 +0530440 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530441
Ankush Menat494bd9e2022-03-28 18:52:46 +0530442
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530443def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
444 if not reposting_data and doc and doc.reposting_data_file:
445 reposting_data = get_reposting_data(doc.reposting_data_file)
446
447 if reposting_data and reposting_data.distinct_item_and_warehouse:
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530448 return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530449
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530450 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530451
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530452 if doc and doc.distinct_item_and_warehouse:
453 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530454 distinct_item_warehouses = {
455 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
456 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530457 else:
458 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530459 distinct_item_warehouses.setdefault(
460 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
461 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530462
463 return distinct_item_warehouses
464
Ankush Menat494bd9e2022-03-28 18:52:46 +0530465
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530466def parse_distinct_items_and_warehouses(distinct_items_and_warehouses):
467 new_dict = frappe._dict({})
468
469 # convert string keys to tuple
470 for k, v in distinct_items_and_warehouses.items():
471 new_dict[frappe.safe_eval(k)] = frappe._dict(v)
472
473 return new_dict
474
475
Akhil Narang3effaf22024-03-27 11:37:26 +0530476def get_affected_transactions(doc, reposting_data=None) -> set[tuple[str, str]]:
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530477 if not reposting_data and doc and doc.reposting_data_file:
478 reposting_data = get_reposting_data(doc.reposting_data_file)
479
480 if reposting_data and reposting_data.affected_transactions:
481 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
482
Ankush Menatecdb4932022-04-17 19:06:13 +0530483 if not doc.affected_transactions:
484 return set()
485
486 transactions = frappe.parse_json(doc.affected_transactions)
487 return {tuple(transaction) for transaction in transactions}
488
489
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530490def get_current_index(doc=None):
491 if doc and doc.current_index:
492 return doc.current_index
493
Ankush Menat494bd9e2022-03-28 18:52:46 +0530494
Akhil Narang3effaf22024-03-27 11:37:26 +0530495class update_entries_after:
Nabin Hait902e8602013-01-08 18:29:24 +0530496 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530497 update valution rate and qty after transaction
498 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530499
Ankush Menat494bd9e2022-03-28 18:52:46 +0530500 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530501
Ankush Menat494bd9e2022-03-28 18:52:46 +0530502 args = {
503 "item_code": "ABC",
504 "warehouse": "XYZ",
505 "posting_date": "2012-12-12",
506 "posting_time": "12:00"
507 }
Nabin Hait902e8602013-01-08 18:29:24 +0530508 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530509
510 def __init__(
511 self,
512 args,
513 allow_zero_rate=False,
514 allow_negative_stock=None,
515 via_landed_cost_voucher=False,
516 verbose=1,
517 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530518 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530519 self.verbose = verbose
520 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530521 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530522 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530523 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
524 item_code=self.item_code
525 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530526
Nabin Haita77b8c92020-12-21 14:45:50 +0530527 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530528 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530529 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530530
Nabin Haita77b8c92020-12-21 14:45:50 +0530531 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530532 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530533 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530534
535 self.new_items_found = False
536 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Akhil Narang3effaf22024-03-27 11:37:26 +0530537 self.affected_transactions: set[tuple[str, str]] = set()
s-aga-r73b65ac2023-11-01 18:35:07 +0530538 self.reserved_stock = flt(self.args.reserved_stock)
Nabin Haita77b8c92020-12-21 14:45:50 +0530539
540 self.data = frappe._dict()
541 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530542 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530543
Maricad6078aa2022-06-17 15:13:13 +0530544 def set_precision(self):
545 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
546 self.currency_precision = get_field_precision(
547 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530548 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530549
550 def initialize_previous_data(self, args):
551 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530552 Get previous sl entries for current item for each related warehouse
553 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530554
Ankush Menat494bd9e2022-03-28 18:52:46 +0530555 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530556
Ankush Menat494bd9e2022-03-28 18:52:46 +0530557 self.data = {
558 warehouse1: {
559 'previus_sle': {},
560 'qty_after_transaction': 10,
561 'valuation_rate': 100,
562 'stock_value': 1000,
563 'prev_stock_value': 1000,
564 'stock_queue': '[[10, 100]]',
565 'stock_value_difference': 1000
566 }
567 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530568
569 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530570 self.data.setdefault(args.warehouse, frappe._dict())
571 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530572 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530573 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530574
Ankush Menatc1d986a2021-08-31 19:43:42 +0530575 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
576 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
577
Ankush Menat494bd9e2022-03-28 18:52:46 +0530578 warehouse_dict.update(
579 {
580 "prev_stock_value": previous_sle.stock_value or 0.0,
581 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
582 "stock_value_difference": 0.0,
583 }
584 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530585
Nabin Haita77b8c92020-12-21 14:45:50 +0530586 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530587 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530588
Nabin Haita77b8c92020-12-21 14:45:50 +0530589 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530590 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530591 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530592 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530593 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530594 entries_to_fix = self.get_future_entries_to_fix()
595
596 i = 0
597 while i < len(entries_to_fix):
598 sle = entries_to_fix[i]
599 i += 1
600
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530601 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530602 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530603
Nabin Haita77b8c92020-12-21 14:45:50 +0530604 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530605 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530606
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530607 if self.exceptions:
608 self.raise_exceptions()
609
Nabin Hait186a0452021-02-18 14:14:21 +0530610 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530611 sl_entries = self.get_sle_against_current_voucher()
612 for sle in sl_entries:
613 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530614
Nabin Haita77b8c92020-12-21 14:45:50 +0530615 def get_sle_against_current_voucher(self):
Akhil Narang3effaf22024-03-27 11:37:26 +0530616 self.args["posting_datetime"] = get_combine_datetime(self.args.posting_date, self.args.posting_time)
Nabin Haitf2be0802021-02-15 19:27:49 +0530617
Ankush Menat494bd9e2022-03-28 18:52:46 +0530618 return frappe.db.sql(
619 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530620 select
Rohit Waghchaured80ca522024-02-07 21:56:21 +0530621 *, posting_datetime as "timestamp"
Nabin Haita77b8c92020-12-21 14:45:50 +0530622 from
623 `tabStock Ledger Entry`
624 where
625 item_code = %(item_code)s
626 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530627 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530628 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +0530629 posting_datetime = %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530630 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530631 order by
Rohit Waghchaurea73ba2c2024-02-21 17:32:02 +0530632 creation ASC
Nabin Haita77b8c92020-12-21 14:45:50 +0530633 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530634 """,
635 self.args,
636 as_dict=1,
637 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530638
Nabin Haita77b8c92020-12-21 14:45:50 +0530639 def get_future_entries_to_fix(self):
640 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530641 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
642 {"item_code": self.item_code, "warehouse": self.args.warehouse}
643 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530644
Nabin Haita77b8c92020-12-21 14:45:50 +0530645 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530646
Nabin Haita77b8c92020-12-21 14:45:50 +0530647 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530648 dependant_sle = get_sle_by_voucher_detail_no(
649 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
650 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530651
Nabin Haita77b8c92020-12-21 14:45:50 +0530652 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530653 return entries_to_fix
Akhil Narang3effaf22024-03-27 11:37:26 +0530654 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
Nabin Hait243d59b2021-02-02 16:55:13 +0530655 return entries_to_fix
656 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530657 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530658 return entries_to_fix
659 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
660 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530661 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530662 self.initialize_previous_data(dependant_sle)
663 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530664 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530665
666 def update_distinct_item_warehouses(self, dependant_sle):
667 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530668 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530669
Nabin Hait97bce3a2021-07-12 13:24:43 +0530670 if key not in self.distinct_item_warehouses:
671 self.distinct_item_warehouses[key] = val
672 self.new_items_found = True
673 else:
Akhil Narang3effaf22024-03-27 11:37:26 +0530674 existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530675
676 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
677
Nabin Hait97bce3a2021-07-12 13:24:43 +0530678 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
679 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530680 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
681 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530682 self.distinct_item_warehouses[key] = val
683 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530684 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
685 # Future dependent voucher needs to be repost to get the correct stock value
686 # If dependent voucher has not reposted, then add it to the list
687 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530688 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530689 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
690 self.distinct_item_warehouses[key] = val
691
692 def get_dependent_voucher_detail_nos(self, key):
693 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
694 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
695
696 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530697
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530698 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530699 # previous sle data for this warehouse
700 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530701 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530702
Anand Doshi0dc79f42015-04-06 12:59:34 +0530703 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 +0530704 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530705 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530706 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530707 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530708 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530709
Nabin Haita77b8c92020-12-21 14:45:50 +0530710 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530711 if not self.args.get("sle_id"):
712 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530713
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530714 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530715 sle.voucher_type == "Stock Reconciliation"
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530716 and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530717 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530718 and not self.args.get("sle_id")
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530719 and sle.is_cancelled == 0
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530720 ):
721 self.reset_actual_qty_for_stock_reco(sle)
722
723 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530724 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
725 and sle.voucher_detail_no
726 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530727 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530728 ):
729 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
730
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530731 dimensions = get_inventory_dimensions()
732 has_dimensions = False
733 if dimensions:
734 for dimension in dimensions:
735 if sle.get(dimension.get("fieldname")):
736 has_dimensions = True
737
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530738 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530739 self.calculate_valuation_for_serial_batch_bundle(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530740 elif sle.serial_no and not self.args.get("sle_id"):
741 # Only run in reposting
742 self.get_serialized_values(sle)
743 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
744 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
745 self.wh_data.qty_after_transaction = sle.qty_after_transaction
746
747 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
748 self.wh_data.valuation_rate
749 )
750 elif (
751 sle.batch_no
752 and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
753 and not self.args.get("sle_id")
754 ):
755 # Only run in reposting
756 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530757 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530758 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530759 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530760 self.wh_data.valuation_rate = sle.valuation_rate
761 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530762 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
763 self.wh_data.valuation_rate
764 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530765 if self.valuation_method != "Moving Average":
Akhil Narang3effaf22024-03-27 11:37:26 +0530766 self.wh_data.stock_queue = [
767 [self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]
768 ]
Nabin Haitb96c0142014-10-07 11:25:04 +0530769 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530770 if self.valuation_method == "Moving Average":
771 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530772 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
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 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530776 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530777 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530778
Rushabh Mehta54047782013-12-26 11:07:46 +0530779 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530780 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530781 if not self.wh_data.qty_after_transaction:
782 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530783
Nabin Haita77b8c92020-12-21 14:45:50 +0530784 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
785 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530786
Nabin Hait902e8602013-01-08 18:29:24 +0530787 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530788 sle.qty_after_transaction = self.wh_data.qty_after_transaction
789 sle.valuation_rate = self.wh_data.valuation_rate
790 sle.stock_value = self.wh_data.stock_value
791 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530792
rohitwaghchaurea8216b92023-11-09 12:22:26 +0530793 if not sle.is_adjustment_entry or not self.args.get("sle_id"):
794 sle.stock_value_difference = stock_value_difference
795
796 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530797 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530798
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530799 if not self.args.get("sle_id") or (
800 sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
801 ):
Ankush Menat701878f2022-03-01 18:08:29 +0530802 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530803
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530804 def get_serialized_values(self, sle):
805 incoming_rate = flt(sle.incoming_rate)
806 actual_qty = flt(sle.actual_qty)
807 serial_nos = cstr(sle.serial_no).split("\n")
808
809 if incoming_rate < 0:
810 # wrong incoming rate
811 incoming_rate = self.wh_data.valuation_rate
812
813 stock_value_change = 0
814 if actual_qty > 0:
815 stock_value_change = actual_qty * incoming_rate
816 else:
817 # In case of delivery/stock issue, get average purchase rate
818 # of serial nos of current entry
819 if not sle.is_cancelled:
820 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
821 stock_value_change = -1 * outgoing_value
822 else:
823 stock_value_change = actual_qty * sle.outgoing_rate
824
825 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
826
827 if new_stock_qty > 0:
828 new_stock_value = (
829 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
830 ) + stock_value_change
831 if new_stock_value >= 0:
832 # calculate new valuation rate only if stock value is positive
833 # else it remains the same as that of previous entry
834 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
835
836 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Akhil Narang3effaf22024-03-27 11:37:26 +0530837 allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530838 if not allow_zero_rate:
839 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
840
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530841 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530842 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
843 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
844
845 if sle.actual_qty < 0:
846 sle.actual_qty = (
847 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
848 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530849 )
850
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530851 if abs(sle.actual_qty) == 0.0:
852 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530853
Akhil Narang3effaf22024-03-27 11:37:26 +0530854 if sle.serial_and_batch_bundle and frappe.get_cached_value("Item", sle.item_code, "has_serial_no"):
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530855 self.update_serial_no_status(sle)
856
857 def update_serial_no_status(self, sle):
858 from erpnext.stock.serial_batch_bundle import get_serial_nos
859
860 serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
861 if not serial_nos:
862 return
863
864 warehouse = None
865 status = "Inactive"
866
867 if sle.actual_qty > 0:
868 warehouse = sle.warehouse
869 status = "Active"
870
871 sn_table = frappe.qb.DocType("Serial No")
872
873 query = (
874 frappe.qb.update(sn_table)
875 .set(sn_table.warehouse, warehouse)
876 .set(sn_table.status, status)
877 .where(sn_table.name.isin(serial_nos))
878 )
879
880 query.run()
881
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530882 def calculate_valuation_for_serial_batch_bundle(self, sle):
rohitwaghchaure01856a62024-03-07 14:14:19 +0530883 if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
884 return
885
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530886 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
887
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530888 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530889 doc.calculate_qty_and_amount(save=True)
890
891 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
892
s-aga-r1e15a3c2024-02-02 13:07:26 +0530893 precision = doc.precision("total_qty")
894 self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
Rohit Waghchauree8ae4ed2024-02-19 22:18:57 +0530895 if flt(self.wh_data.qty_after_transaction, precision):
s-aga-r1e15a3c2024-02-02 13:07:26 +0530896 self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
897 self.wh_data.qty_after_transaction, precision
898 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530899
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530900 def validate_negative_stock(self, sle):
901 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530902 validate negative stock for entries current datetime onwards
903 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530904 """
s-aga-rf0acb202023-04-12 14:13:54 +0530905 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530906 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530907
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530908 if diff < 0 and abs(diff) > 0.0001:
909 # negative stock!
910 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530911 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530912 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530913 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530914 return True
915
Nabin Haita77b8c92020-12-21 14:45:50 +0530916 def get_dynamic_incoming_outgoing_rate(self, sle):
917 # Get updated incoming/outgoing rate from transaction
918 if sle.recalculate_rate:
919 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
920
921 if flt(sle.actual_qty) >= 0:
922 sle.incoming_rate = rate
923 else:
924 sle.outgoing_rate = rate
925
926 def get_incoming_outgoing_rate_from_transaction(self, sle):
927 rate = 0
928 # Material Transfer, Repack, Manufacturing
929 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530930 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530931 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
932 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530933 elif sle.voucher_type in (
934 "Purchase Receipt",
935 "Purchase Invoice",
936 "Delivery Note",
937 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530938 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530939 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530940 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530941 from erpnext.controllers.sales_and_purchase_return import (
942 get_rate_for_return, # don't move this import to top
943 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530944
rohitwaghchaure63792382024-03-04 12:04:41 +0530945 if (
946 self.valuation_method == "Moving Average"
947 and not sle.get("serial_no")
948 and not sle.get("batch_no")
949 and not sle.get("serial_and_batch_bundle")
950 ):
mergify[bot]07175362023-12-21 14:40:52 +0530951 rate = get_incoming_rate(
952 {
953 "item_code": sle.item_code,
954 "warehouse": sle.warehouse,
955 "posting_date": sle.posting_date,
956 "posting_time": sle.posting_time,
957 "qty": sle.actual_qty,
958 "serial_no": sle.get("serial_no"),
959 "batch_no": sle.get("batch_no"),
960 "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
961 "company": sle.company,
962 "voucher_type": sle.voucher_type,
963 "voucher_no": sle.voucher_no,
964 "allow_zero_valuation": self.allow_zero_rate,
965 "sle": sle.name,
966 }
967 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530968
mergify[bot]07175362023-12-21 14:40:52 +0530969 else:
970 rate = get_rate_for_return(
971 sle.voucher_type,
972 sle.voucher_no,
973 sle.item_code,
974 voucher_detail_no=sle.voucher_detail_no,
975 sle=sle,
976 )
rohitwaghchaure63792382024-03-04 12:04:41 +0530977
978 if (
979 sle.get("serial_and_batch_bundle")
980 and rate > 0
981 and sle.voucher_type in ["Delivery Note", "Sales Invoice"]
982 ):
983 frappe.db.set_value(
984 sle.voucher_type + " Item",
985 sle.voucher_detail_no,
986 "incoming_rate",
987 rate,
988 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530989 elif (
990 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530991 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530992 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530993 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530994 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530995 else:
996 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530997 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530998 elif sle.voucher_type == "Subcontracting Receipt":
999 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +05301000 else:
1001 rate_field = "incoming_rate"
1002
1003 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +05301004 item_code, incoming_rate = frappe.db.get_value(
1005 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
1006 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301007
1008 if item_code == sle.item_code:
1009 rate = incoming_rate
1010 else:
1011 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1012 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +05301013 elif sle == "Subcontracting Receipt":
1014 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +05301015 else:
1016 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +05301017
Ankush Menat494bd9e2022-03-28 18:52:46 +05301018 rate = frappe.db.get_value(
1019 ref_doctype,
1020 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
1021 rate_field,
1022 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301023
1024 return rate
1025
1026 def update_outgoing_rate_on_transaction(self, sle):
1027 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301028 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
1029 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +05301030 """
1031 if sle.actual_qty and sle.voucher_detail_no:
1032 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
1033
1034 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
1035 self.update_rate_on_stock_entry(sle, outgoing_rate)
1036 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1037 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
1038 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
1039 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301040 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
1041 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +05301042 elif sle.voucher_type == "Stock Reconciliation":
1043 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +05301044
1045 def update_rate_on_stock_entry(self, sle, outgoing_rate):
1046 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
1047
Ankush Menat701878f2022-03-01 18:08:29 +05301048 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
1049 if not sle.dependant_sle_voucher_detail_no:
1050 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +05301051
1052 def recalculate_amounts_in_stock_entry(self, voucher_no):
1053 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +05301054 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
1055 stock_entry.db_update()
1056 for d in stock_entry.items:
1057 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +05301058
Nabin Haita77b8c92020-12-21 14:45:50 +05301059 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
1060 # Update item's incoming rate on transaction
1061 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
1062 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301063 frappe.db.set_value(
1064 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
1065 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301066 else:
1067 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +05301068 frappe.db.set_value(
1069 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +05301070 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301071 "incoming_rate",
1072 outgoing_rate,
1073 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301074
1075 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
1076 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301077 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
1078 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
1079 ):
1080 frappe.db.set_value(
1081 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
1082 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301083 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301084 frappe.db.set_value(
1085 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
1086 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301087
1088 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +05301089 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +05301090 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +05301091 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301092 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +05301093 d.db_update()
1094
Sagar Sharma323bdf82022-05-17 15:14:07 +05301095 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +05301096 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
1097 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301098 else:
1099 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301100 "Subcontracting Receipt Supplied Item",
1101 sle.voucher_detail_no,
1102 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +05301103 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301104
s-aga-ra6cb6c62023-05-03 09:51:58 +05301105 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301106 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +05301107 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301108 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +05301109 d.db_update()
1110
s-aga-r88a3f652023-05-30 16:54:28 +05301111 def update_rate_on_stock_reconciliation(self, sle):
1112 if not sle.serial_no and not sle.batch_no:
1113 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
1114
1115 for item in sr.items:
1116 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +05301117 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +05301118 continue
1119
1120 previous_sle = get_previous_sle(
1121 {
1122 "item_code": item.item_code,
1123 "warehouse": item.warehouse,
1124 "posting_date": sr.posting_date,
1125 "posting_time": sr.posting_time,
1126 "sle": sle.name,
1127 }
1128 )
1129
1130 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
1131 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
1132 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
1133
1134 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +05301135 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +05301136 item.amount_difference = item.amount - item.current_amount
1137 else:
1138 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1139 sr.db_update()
1140
1141 for item in sr.items:
1142 item.db_update()
1143
Nabin Hait328c4f92020-01-02 19:00:32 +05301144 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1145 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301146 all_serial_nos = frappe.get_all(
1147 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1148 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301149
Ankush Menat494bd9e2022-03-28 18:52:46 +05301150 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 +05301151
1152 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301153 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301154 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301155 incoming_rate = frappe.db.sql(
1156 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301157 select incoming_rate
1158 from `tabStock Ledger Entry`
1159 where
1160 company = %s
1161 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301162 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301163 and (serial_no = %s
1164 or serial_no like %s
1165 or serial_no like %s
1166 or serial_no like %s
1167 )
1168 order by posting_date desc
1169 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301170 """,
1171 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1172 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301173
1174 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1175
1176 return incoming_values
1177
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301178 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301179 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301180 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301181 if new_stock_qty >= 0:
1182 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301183 if flt(self.wh_data.qty_after_transaction) <= 0:
1184 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301185 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301186 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1187 actual_qty * sle.incoming_rate
1188 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301189
Nabin Haita77b8c92020-12-21 14:45:50 +05301190 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301191
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301192 elif sle.outgoing_rate:
1193 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301194 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1195 actual_qty * sle.outgoing_rate
1196 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301197
Nabin Haita77b8c92020-12-21 14:45:50 +05301198 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301199 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301200 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301201 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301202 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1203 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301204
Nabin Haita77b8c92020-12-21 14:45:50 +05301205 if not self.wh_data.valuation_rate and actual_qty > 0:
1206 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301207
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301208 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001209 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301210 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301211 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1212 sle.voucher_type, sle.voucher_detail_no
1213 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001214 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301215 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301216
Ankush Menatf089d392022-02-02 12:51:21 +05301217 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301218 incoming_rate = flt(sle.incoming_rate)
1219 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301220 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301221
Ankush Menat494bd9e2022-03-28 18:52:46 +05301222 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1223 self.wh_data.qty_after_transaction + actual_qty
1224 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301225
Ankush Menat97e18a12022-01-15 17:42:25 +05301226 if self.valuation_method == "LIFO":
1227 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1228 else:
1229 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1230
Ankush Menatb534fee2022-02-19 20:58:36 +05301231 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1232
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301233 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301234 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301235 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301236
Ankush Menat4b29fb62021-12-18 18:40:22 +05301237 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301238 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1239 sle.voucher_type, sle.voucher_detail_no
1240 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301241 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301242 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301243 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301244 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301245
Ankush Menat494bd9e2022-03-28 18:52:46 +05301246 stock_queue.remove_stock(
1247 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1248 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301249
Ankush Menatb534fee2022-02-19 20:58:36 +05301250 _qty, stock_value = stock_queue.get_total_stock_and_value()
1251
1252 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301253
Ankush Menat97e18a12022-01-15 17:42:25 +05301254 self.wh_data.stock_queue = stock_queue.state
Akhil Narang3effaf22024-03-27 11:37:26 +05301255 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301256
Nabin Haita77b8c92020-12-21 14:45:50 +05301257 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301258 self.wh_data.stock_queue.append(
1259 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1260 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301261
Ankush Menatb534fee2022-02-19 20:58:36 +05301262 if self.wh_data.qty_after_transaction:
1263 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1264
Ankush Menatce0514c2022-02-15 11:41:41 +05301265 def update_batched_values(self, sle):
1266 incoming_rate = flt(sle.incoming_rate)
1267 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301268
Ankush Menat494bd9e2022-03-28 18:52:46 +05301269 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1270 self.wh_data.qty_after_transaction + actual_qty
1271 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301272
1273 if actual_qty > 0:
1274 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301275 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301276 outgoing_rate = get_batch_incoming_rate(
1277 item_code=sle.item_code,
1278 warehouse=sle.warehouse,
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301279 batch_no=sle.batch_no,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301280 posting_date=sle.posting_date,
1281 posting_time=sle.posting_time,
1282 creation=sle.creation,
1283 )
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301284
Ankush Menataba7a7c2022-02-19 19:36:28 +05301285 if outgoing_rate is None:
1286 # This can *only* happen if qty available for the batch is zero.
1287 # in such case fall back various other rates.
1288 # future entries will correct the overall accounting as each
1289 # batch individually uses moving average rates.
1290 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301291 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301292
Akhil Narang3effaf22024-03-27 11:37:26 +05301293 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
Ankush Menatce0514c2022-02-15 11:41:41 +05301294 if self.wh_data.qty_after_transaction:
1295 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301296
Javier Wong9b11d9b2017-04-14 18:24:04 +08001297 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301298 ref_item_dt = ""
1299
1300 if voucher_type == "Stock Entry":
1301 ref_item_dt = voucher_type + " Detail"
1302 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1303 ref_item_dt = voucher_type + " Item"
1304
1305 if ref_item_dt:
1306 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1307 else:
1308 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301309
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301310 def get_fallback_rate(self, sle) -> float:
1311 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301312 This should only get used for negative stock."""
1313 return get_valuation_rate(
1314 sle.item_code,
1315 sle.warehouse,
1316 sle.voucher_type,
1317 sle.voucher_no,
1318 self.allow_zero_rate,
1319 currency=erpnext.get_company_currency(sle.company),
1320 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301321 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301322
Nabin Haita77b8c92020-12-21 14:45:50 +05301323 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301324 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301325 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1326 sle = sle[0] if sle else frappe._dict()
1327 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301328
Nabin Haita77b8c92020-12-21 14:45:50 +05301329 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301330 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301331 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301332
1333 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301334 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301335 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301336 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301337
Ankush Menat494bd9e2022-03-28 18:52:46 +05301338 if (
1339 exceptions[0]["voucher_type"],
1340 exceptions[0]["voucher_no"],
1341 ) in frappe.local.flags.currently_saving:
Nabin Haita77b8c92020-12-21 14:45:50 +05301342 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301343 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301344 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1345 frappe.get_desk_link("Warehouse", warehouse),
1346 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301347 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301348 msg = _(
1349 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1350 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301351 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301352 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1353 frappe.get_desk_link("Warehouse", warehouse),
1354 exceptions[0]["posting_date"],
1355 exceptions[0]["posting_time"],
1356 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1357 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301358
Nabin Haita77b8c92020-12-21 14:45:50 +05301359 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301360 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301361 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301362
1363 if allowed_qty > 0:
Akhil Narang3effaf22024-03-27 11:37:26 +05301364 msg = "{} As {} units are reserved for other sales orders, you are allowed to consume only {} units.".format(
s-aga-r2d8363a2023-09-02 11:02:24 +05301365 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1366 )
1367 else:
Akhil Narang3effaf22024-03-27 11:37:26 +05301368 msg = f"{msg} As the full stock is reserved for other sales orders, you're not allowed to consume the stock."
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(
Akhil Narang3effaf22024-03-27 11:37:26 +05301417 f"""
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
Akhil Narang3effaf22024-03-27 11:37:26 +05301429 for update""",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301430 args,
1431 as_dict=1,
1432 )
marination8418c4b2021-06-22 21:35:25 +05301433
1434 return sle[0] if sle else frappe._dict()
1435
Ankush Menat494bd9e2022-03-28 18:52:46 +05301436
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301437def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301438 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301439 get the last sle on or before the current time-bucket,
1440 to get actual qty before transaction, this function
1441 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301442
Ankush Menat494bd9e2022-03-28 18:52:46 +05301443 args = {
1444 "item_code": "ABC",
1445 "warehouse": "XYZ",
1446 "posting_date": "2012-12-12",
1447 "posting_time": "12:00",
1448 "sle": "name of reference Stock Ledger Entry"
1449 }
Anand Doshi1b531862013-01-10 19:29:51 +05301450 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301451 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301452 sle = get_stock_ledger_entries(
1453 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1454 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301455 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301456
Ankush Menat494bd9e2022-03-28 18:52:46 +05301457
1458def get_stock_ledger_entries(
1459 previous_sle,
1460 operator=None,
1461 order="desc",
1462 limit=None,
1463 for_update=False,
1464 debug=False,
1465 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301466 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301467):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301468 """get stock ledger entries filtered by specific posting datetime conditions"""
Akhil Narang3effaf22024-03-27 11:37:26 +05301469 conditions = f" and posting_datetime {operator} %(posting_datetime)s"
Nabin Haitb9ce1042018-02-01 14:58:50 +05301470 if previous_sle.get("warehouse"):
1471 conditions += " and warehouse = %(warehouse)s"
1472 elif previous_sle.get("warehouse_condition"):
1473 conditions += " and " + previous_sle.get("warehouse_condition")
1474
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301475 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301476 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1477 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301478 conditions += (
1479 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301480 (
Akhil Narang3effaf22024-03-27 11:37:26 +05301481 serial_no = {}
1482 or serial_no like {}
1483 or serial_no like {}
1484 or serial_no like {}
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301485 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301486 """
1487 ).format(
1488 frappe.db.escape(serial_no),
Akhil Narang3effaf22024-03-27 11:37:26 +05301489 frappe.db.escape(f"{serial_no}\n%"),
1490 frappe.db.escape(f"%\n{serial_no}"),
1491 frappe.db.escape(f"%\n{serial_no}\n%"),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301492 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301493
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301494 if not previous_sle.get("posting_date"):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301495 previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
1496 else:
1497 previous_sle["posting_datetime"] = get_combine_datetime(
1498 previous_sle["posting_date"], previous_sle["posting_time"]
1499 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301500
1501 if operator in (">", "<=") and previous_sle.get("name"):
1502 conditions += " and name!=%(name)s"
1503
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301504 if extra_cond:
1505 conditions += f"{extra_cond}"
1506
Ankush Menat494bd9e2022-03-28 18:52:46 +05301507 return frappe.db.sql(
1508 """
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301509 select *, posting_datetime as "timestamp"
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301510 from `tabStock Ledger Entry`
Akhil Narang3effaf22024-03-27 11:37:26 +05301511 where item_code = %(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301512 and is_cancelled = 0
Akhil Narang3effaf22024-03-27 11:37:26 +05301513 {conditions}
1514 order by posting_datetime {order}, creation {order}
1515 {limit} {for_update}""".format(
1516 conditions=conditions,
1517 limit=limit or "",
1518 for_update=for_update and "for update" or "",
1519 order=order,
1520 ),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301521 previous_sle,
1522 as_dict=1,
1523 debug=debug,
1524 )
1525
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301526
Nabin Haita77b8c92020-12-21 14:45:50 +05301527def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301528 return frappe.db.get_value(
1529 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301530 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301531 [
1532 "item_code",
1533 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301534 "actual_qty",
1535 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301536 "posting_date",
1537 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301538 "voucher_detail_no",
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301539 "posting_datetime as timestamp",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301540 ],
1541 as_dict=1,
1542 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301543
Ankush Menatce0514c2022-02-15 11:41:41 +05301544
Akhil Narang3effaf22024-03-27 11:37:26 +05301545def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None):
Ankush Menat102fff22022-02-19 15:51:04 +05301546 sle = frappe.qb.DocType("Stock Ledger Entry")
1547
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301548 timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time)
Ankush Menat102fff22022-02-19 15:51:04 +05301549 if creation:
Akhil Narang3effaf22024-03-27 11:37:26 +05301550 timestamp_condition |= (sle.posting_datetime == get_combine_datetime(posting_date, posting_time)) & (
1551 sle.creation < creation
1552 )
Ankush Menat102fff22022-02-19 15:51:04 +05301553
1554 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301555 frappe.qb.from_(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301556 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301557 .where(
1558 (sle.item_code == item_code)
1559 & (sle.warehouse == warehouse)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301560 & (sle.batch_no == batch_no)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301561 & (sle.is_cancelled == 0)
1562 )
1563 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301564 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301565
1566 if batch_details and batch_details[0].batch_qty:
1567 return batch_details[0].batch_value / batch_details[0].batch_qty
1568
1569
Ankush Menat494bd9e2022-03-28 18:52:46 +05301570def get_valuation_rate(
1571 item_code,
1572 warehouse,
1573 voucher_type,
1574 voucher_no,
1575 allow_zero_rate=False,
1576 currency=None,
1577 company=None,
1578 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301579 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301580 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301581):
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301582 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1583
Ankush Menatf7ffe042021-11-01 13:21:14 +05301584 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301585 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301586
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301587 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1588 table = frappe.qb.DocType("Stock Ledger Entry")
1589 query = (
1590 frappe.qb.from_(table)
1591 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1592 .where(
1593 (table.item_code == item_code)
1594 & (table.warehouse == warehouse)
1595 & (table.batch_no == batch_no)
1596 & (table.is_cancelled == 0)
1597 & (table.voucher_no != voucher_no)
1598 & (table.voucher_type != voucher_type)
1599 )
1600 )
1601
1602 last_valuation_rate = query.run()
1603 if last_valuation_rate:
1604 return flt(last_valuation_rate[0][0])
1605
Ankush Menat342d09a2022-02-19 14:28:51 +05301606 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301607 if warehouse and serial_and_batch_bundle:
1608 batch_obj = BatchNoValuation(
1609 sle=frappe._dict(
1610 {
1611 "item_code": item_code,
1612 "warehouse": warehouse,
1613 "actual_qty": -1,
1614 "serial_and_batch_bundle": serial_and_batch_bundle,
1615 }
1616 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301617 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301618
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301619 return batch_obj.get_incoming_rate()
1620
Ankush Menatf7ffe042021-11-01 13:21:14 +05301621 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301622 if last_valuation_rate := frappe.db.sql(
1623 """select valuation_rate
1624 from `tabStock Ledger Entry` force index (item_warehouse)
1625 where
1626 item_code = %s
1627 AND warehouse = %s
1628 AND valuation_rate >= 0
1629 AND is_cancelled = 0
1630 AND NOT (voucher_no = %s AND voucher_type = %s)
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301631 order by posting_datetime desc, name desc limit 1""",
Akhil Narangdd911aa2023-09-26 13:45:39 +05301632 (item_code, warehouse, voucher_no, voucher_type),
1633 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301634 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301635
1636 # If negative stock allowed, and item delivered without any incoming entry,
1637 # system does not found any SLE, then take valuation rate from Item
1638 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301639
1640 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301641 # try Item Standard rate
1642 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301643
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301644 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301645 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301646 valuation_rate = frappe.db.get_value(
1647 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1648 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301649
Ankush Menat494bd9e2022-03-28 18:52:46 +05301650 if (
1651 not allow_zero_rate
1652 and not valuation_rate
1653 and raise_error_if_no_rate
1654 and cint(erpnext.is_perpetual_inventory_enabled(company))
1655 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301656 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301657
Ankush Menat494bd9e2022-03-28 18:52:46 +05301658 message = _(
1659 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1660 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301661 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301662 solutions = (
1663 "<li>"
1664 + _(
1665 "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."
1666 ).format(voucher_type)
1667 + "</li>"
1668 )
1669 solutions += (
1670 "<li>"
1671 + _("If not, you can Cancel / Submit this entry")
Akhil Narang3effaf22024-03-27 11:37:26 +05301672 + " {} ".format(frappe.bold(_("after")))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301673 + _("performing either one below:")
1674 + "</li>"
1675 )
Marica97715f22020-05-11 20:45:37 +05301676 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1677 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1678 msg = message + solutions + sub_solutions + "</li>"
1679
1680 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301681
1682 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301683
Ankush Menat494bd9e2022-03-28 18:52:46 +05301684
Ankush Menate7109c12021-08-26 16:40:45 +05301685def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301686 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301687 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301688 qty_shift = args.actual_qty
1689
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301690 args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
Ankush Menat7c839c42022-05-06 12:09:08 +05301691
marination8418c4b2021-06-22 21:35:25 +05301692 # find difference/shift in qty caused by stock reconciliation
1693 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301694 qty_shift = get_stock_reco_qty_shift(args)
1695
1696 # find the next nearest stock reco so that we only recalculate SLEs till that point
1697 next_stock_reco_detail = get_next_stock_reco(args)
1698 if next_stock_reco_detail:
1699 detail = next_stock_reco_detail[0]
marination40389772021-07-02 17:13:45 +05301700 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301701
Ankush Menat494bd9e2022-03-28 18:52:46 +05301702 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301703 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301704 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301705 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301706 where
1707 item_code = %(item_code)s
1708 and warehouse = %(warehouse)s
1709 and voucher_no != %(voucher_no)s
1710 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301711 and (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301712 posting_datetime > %(posting_datetime)s
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301713 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301714 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301715 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301716 args,
1717 )
Nabin Hait186a0452021-02-18 14:14:21 +05301718
1719 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1720
Ankush Menat494bd9e2022-03-28 18:52:46 +05301721
marination40389772021-07-02 17:13:45 +05301722def get_stock_reco_qty_shift(args):
1723 stock_reco_qty_shift = 0
1724 if args.get("is_cancelled"):
1725 if args.get("previous_qty_after_transaction"):
1726 # get qty (balance) that was set at submission
1727 last_balance = args.get("previous_qty_after_transaction")
1728 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1729 else:
1730 stock_reco_qty_shift = flt(args.actual_qty)
1731 else:
1732 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301733 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301734 "qty_after_transaction"
1735 )
marination40389772021-07-02 17:13:45 +05301736
1737 if last_balance is not None:
1738 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1739 else:
1740 stock_reco_qty_shift = args.qty_after_transaction
1741
1742 return stock_reco_qty_shift
1743
Ankush Menat494bd9e2022-03-28 18:52:46 +05301744
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301745def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301746 """Returns next nearest stock reconciliaton's details."""
1747
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301748 sle = frappe.qb.DocType("Stock Ledger Entry")
1749
1750 query = (
1751 frappe.qb.from_(sle)
1752 .select(
1753 sle.name,
1754 sle.posting_date,
1755 sle.posting_time,
1756 sle.creation,
1757 sle.voucher_no,
1758 sle.item_code,
1759 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301760 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301761 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301762 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301763 )
1764 .where(
1765 (sle.item_code == kwargs.get("item_code"))
1766 & (sle.warehouse == kwargs.get("warehouse"))
1767 & (sle.voucher_type == "Stock Reconciliation")
1768 & (sle.voucher_no != kwargs.get("voucher_no"))
1769 & (sle.is_cancelled == 0)
1770 & (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301771 sle.posting_datetime
1772 >= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
marination40389772021-07-02 17:13:45 +05301773 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301774 )
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301775 .orderby(sle.posting_datetime)
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301776 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301777 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301778 )
1779
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301780 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301781 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301782
1783 return query.run(as_dict=True)
1784
marination40389772021-07-02 17:13:45 +05301785
1786def get_datetime_limit_condition(detail):
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301787 posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time)
1788
marination40389772021-07-02 17:13:45 +05301789 return f"""
1790 and
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301791 (posting_datetime < '{posting_datetime}'
marination40389772021-07-02 17:13:45 +05301792 or (
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301793 posting_datetime = '{posting_datetime}'
marination40389772021-07-02 17:13:45 +05301794 and creation < '{detail.creation}'
1795 )
1796 )"""
1797
Ankush Menat494bd9e2022-03-28 18:52:46 +05301798
Ankush Menate7109c12021-08-26 16:40:45 +05301799def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301800 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301801 return
barredterraeb9ee3f2023-12-05 11:22:55 +01001802 if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
Ankush Menat5eba5752021-12-07 23:03:52 +05301803 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301804
Ankush Menat5eba5752021-12-07 23:03:52 +05301805 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301806
1807 if is_negative_with_precision(neg_sle):
Akhil Narang3effaf22024-03-27 11:37:26 +05301808 message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301809 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301810 frappe.get_desk_link("Item", args.item_code),
1811 frappe.get_desk_link("Warehouse", args.warehouse),
1812 neg_sle[0]["posting_date"],
1813 neg_sle[0]["posting_time"],
1814 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1815 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301816
Ankush Menat494bd9e2022-03-28 18:52:46 +05301817 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301818
s-aga-rd9e28432023-10-27 16:35:35 +05301819 if args.batch_no:
1820 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1821 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1822 message = _(
1823 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1824 ).format(
1825 abs(neg_batch_sle[0]["cumulative_total"]),
1826 frappe.get_desk_link("Batch", args.batch_no),
1827 frappe.get_desk_link("Warehouse", args.warehouse),
1828 neg_batch_sle[0]["posting_date"],
1829 neg_batch_sle[0]["posting_time"],
1830 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1831 )
1832 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301833
s-aga-r73b65ac2023-11-01 18:35:07 +05301834 if args.reserved_stock:
1835 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301836
Nabin Haita77b8c92020-12-21 14:45:50 +05301837
Maricad6078aa2022-06-17 15:13:13 +05301838def is_negative_with_precision(neg_sle, is_batch=False):
1839 """
1840 Returns whether system precision rounded qty is insufficient.
1841 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1842 """
1843
1844 if not neg_sle:
1845 return False
1846
1847 field = "cumulative_total" if is_batch else "qty_after_transaction"
1848 precision = cint(frappe.db.get_default("float_precision")) or 2
1849 qty_deficit = flt(neg_sle[0][field], precision)
1850
1851 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1852
1853
Nabin Haita77b8c92020-12-21 14:45:50 +05301854def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301855 return frappe.db.sql(
1856 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301857 select
1858 qty_after_transaction, posting_date, posting_time,
1859 voucher_type, voucher_no
1860 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301861 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301862 item_code = %(item_code)s
1863 and warehouse = %(warehouse)s
1864 and voucher_no != %(voucher_no)s
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301865 and posting_datetime >= %(posting_datetime)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301866 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301867 and qty_after_transaction < 0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301868 order by posting_datetime asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301869 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301870 """,
1871 args,
1872 as_dict=1,
1873 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301874
Ankush Menat5eba5752021-12-07 23:03:52 +05301875
1876def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301877 return frappe.db.sql(
1878 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301879 with batch_ledger as (
1880 select
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301881 posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
1882 sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
Ankush Menat5eba5752021-12-07 23:03:52 +05301883 from `tabStock Ledger Entry`
1884 where
1885 item_code = %(item_code)s
1886 and warehouse = %(warehouse)s
1887 and batch_no=%(batch_no)s
1888 and is_cancelled = 0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301889 order by posting_datetime, creation
Ankush Menat5eba5752021-12-07 23:03:52 +05301890 )
1891 select * from batch_ledger
1892 where
1893 cumulative_total < 0.0
Rohit Waghchaured80ca522024-02-07 21:56:21 +05301894 and posting_datetime >= %(posting_datetime)s
Ankush Menat5eba5752021-12-07 23:03:52 +05301895 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301896 """,
1897 args,
1898 as_dict=1,
1899 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301900
1901
s-aga-rd9e28432023-10-27 16:35:35 +05301902def validate_reserved_stock(kwargs):
1903 if kwargs.serial_no:
1904 serial_nos = kwargs.serial_no.split("\n")
1905 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1906
s-aga-re1a87a82023-10-31 18:41:58 +05301907 elif kwargs.batch_no:
1908 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1909
s-aga-rd9e28432023-10-27 16:35:35 +05301910 elif kwargs.serial_and_batch_bundle:
1911 sbb_entries = frappe.db.get_all(
1912 "Serial and Batch Entry",
1913 {
1914 "parenttype": "Serial and Batch Bundle",
1915 "parent": kwargs.serial_and_batch_bundle,
1916 "docstatus": 1,
1917 },
s-aga-re1a87a82023-10-31 18:41:58 +05301918 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301919 )
s-aga-rd9e28432023-10-27 16:35:35 +05301920
s-aga-re1a87a82023-10-31 18:41:58 +05301921 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301922 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301923 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1924 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301925
s-aga-r92317062023-11-02 10:36:00 +05301926 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1927 precision = cint(frappe.db.get_default("float_precision")) or 2
1928 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301929
s-aga-r92317062023-11-02 10:36:00 +05301930 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1931 if diff < 0 and abs(diff) > 0.0001:
1932 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1933 abs(diff),
1934 frappe.get_desk_link("Item", kwargs.item_code),
1935 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1936 nowdate(),
1937 nowtime(),
1938 )
1939 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301940
s-aga-rd9e28432023-10-27 16:35:35 +05301941
1942def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
Akhil Narang3effaf22024-03-27 11:37:26 +05301943 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(item_code, warehouse, serial_nos):
1944 if common_serial_nos := list(set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))):
s-aga-rd9e28432023-10-27 16:35:35 +05301945 msg = _(
1946 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1947 )
1948 msg += "<br />"
1949 msg += _("Example: Serial No {0} reserved in {1}.").format(
1950 frappe.bold(common_serial_nos[0]),
1951 frappe.get_desk_link(
1952 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1953 ),
1954 )
1955 frappe.throw(msg, title=_("Reserved Serial No."))
1956
1957
s-aga-re1a87a82023-10-31 18:41:58 +05301958def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1959 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1960 available_batches = get_available_batches(
1961 frappe._dict(
1962 {
1963 "item_code": item_code,
1964 "warehouse": warehouse,
1965 "posting_date": nowdate(),
1966 "posting_time": nowtime(),
1967 }
1968 )
1969 )
1970 available_batches_map = {row.batch_no: row.qty for row in available_batches}
1971 precision = cint(frappe.db.get_default("float_precision")) or 2
1972
1973 for batch_no in batch_nos:
1974 diff = flt(
1975 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
1976 )
1977 if diff < 0 and abs(diff) > 0.0001:
1978 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1979 abs(diff),
1980 frappe.get_desk_link("Batch", batch_no),
1981 frappe.get_desk_link("Warehouse", warehouse),
1982 nowdate(),
1983 nowtime(),
1984 )
1985 frappe.throw(msg, title=_("Reserved Stock for Batch"))
1986
1987
Akhil Narang3effaf22024-03-27 11:37:26 +05301988def is_negative_stock_allowed(*, item_code: str | None = None) -> bool:
Ankush Menateb8b4242022-02-12 13:08:28 +05301989 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1990 return True
1991 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1992 return True
1993 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301994
1995
1996def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1997 """
1998 For inter company transfer, incoming rate is the average of the outgoing rate
1999 """
2000 rate = 0.0
2001
2002 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
2003
2004 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
2005
2006 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
2007
2008 if reference_name:
2009 rate = frappe.get_cached_value(
2010 doctype,
2011 reference_name,
2012 "incoming_rate",
2013 )
2014
2015 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05302016
2017
2018def is_internal_transfer(sle):
2019 data = frappe.get_cached_value(
2020 sle.voucher_type,
2021 sle.voucher_no,
2022 ["is_internal_supplier", "represents_company", "company"],
2023 as_dict=True,
2024 )
2025
2026 if data.is_internal_supplier and data.represents_company == data.company:
2027 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302028
2029
2030def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
2031 table = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaured80ca522024-02-07 21:56:21 +05302032 posting_datetime = get_combine_datetime(posting_date, posting_time)
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302033
2034 query = (
2035 frappe.qb.from_(table)
2036 .select(Sum(table.stock_value_difference).as_("value"))
2037 .where(
2038 (table.is_cancelled == 0)
2039 & (table.item_code == item_code)
2040 & (table.warehouse == warehouse)
Rohit Waghchaured80ca522024-02-07 21:56:21 +05302041 & (table.posting_datetime <= posting_datetime)
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302042 )
2043 )
2044
2045 if voucher_no:
2046 query = query.where(table.voucher_no != voucher_no)
2047
2048 difference_amount = query.run()
2049 return flt(difference_amount[0][0]) if difference_amount else 0