blob: 9142a27f4c09189b60b2594b2d05a78c317f0284 [file] [log] [blame]
Maricad6078aa2022-06-17 15:13:13 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait902e8602013-01-08 18:29:24 +05303
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05304import copy
Akhil Narang21c3d9c2023-10-21 11:19:45 +05305import gzip
Nabin Hait26d46552013-01-09 15:23:05 +05306import json
Ankush Menatecdb4932022-04-17 19:06:13 +05307from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05308
9import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053010from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053011from frappe.model.meta import get_field_precision
Rohit Waghchaurebb954512023-06-02 00:11:43 +053012from frappe.query_builder import Case
Ankush Menate1c16872022-04-21 20:01:48 +053013from frappe.query_builder.functions import CombineDatetime, Sum
s-aga-re1a87a82023-10-31 18:41:58 +053014from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json
Achilles Rasquinha361366e2018-02-14 17:08:59 +053015
Chillar Anand915b3432021-09-02 16:44:59 +053016import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053017from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
mergify[bot]27a1e3b2023-10-16 19:15:18 +053018from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
s-aga-re1a87a82023-10-31 18:41:58 +053019from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
20 get_available_batches,
21)
22from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
23 get_sre_reserved_batch_nos_details,
s-aga-rd9e28432023-10-27 16:35:35 +053024 get_sre_reserved_serial_nos_details,
25)
Chillar Anand915b3432021-09-02 16:44:59 +053026from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053027 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053028 get_or_make_bin,
s-aga-r73b65ac2023-11-01 18:35:07 +053029 get_stock_balance,
Chillar Anand915b3432021-09-02 16:44:59 +053030 get_valuation_method,
31)
Ankush Menatb534fee2022-02-19 20:58:36 +053032from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053033
Nabin Hait97bce3a2021-07-12 13:24:43 +053034
Ankush Menat494bd9e2022-03-28 18:52:46 +053035class NegativeStockError(frappe.ValidationError):
36 pass
37
38
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053039class SerialNoExistsInFutureTransaction(frappe.ValidationError):
40 pass
Nabin Hait902e8602013-01-08 18:29:24 +053041
Anand Doshi5b004ff2013-09-25 19:55:41 +053042
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053043def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053044 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053045
Ankush Menat494bd9e2022-03-28 18:52:46 +053046 args:
47 - allow_negative_stock: disable negative stock valiations if true
48 - via_landed_cost_voucher: landed cost voucher cancels and reposts
49 entries of purchase document. This flag is used to identify if
50 cancellation and repost is happening via landed cost voucher, in
51 such cases certain validations need to be ignored (like negative
52 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053053 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053054 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053055
Nabin Haitca775742013-09-26 16:16:44 +053056 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053057 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053058 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053059 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053060 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053061
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053062 args = get_args_for_future_sle(sl_entries[0])
63 future_sle_exists(args, sl_entries)
64
Nabin Haitca775742013-09-26 16:16:44 +053065 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053066 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053067 validate_serial_no(sle)
68
Nabin Haita77b8c92020-12-21 14:45:50 +053069 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053070 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053071
Ankush Menat494bd9e2022-03-28 18:52:46 +053072 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
73 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
74 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
75 )
76 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053077
Ankush Menat494bd9e2022-03-28 18:52:46 +053078 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
79 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
80 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
81 )
82 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053083
Ankush Menat494bd9e2022-03-28 18:52:46 +053084 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053085 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053086
Nabin Haita77b8c92020-12-21 14:45:50 +053087 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053088
89 if sle.get("voucher_type") == "Stock Reconciliation":
90 # preserve previous_qty_after_transaction for qty reposting
91 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
92
Ankush Menat494bd9e2022-03-28 18:52:46 +053093 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053094 if is_stock_item:
95 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
s-aga-r73b65ac2023-11-01 18:35:07 +053096 args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
Ankush Menatcef84c22021-12-03 12:18:59 +053097 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053098 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053099 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530100 frappe.msgprint(
101 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
102 )
103
Ankush Menatcef84c22021-12-03 12:18:59 +0530104
105def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
106 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
107 if not args.get("posting_date"):
108 args["posting_date"] = nowdate()
109
marination7a5fd712022-07-04 17:46:54 +0530110 if not (args.get("is_cancelled") and via_landed_cost_voucher):
111 # Reposts only current voucher SL Entries
112 # Updates valuation rate, stock value, stock queue for current transaction
113 update_entries_after(
114 {
115 "item_code": args.get("item_code"),
116 "warehouse": args.get("warehouse"),
117 "posting_date": args.get("posting_date"),
118 "posting_time": args.get("posting_time"),
119 "voucher_type": args.get("voucher_type"),
120 "voucher_no": args.get("voucher_no"),
121 "sle_id": args.get("name"),
122 "creation": args.get("creation"),
s-aga-r73b65ac2023-11-01 18:35:07 +0530123 "reserved_stock": args.get("reserved_stock"),
marination7a5fd712022-07-04 17:46:54 +0530124 },
125 allow_negative_stock=allow_negative_stock,
126 via_landed_cost_voucher=via_landed_cost_voucher,
127 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530128
129 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530130 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530131 update_qty_in_future_sle(args, allow_negative_stock)
132
Nabin Haitadeb9762014-10-06 11:53:52 +0530133
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530134def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530135 return frappe._dict(
136 {
137 "voucher_type": row.get("voucher_type"),
138 "voucher_no": row.get("voucher_no"),
139 "posting_date": row.get("posting_date"),
140 "posting_time": row.get("posting_time"),
141 }
142 )
143
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530144
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530145def validate_serial_no(sle):
146 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530147
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530148 for sn in get_serial_nos(sle.serial_no):
149 args = copy.deepcopy(sle)
150 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530151 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530152
153 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530154 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530155 voucher_type = frappe.bold(row.voucher_type)
156 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530157 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530158
159 if vouchers:
160 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530161 msg = (
162 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
163 The list of the transactions are as below."""
164 + "<br><br><ul><li>"
165 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530166
Ankush Menat494bd9e2022-03-28 18:52:46 +0530167 msg += "</li><li>".join(vouchers)
168 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530169
Ankush Menat494bd9e2022-03-28 18:52:46 +0530170 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530171 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
172
Ankush Menat494bd9e2022-03-28 18:52:46 +0530173
Nabin Hait186a0452021-02-18 14:14:21 +0530174def validate_cancellation(args):
175 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530176 repost_entry = frappe.db.get_value(
177 "Repost Item Valuation",
178 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
179 ["name", "status"],
180 as_dict=1,
181 )
Nabin Hait186a0452021-02-18 14:14:21 +0530182
183 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530184 if repost_entry.status == "In Progress":
185 frappe.throw(
186 _(
187 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
188 )
189 )
190 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530191 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530192 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530193 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530194 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530195
Ankush Menat494bd9e2022-03-28 18:52:46 +0530196
Nabin Hait9653f602013-08-20 15:37:33 +0530197def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530198 frappe.db.sql(
199 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530200 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530201 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530202 (now(), frappe.session.user, voucher_type, voucher_no),
203 )
204
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530205
Nabin Hait54c865e2015-03-27 15:38:31 +0530206def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530207 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530208 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530209 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530210 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530211 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530212 sle.submit()
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530213
214 # Added to handle the case when the stock ledger entry is created from the repostig
215 if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
216 sle.db_set("creation", args.get("creation_time"))
217
Nabin Haita77b8c92020-12-21 14:45:50 +0530218 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530219
Ankush Menat494bd9e2022-03-28 18:52:46 +0530220
221def repost_future_sle(
222 args=None,
223 voucher_type=None,
224 voucher_no=None,
225 allow_negative_stock=None,
226 via_landed_cost_voucher=False,
227 doc=None,
228):
Nabin Haite1fa7232022-07-20 15:19:09 +0530229 if not args:
230 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530231
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530232 reposting_data = {}
233 if doc and doc.reposting_data_file:
234 reposting_data = get_reposting_data(doc.reposting_data_file)
235
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530236 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530237 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530238 )
239 if items_to_be_repost:
240 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530241
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530242 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
243 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530244
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530245 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530246 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530247 validate_item_warehouse(args[i])
248
Ankush Menat494bd9e2022-03-28 18:52:46 +0530249 obj = update_entries_after(
250 {
251 "item_code": args[i].get("item_code"),
252 "warehouse": args[i].get("warehouse"),
253 "posting_date": args[i].get("posting_date"),
254 "posting_time": args[i].get("posting_time"),
255 "creation": args[i].get("creation"),
256 "distinct_item_warehouses": distinct_item_warehouses,
257 },
258 allow_negative_stock=allow_negative_stock,
259 via_landed_cost_voucher=via_landed_cost_voucher,
260 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530261 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530262
Ankush Menat494bd9e2022-03-28 18:52:46 +0530263 distinct_item_warehouses[
264 (args[i].get("item_code"), args[i].get("warehouse"))
265 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530266
Nabin Hait97bce3a2021-07-12 13:24:43 +0530267 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530268 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530269 if ("args_idx" not in data and not data.reposting_status) or (
270 data.sle_changed and data.reposting_status
271 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530272 data.args_idx = len(args)
273 args.append(data.sle)
274 elif data.sle_changed and not data.reposting_status:
275 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530276
Nabin Hait97bce3a2021-07-12 13:24:43 +0530277 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530278 i += 1
279
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530280 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530281 update_args_in_repost_item_valuation(
282 doc, i, args, distinct_item_warehouses, affected_transactions
283 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530284
Ankush Menat494bd9e2022-03-28 18:52:46 +0530285
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530286def get_reposting_data(file_path) -> dict:
287 file_name = frappe.db.get_value(
288 "File",
289 {
290 "file_url": file_path,
291 "attached_to_field": "reposting_data_file",
292 },
293 "name",
294 )
295
296 if not file_name:
297 return frappe._dict()
298
299 attached_file = frappe.get_doc("File", file_name)
300
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530301 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530302 if data := json.loads(data.decode("utf-8")):
303 data = data
304
305 return parse_json(data)
306
307
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530308def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530309 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530310 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530311 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530312 frappe.throw(_(validation_msg))
313
Ankush Menat494bd9e2022-03-28 18:52:46 +0530314
Ankush Menatecdb4932022-04-17 19:06:13 +0530315def update_args_in_repost_item_valuation(
316 doc, index, args, distinct_item_warehouses, affected_transactions
317):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530318 if not doc.items_to_be_repost:
319 file_name = ""
320 if doc.reposting_data_file:
321 file_name = get_reposting_file_name(doc.doctype, doc.name)
322 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
323
324 doc.reposting_data_file = create_json_gz_file(
325 {
326 "items_to_be_repost": args,
327 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
328 "affected_transactions": affected_transactions,
329 },
330 doc,
331 file_name,
332 )
333
334 doc.db_set(
335 {
336 "current_index": index,
337 "total_reposting_count": len(args),
338 "reposting_data_file": doc.reposting_data_file,
339 }
340 )
341
342 else:
343 doc.db_set(
344 {
345 "items_to_be_repost": json.dumps(args, default=str),
346 "distinct_item_and_warehouse": json.dumps(
347 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
348 ),
349 "current_index": index,
350 "affected_transactions": frappe.as_json(affected_transactions),
351 }
352 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530353
Ankush Menatecdb4932022-04-17 19:06:13 +0530354 if not frappe.flags.in_test:
355 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530356
Ankush Menat494bd9e2022-03-28 18:52:46 +0530357 frappe.publish_realtime(
358 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530359 {
360 "name": doc.name,
361 "items_to_be_repost": json.dumps(args, default=str),
362 "current_index": index,
363 "total_reposting_count": len(args),
364 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530365 doctype=doc.doctype,
366 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530367 )
368
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530369
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530370def get_reposting_file_name(dt, dn):
371 return frappe.db.get_value(
372 "File",
373 {
374 "attached_to_doctype": dt,
375 "attached_to_name": dn,
376 "attached_to_field": "reposting_data_file",
377 },
378 "name",
379 )
380
381
382def create_json_gz_file(data, doc, file_name=None) -> str:
383 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530384 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530385
386 if not file_name:
387 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
388 _file = frappe.get_doc(
389 {
390 "doctype": "File",
391 "file_name": json_filename,
392 "attached_to_doctype": doc.doctype,
393 "attached_to_name": doc.name,
394 "attached_to_field": "reposting_data_file",
395 "content": compressed_content,
396 "is_private": 1,
397 }
398 )
399 _file.save(ignore_permissions=True)
400
401 return _file.file_url
402 else:
403 file_doc = frappe.get_doc("File", file_name)
404 path = file_doc.get_full_path()
405
406 with open(path, "wb") as f:
407 f.write(compressed_content)
408
409 return doc.reposting_data_file
410
411
412def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
413 if not reposting_data and doc and doc.reposting_data_file:
414 reposting_data = get_reposting_data(doc.reposting_data_file)
415
416 if reposting_data and reposting_data.items_to_be_repost:
417 return reposting_data.items_to_be_repost
418
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530419 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530420
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530421 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530422 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530423
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530424 if not items_to_be_repost and voucher_type and voucher_no:
425 items_to_be_repost = frappe.db.get_all(
426 "Stock Ledger Entry",
427 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
428 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
429 order_by="creation asc",
430 group_by="item_code, warehouse",
431 )
432
Nabin Haite1fa7232022-07-20 15:19:09 +0530433 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530434
Ankush Menat494bd9e2022-03-28 18:52:46 +0530435
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530436def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
437 if not reposting_data and doc and doc.reposting_data_file:
438 reposting_data = get_reposting_data(doc.reposting_data_file)
439
440 if reposting_data and reposting_data.distinct_item_and_warehouse:
441 return reposting_data.distinct_item_and_warehouse
442
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530443 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530444
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530445 if doc and doc.distinct_item_and_warehouse:
446 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530447 distinct_item_warehouses = {
448 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
449 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530450 else:
451 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530452 distinct_item_warehouses.setdefault(
453 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
454 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530455
456 return distinct_item_warehouses
457
Ankush Menat494bd9e2022-03-28 18:52:46 +0530458
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530459def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
460 if not reposting_data and doc and doc.reposting_data_file:
461 reposting_data = get_reposting_data(doc.reposting_data_file)
462
463 if reposting_data and reposting_data.affected_transactions:
464 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
465
Ankush Menatecdb4932022-04-17 19:06:13 +0530466 if not doc.affected_transactions:
467 return set()
468
469 transactions = frappe.parse_json(doc.affected_transactions)
470 return {tuple(transaction) for transaction in transactions}
471
472
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530473def get_current_index(doc=None):
474 if doc and doc.current_index:
475 return doc.current_index
476
Ankush Menat494bd9e2022-03-28 18:52:46 +0530477
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530478class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530479 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530480 update valution rate and qty after transaction
481 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530482
Ankush Menat494bd9e2022-03-28 18:52:46 +0530483 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530484
Ankush Menat494bd9e2022-03-28 18:52:46 +0530485 args = {
486 "item_code": "ABC",
487 "warehouse": "XYZ",
488 "posting_date": "2012-12-12",
489 "posting_time": "12:00"
490 }
Nabin Hait902e8602013-01-08 18:29:24 +0530491 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530492
493 def __init__(
494 self,
495 args,
496 allow_zero_rate=False,
497 allow_negative_stock=None,
498 via_landed_cost_voucher=False,
499 verbose=1,
500 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530501 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530502 self.verbose = verbose
503 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530504 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530505 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530506 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
507 item_code=self.item_code
508 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530509
Nabin Haita77b8c92020-12-21 14:45:50 +0530510 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530511 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530512 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530513
Nabin Haita77b8c92020-12-21 14:45:50 +0530514 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530515 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530516 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530517
518 self.new_items_found = False
519 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530520 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-r73b65ac2023-11-01 18:35:07 +0530521 self.reserved_stock = flt(self.args.reserved_stock)
Nabin Haita77b8c92020-12-21 14:45:50 +0530522
523 self.data = frappe._dict()
524 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530525 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530526
Maricad6078aa2022-06-17 15:13:13 +0530527 def set_precision(self):
528 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
529 self.currency_precision = get_field_precision(
530 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530531 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530532
533 def initialize_previous_data(self, args):
534 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530535 Get previous sl entries for current item for each related warehouse
536 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530537
Ankush Menat494bd9e2022-03-28 18:52:46 +0530538 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530539
Ankush Menat494bd9e2022-03-28 18:52:46 +0530540 self.data = {
541 warehouse1: {
542 'previus_sle': {},
543 'qty_after_transaction': 10,
544 'valuation_rate': 100,
545 'stock_value': 1000,
546 'prev_stock_value': 1000,
547 'stock_queue': '[[10, 100]]',
548 'stock_value_difference': 1000
549 }
550 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530551
552 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530553 self.data.setdefault(args.warehouse, frappe._dict())
554 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530555 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530556 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530557
Ankush Menatc1d986a2021-08-31 19:43:42 +0530558 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
559 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
560
Ankush Menat494bd9e2022-03-28 18:52:46 +0530561 warehouse_dict.update(
562 {
563 "prev_stock_value": previous_sle.stock_value or 0.0,
564 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
565 "stock_value_difference": 0.0,
566 }
567 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530568
Nabin Haita77b8c92020-12-21 14:45:50 +0530569 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530570 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530571
Nabin Haita77b8c92020-12-21 14:45:50 +0530572 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530573 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530574 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530575 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530576 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530577 entries_to_fix = self.get_future_entries_to_fix()
578
579 i = 0
580 while i < len(entries_to_fix):
581 sle = entries_to_fix[i]
582 i += 1
583
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530584 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530585 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530586
Nabin Haita77b8c92020-12-21 14:45:50 +0530587 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530588 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530589
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530590 if self.exceptions:
591 self.raise_exceptions()
592
Nabin Hait186a0452021-02-18 14:14:21 +0530593 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530594 sl_entries = self.get_sle_against_current_voucher()
595 for sle in sl_entries:
596 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530597
Nabin Haita77b8c92020-12-21 14:45:50 +0530598 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530599 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530600
Ankush Menat494bd9e2022-03-28 18:52:46 +0530601 return frappe.db.sql(
602 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530603 select
604 *, timestamp(posting_date, posting_time) as "timestamp"
605 from
606 `tabStock Ledger Entry`
607 where
608 item_code = %(item_code)s
609 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530610 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530611 and (
612 posting_date = %(posting_date)s and
613 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
614 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530615 order by
616 creation ASC
617 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530618 """,
619 self.args,
620 as_dict=1,
621 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530622
Nabin Haita77b8c92020-12-21 14:45:50 +0530623 def get_future_entries_to_fix(self):
624 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530625 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
626 {"item_code": self.item_code, "warehouse": self.args.warehouse}
627 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530628
Nabin Haita77b8c92020-12-21 14:45:50 +0530629 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530630
Nabin Haita77b8c92020-12-21 14:45:50 +0530631 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530632 dependant_sle = get_sle_by_voucher_detail_no(
633 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
634 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530635
Nabin Haita77b8c92020-12-21 14:45:50 +0530636 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530637 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530638 elif (
639 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
640 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530641 return entries_to_fix
642 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530643 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530644 return entries_to_fix
645 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
646 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530647 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530648 self.initialize_previous_data(dependant_sle)
649 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530650 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530651
652 def update_distinct_item_warehouses(self, dependant_sle):
653 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530654 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530655
Nabin Hait97bce3a2021-07-12 13:24:43 +0530656 if key not in self.distinct_item_warehouses:
657 self.distinct_item_warehouses[key] = val
658 self.new_items_found = True
659 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530660 existing_sle_posting_date = (
661 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
662 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530663
664 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
665
Nabin Hait97bce3a2021-07-12 13:24:43 +0530666 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
667 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530668 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
669 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530670 self.distinct_item_warehouses[key] = val
671 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530672 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
673 # Future dependent voucher needs to be repost to get the correct stock value
674 # If dependent voucher has not reposted, then add it to the list
675 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530676 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530677 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
678 self.distinct_item_warehouses[key] = val
679
680 def get_dependent_voucher_detail_nos(self, key):
681 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
682 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
683
684 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530685
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530686 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530687 # previous sle data for this warehouse
688 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530689 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530690
Anand Doshi0dc79f42015-04-06 12:59:34 +0530691 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 +0530692 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530693 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530694 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530695 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530696 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530697
Nabin Haita77b8c92020-12-21 14:45:50 +0530698 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530699 if not self.args.get("sle_id"):
700 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530701
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530702 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530703 sle.voucher_type == "Stock Reconciliation"
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530704 and (
705 sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
706 )
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530707 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530708 and not self.args.get("sle_id")
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530709 ):
710 self.reset_actual_qty_for_stock_reco(sle)
711
712 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530713 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
714 and sle.voucher_detail_no
715 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530716 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530717 ):
718 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
719
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530720 dimensions = get_inventory_dimensions()
721 has_dimensions = False
722 if dimensions:
723 for dimension in dimensions:
724 if sle.get(dimension.get("fieldname")):
725 has_dimensions = True
726
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530727 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530728 self.calculate_valuation_for_serial_batch_bundle(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530729 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530730 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530731 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530732 self.wh_data.valuation_rate = sle.valuation_rate
733 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530734 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
735 self.wh_data.valuation_rate
736 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530737 if self.valuation_method != "Moving Average":
738 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530739 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530740 if self.valuation_method == "Moving Average":
741 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530742 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530743 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
744 self.wh_data.valuation_rate
745 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530746 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530747 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530748
Rushabh Mehta54047782013-12-26 11:07:46 +0530749 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530750 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530751 if not self.wh_data.qty_after_transaction:
752 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530753
Nabin Haita77b8c92020-12-21 14:45:50 +0530754 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
755 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530756
Nabin Hait902e8602013-01-08 18:29:24 +0530757 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530758 sle.qty_after_transaction = self.wh_data.qty_after_transaction
759 sle.valuation_rate = self.wh_data.valuation_rate
760 sle.stock_value = self.wh_data.stock_value
761 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530762
rohitwaghchaurea8216b92023-11-09 12:22:26 +0530763 if not sle.is_adjustment_entry or not self.args.get("sle_id"):
764 sle.stock_value_difference = stock_value_difference
765
766 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530767 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530768
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530769 if not self.args.get("sle_id") or (
770 sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
771 ):
Ankush Menat701878f2022-03-01 18:08:29 +0530772 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530773
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530774 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530775 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
776 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
777
778 if sle.actual_qty < 0:
779 sle.actual_qty = (
780 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
781 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530782 )
783
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530784 if abs(sle.actual_qty) == 0.0:
785 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530786
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530787 def calculate_valuation_for_serial_batch_bundle(self, sle):
788 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
789
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530790 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530791 doc.calculate_qty_and_amount(save=True)
792
793 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
794
795 self.wh_data.qty_after_transaction += doc.total_qty
796 if self.wh_data.qty_after_transaction:
797 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
798
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530799 def validate_negative_stock(self, sle):
800 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530801 validate negative stock for entries current datetime onwards
802 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530803 """
s-aga-rf0acb202023-04-12 14:13:54 +0530804 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530805 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530806
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530807 if diff < 0 and abs(diff) > 0.0001:
808 # negative stock!
809 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530810 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530811 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530812 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530813 return True
814
Nabin Haita77b8c92020-12-21 14:45:50 +0530815 def get_dynamic_incoming_outgoing_rate(self, sle):
816 # Get updated incoming/outgoing rate from transaction
817 if sle.recalculate_rate:
818 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
819
820 if flt(sle.actual_qty) >= 0:
821 sle.incoming_rate = rate
822 else:
823 sle.outgoing_rate = rate
824
825 def get_incoming_outgoing_rate_from_transaction(self, sle):
826 rate = 0
827 # Material Transfer, Repack, Manufacturing
828 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530829 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530830 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
831 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530832 elif sle.voucher_type in (
833 "Purchase Receipt",
834 "Purchase Invoice",
835 "Delivery Note",
836 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530837 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530838 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530839 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530840 from erpnext.controllers.sales_and_purchase_return import (
841 get_rate_for_return, # don't move this import to top
842 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530843
844 rate = get_rate_for_return(
845 sle.voucher_type,
846 sle.voucher_no,
847 sle.item_code,
848 voucher_detail_no=sle.voucher_detail_no,
849 sle=sle,
850 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530851
852 elif (
853 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530854 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530855 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530856 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530857 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530858 else:
859 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530860 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530861 elif sle.voucher_type == "Subcontracting Receipt":
862 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530863 else:
864 rate_field = "incoming_rate"
865
866 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530867 item_code, incoming_rate = frappe.db.get_value(
868 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
869 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530870
871 if item_code == sle.item_code:
872 rate = incoming_rate
873 else:
874 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
875 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530876 elif sle == "Subcontracting Receipt":
877 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530878 else:
879 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530880
Ankush Menat494bd9e2022-03-28 18:52:46 +0530881 rate = frappe.db.get_value(
882 ref_doctype,
883 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
884 rate_field,
885 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530886
887 return rate
888
889 def update_outgoing_rate_on_transaction(self, sle):
890 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530891 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
892 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530893 """
894 if sle.actual_qty and sle.voucher_detail_no:
895 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
896
897 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
898 self.update_rate_on_stock_entry(sle, outgoing_rate)
899 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
900 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
901 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
902 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530903 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
904 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530905 elif sle.voucher_type == "Stock Reconciliation":
906 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530907
908 def update_rate_on_stock_entry(self, sle, outgoing_rate):
909 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
910
Ankush Menat701878f2022-03-01 18:08:29 +0530911 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
912 if not sle.dependant_sle_voucher_detail_no:
913 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530914
915 def recalculate_amounts_in_stock_entry(self, voucher_no):
916 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530917 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
918 stock_entry.db_update()
919 for d in stock_entry.items:
920 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530921
Nabin Haita77b8c92020-12-21 14:45:50 +0530922 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
923 # Update item's incoming rate on transaction
924 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
925 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530926 frappe.db.set_value(
927 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
928 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530929 else:
930 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530931 frappe.db.set_value(
932 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530933 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530934 "incoming_rate",
935 outgoing_rate,
936 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530937
938 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
939 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530940 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
941 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
942 ):
943 frappe.db.set_value(
944 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
945 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530946 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530947 frappe.db.set_value(
948 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
949 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530950
951 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530952 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530953 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530954 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530955 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530956 d.db_update()
957
Sagar Sharma323bdf82022-05-17 15:14:07 +0530958 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530959 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
960 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530961 else:
962 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530963 "Subcontracting Receipt Supplied Item",
964 sle.voucher_detail_no,
965 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530966 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530967
s-aga-ra6cb6c62023-05-03 09:51:58 +0530968 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530969 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530970 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530971 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530972 d.db_update()
973
s-aga-r88a3f652023-05-30 16:54:28 +0530974 def update_rate_on_stock_reconciliation(self, sle):
975 if not sle.serial_no and not sle.batch_no:
976 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
977
978 for item in sr.items:
979 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +0530980 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +0530981 continue
982
983 previous_sle = get_previous_sle(
984 {
985 "item_code": item.item_code,
986 "warehouse": item.warehouse,
987 "posting_date": sr.posting_date,
988 "posting_time": sr.posting_time,
989 "sle": sle.name,
990 }
991 )
992
993 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
994 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
995 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
996
997 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +0530998 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +0530999 item.amount_difference = item.amount - item.current_amount
1000 else:
1001 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1002 sr.db_update()
1003
1004 for item in sr.items:
1005 item.db_update()
1006
Nabin Hait328c4f92020-01-02 19:00:32 +05301007 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1008 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301009 all_serial_nos = frappe.get_all(
1010 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1011 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301012
Ankush Menat494bd9e2022-03-28 18:52:46 +05301013 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 +05301014
1015 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301016 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301017 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301018 incoming_rate = frappe.db.sql(
1019 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301020 select incoming_rate
1021 from `tabStock Ledger Entry`
1022 where
1023 company = %s
1024 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301025 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301026 and (serial_no = %s
1027 or serial_no like %s
1028 or serial_no like %s
1029 or serial_no like %s
1030 )
1031 order by posting_date desc
1032 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301033 """,
1034 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1035 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301036
1037 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1038
1039 return incoming_values
1040
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301041 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301042 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301043 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301044 if new_stock_qty >= 0:
1045 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301046 if flt(self.wh_data.qty_after_transaction) <= 0:
1047 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301048 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301049 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1050 actual_qty * sle.incoming_rate
1051 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301052
Nabin Haita77b8c92020-12-21 14:45:50 +05301053 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301054
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301055 elif sle.outgoing_rate:
1056 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301057 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1058 actual_qty * sle.outgoing_rate
1059 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301060
Nabin Haita77b8c92020-12-21 14:45:50 +05301061 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301062 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301063 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301064 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301065 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1066 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301067
Nabin Haita77b8c92020-12-21 14:45:50 +05301068 if not self.wh_data.valuation_rate and actual_qty > 0:
1069 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301070
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301071 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001072 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301073 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301074 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1075 sle.voucher_type, sle.voucher_detail_no
1076 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001077 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301078 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301079
Ankush Menatf089d392022-02-02 12:51:21 +05301080 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301081 incoming_rate = flt(sle.incoming_rate)
1082 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301083 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301084
Ankush Menat494bd9e2022-03-28 18:52:46 +05301085 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1086 self.wh_data.qty_after_transaction + actual_qty
1087 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301088
Ankush Menat97e18a12022-01-15 17:42:25 +05301089 if self.valuation_method == "LIFO":
1090 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1091 else:
1092 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1093
Ankush Menatb534fee2022-02-19 20:58:36 +05301094 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1095
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301096 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301097 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301098 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301099
Ankush Menat4b29fb62021-12-18 18:40:22 +05301100 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301101 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1102 sle.voucher_type, sle.voucher_detail_no
1103 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301104 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301105 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301106 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301107 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301108
Ankush Menat494bd9e2022-03-28 18:52:46 +05301109 stock_queue.remove_stock(
1110 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1111 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301112
Ankush Menatb534fee2022-02-19 20:58:36 +05301113 _qty, stock_value = stock_queue.get_total_stock_and_value()
1114
1115 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301116
Ankush Menat97e18a12022-01-15 17:42:25 +05301117 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301118 self.wh_data.stock_value = round_off_if_near_zero(
1119 self.wh_data.stock_value + stock_value_difference
1120 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301121
Nabin Haita77b8c92020-12-21 14:45:50 +05301122 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301123 self.wh_data.stock_queue.append(
1124 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1125 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301126
Ankush Menatb534fee2022-02-19 20:58:36 +05301127 if self.wh_data.qty_after_transaction:
1128 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1129
Ankush Menatce0514c2022-02-15 11:41:41 +05301130 def update_batched_values(self, sle):
1131 incoming_rate = flt(sle.incoming_rate)
1132 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301133
Ankush Menat494bd9e2022-03-28 18:52:46 +05301134 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1135 self.wh_data.qty_after_transaction + actual_qty
1136 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301137
1138 if actual_qty > 0:
1139 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301140 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301141 outgoing_rate = get_batch_incoming_rate(
1142 item_code=sle.item_code,
1143 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301144 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301145 posting_date=sle.posting_date,
1146 posting_time=sle.posting_time,
1147 creation=sle.creation,
1148 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301149 if outgoing_rate is None:
1150 # This can *only* happen if qty available for the batch is zero.
1151 # in such case fall back various other rates.
1152 # future entries will correct the overall accounting as each
1153 # batch individually uses moving average rates.
1154 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301155 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301156
Ankush Menat494bd9e2022-03-28 18:52:46 +05301157 self.wh_data.stock_value = round_off_if_near_zero(
1158 self.wh_data.stock_value + stock_value_difference
1159 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301160 if self.wh_data.qty_after_transaction:
1161 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301162
Javier Wong9b11d9b2017-04-14 18:24:04 +08001163 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301164 ref_item_dt = ""
1165
1166 if voucher_type == "Stock Entry":
1167 ref_item_dt = voucher_type + " Detail"
1168 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1169 ref_item_dt = voucher_type + " Item"
1170
1171 if ref_item_dt:
1172 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1173 else:
1174 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301175
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301176 def get_fallback_rate(self, sle) -> float:
1177 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301178 This should only get used for negative stock."""
1179 return get_valuation_rate(
1180 sle.item_code,
1181 sle.warehouse,
1182 sle.voucher_type,
1183 sle.voucher_no,
1184 self.allow_zero_rate,
1185 currency=erpnext.get_company_currency(sle.company),
1186 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301187 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301188
Nabin Haita77b8c92020-12-21 14:45:50 +05301189 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301190 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301191 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1192 sle = sle[0] if sle else frappe._dict()
1193 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301194
Nabin Haita77b8c92020-12-21 14:45:50 +05301195 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301196 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301197 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301198
1199 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301200 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301201 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301202 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301203
Ankush Menat494bd9e2022-03-28 18:52:46 +05301204 if (
1205 exceptions[0]["voucher_type"],
1206 exceptions[0]["voucher_no"],
1207 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301208
Nabin Haita77b8c92020-12-21 14:45:50 +05301209 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301210 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301211 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1212 frappe.get_desk_link("Warehouse", warehouse),
1213 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301214 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301215 msg = _(
1216 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1217 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301218 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301219 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1220 frappe.get_desk_link("Warehouse", warehouse),
1221 exceptions[0]["posting_date"],
1222 exceptions[0]["posting_time"],
1223 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1224 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301225
Nabin Haita77b8c92020-12-21 14:45:50 +05301226 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301227 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301228 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301229
1230 if allowed_qty > 0:
1231 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1232 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1233 )
1234 else:
1235 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1236 msg,
1237 )
s-aga-rf0acb202023-04-12 14:13:54 +05301238
Nabin Haita77b8c92020-12-21 14:45:50 +05301239 msg_list.append(msg)
1240
1241 if msg_list:
1242 message = "\n\n".join(msg_list)
1243 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301244 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301245 else:
1246 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301247
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301248 def update_bin_data(self, sle):
1249 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301250 values_to_update = {
1251 "actual_qty": sle.qty_after_transaction,
1252 "stock_value": sle.stock_value,
1253 }
1254
1255 if sle.valuation_rate is not None:
1256 values_to_update["valuation_rate"] = sle.valuation_rate
1257
1258 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301259
Nabin Haita77b8c92020-12-21 14:45:50 +05301260 def update_bin(self):
1261 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301262 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301263 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301264
Ankush Menat494bd9e2022-03-28 18:52:46 +05301265 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301266 if data.valuation_rate is not None:
1267 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301268 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301269
marination8418c4b2021-06-22 21:35:25 +05301270
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301271def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301272 """get stock ledger entries filtered by specific posting datetime conditions"""
1273
Ankush Menat494bd9e2022-03-28 18:52:46 +05301274 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301275 if not args.get("posting_date"):
1276 args["posting_date"] = "1900-01-01"
1277 if not args.get("posting_time"):
1278 args["posting_time"] = "00:00"
1279
1280 voucher_condition = ""
1281 if exclude_current_voucher:
1282 voucher_no = args.get("voucher_no")
1283 voucher_condition = f"and voucher_no != '{voucher_no}'"
1284
Ankush Menat494bd9e2022-03-28 18:52:46 +05301285 sle = frappe.db.sql(
1286 """
marination8418c4b2021-06-22 21:35:25 +05301287 select *, timestamp(posting_date, posting_time) as "timestamp"
1288 from `tabStock Ledger Entry`
1289 where item_code = %(item_code)s
1290 and warehouse = %(warehouse)s
1291 and is_cancelled = 0
1292 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301293 and (
1294 posting_date < %(posting_date)s or
1295 (
1296 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301297 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301298 )
1299 )
marination8418c4b2021-06-22 21:35:25 +05301300 order by timestamp(posting_date, posting_time) desc, creation desc
1301 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301302 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301303 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301304 ),
1305 args,
1306 as_dict=1,
1307 )
marination8418c4b2021-06-22 21:35:25 +05301308
1309 return sle[0] if sle else frappe._dict()
1310
Ankush Menat494bd9e2022-03-28 18:52:46 +05301311
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301312def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301313 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301314 get the last sle on or before the current time-bucket,
1315 to get actual qty before transaction, this function
1316 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301317
Ankush Menat494bd9e2022-03-28 18:52:46 +05301318 args = {
1319 "item_code": "ABC",
1320 "warehouse": "XYZ",
1321 "posting_date": "2012-12-12",
1322 "posting_time": "12:00",
1323 "sle": "name of reference Stock Ledger Entry"
1324 }
Anand Doshi1b531862013-01-10 19:29:51 +05301325 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301326 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301327 sle = get_stock_ledger_entries(
1328 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1329 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301330 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301331
Ankush Menat494bd9e2022-03-28 18:52:46 +05301332
1333def get_stock_ledger_entries(
1334 previous_sle,
1335 operator=None,
1336 order="desc",
1337 limit=None,
1338 for_update=False,
1339 debug=False,
1340 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301341 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301342):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301343 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301344 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1345 operator
1346 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301347 if previous_sle.get("warehouse"):
1348 conditions += " and warehouse = %(warehouse)s"
1349 elif previous_sle.get("warehouse_condition"):
1350 conditions += " and " + previous_sle.get("warehouse_condition")
1351
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301352 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301353 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1354 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301355 conditions += (
1356 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301357 (
1358 serial_no = {0}
1359 or serial_no like {1}
1360 or serial_no like {2}
1361 or serial_no like {3}
1362 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301363 """
1364 ).format(
1365 frappe.db.escape(serial_no),
1366 frappe.db.escape("{}\n%".format(serial_no)),
1367 frappe.db.escape("%\n{}".format(serial_no)),
1368 frappe.db.escape("%\n{}\n%".format(serial_no)),
1369 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301370
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301371 if not previous_sle.get("posting_date"):
1372 previous_sle["posting_date"] = "1900-01-01"
1373 if not previous_sle.get("posting_time"):
1374 previous_sle["posting_time"] = "00:00"
1375
1376 if operator in (">", "<=") and previous_sle.get("name"):
1377 conditions += " and name!=%(name)s"
1378
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301379 if extra_cond:
1380 conditions += f"{extra_cond}"
1381
Ankush Menat494bd9e2022-03-28 18:52:46 +05301382 return frappe.db.sql(
1383 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301384 select *, timestamp(posting_date, posting_time) as "timestamp"
1385 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301386 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301387 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301388 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301389 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301390 %(limit)s %(for_update)s"""
1391 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301392 "conditions": conditions,
1393 "limit": limit or "",
1394 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301395 "order": order,
1396 },
1397 previous_sle,
1398 as_dict=1,
1399 debug=debug,
1400 )
1401
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301402
Nabin Haita77b8c92020-12-21 14:45:50 +05301403def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301404 return frappe.db.get_value(
1405 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301406 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301407 [
1408 "item_code",
1409 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301410 "actual_qty",
1411 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301412 "posting_date",
1413 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301414 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301415 "timestamp(posting_date, posting_time) as timestamp",
1416 ],
1417 as_dict=1,
1418 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301419
Ankush Menatce0514c2022-02-15 11:41:41 +05301420
Ankush Menat494bd9e2022-03-28 18:52:46 +05301421def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301422 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301423):
1424
Ankush Menat102fff22022-02-19 15:51:04 +05301425 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301426 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301427
Ankush Menate1c16872022-04-21 20:01:48 +05301428 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301429 posting_date, posting_time
1430 )
Ankush Menat102fff22022-02-19 15:51:04 +05301431 if creation:
1432 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301433 CombineDatetime(sle.posting_date, sle.posting_time)
1434 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301435 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301436
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301437 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301438 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301439 )
1440
Ankush Menat102fff22022-02-19 15:51:04 +05301441 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301442 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301443 .inner_join(batch_ledger)
1444 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1445 .select(
1446 Sum(
1447 Case()
1448 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1449 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1450 ).as_("batch_value"),
1451 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1452 "batch_qty"
1453 ),
1454 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301455 .where(
1456 (sle.item_code == item_code)
1457 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301458 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301459 & (sle.is_cancelled == 0)
1460 )
1461 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301462 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301463
1464 if batch_details and batch_details[0].batch_qty:
1465 return batch_details[0].batch_value / batch_details[0].batch_qty
1466
1467
Ankush Menat494bd9e2022-03-28 18:52:46 +05301468def get_valuation_rate(
1469 item_code,
1470 warehouse,
1471 voucher_type,
1472 voucher_no,
1473 allow_zero_rate=False,
1474 currency=None,
1475 company=None,
1476 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301477 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301478 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301479):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301480
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301481 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1482
Ankush Menatf7ffe042021-11-01 13:21:14 +05301483 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301484 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301485
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301486 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1487 table = frappe.qb.DocType("Stock Ledger Entry")
1488 query = (
1489 frappe.qb.from_(table)
1490 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1491 .where(
1492 (table.item_code == item_code)
1493 & (table.warehouse == warehouse)
1494 & (table.batch_no == batch_no)
1495 & (table.is_cancelled == 0)
1496 & (table.voucher_no != voucher_no)
1497 & (table.voucher_type != voucher_type)
1498 )
1499 )
1500
1501 last_valuation_rate = query.run()
1502 if last_valuation_rate:
1503 return flt(last_valuation_rate[0][0])
1504
Ankush Menat342d09a2022-02-19 14:28:51 +05301505 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301506 if warehouse and serial_and_batch_bundle:
1507 batch_obj = BatchNoValuation(
1508 sle=frappe._dict(
1509 {
1510 "item_code": item_code,
1511 "warehouse": warehouse,
1512 "actual_qty": -1,
1513 "serial_and_batch_bundle": serial_and_batch_bundle,
1514 }
1515 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301516 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301517
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301518 return batch_obj.get_incoming_rate()
1519
Ankush Menatf7ffe042021-11-01 13:21:14 +05301520 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301521 if last_valuation_rate := frappe.db.sql(
1522 """select valuation_rate
1523 from `tabStock Ledger Entry` force index (item_warehouse)
1524 where
1525 item_code = %s
1526 AND warehouse = %s
1527 AND valuation_rate >= 0
1528 AND is_cancelled = 0
1529 AND NOT (voucher_no = %s AND voucher_type = %s)
1530 order by posting_date desc, posting_time desc, name desc limit 1""",
1531 (item_code, warehouse, voucher_no, voucher_type),
1532 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301533 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301534
1535 # If negative stock allowed, and item delivered without any incoming entry,
1536 # system does not found any SLE, then take valuation rate from Item
1537 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301538
1539 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301540 # try Item Standard rate
1541 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301542
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301543 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301544 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301545 valuation_rate = frappe.db.get_value(
1546 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1547 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301548
Ankush Menat494bd9e2022-03-28 18:52:46 +05301549 if (
1550 not allow_zero_rate
1551 and not valuation_rate
1552 and raise_error_if_no_rate
1553 and cint(erpnext.is_perpetual_inventory_enabled(company))
1554 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301555 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301556
Ankush Menat494bd9e2022-03-28 18:52:46 +05301557 message = _(
1558 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1559 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301560 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301561 solutions = (
1562 "<li>"
1563 + _(
1564 "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."
1565 ).format(voucher_type)
1566 + "</li>"
1567 )
1568 solutions += (
1569 "<li>"
1570 + _("If not, you can Cancel / Submit this entry")
1571 + " {0} ".format(frappe.bold("after"))
1572 + _("performing either one below:")
1573 + "</li>"
1574 )
Marica97715f22020-05-11 20:45:37 +05301575 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1576 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1577 msg = message + solutions + sub_solutions + "</li>"
1578
1579 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301580
1581 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301582
Ankush Menat494bd9e2022-03-28 18:52:46 +05301583
Ankush Menate7109c12021-08-26 16:40:45 +05301584def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301585 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301586 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301587 qty_shift = args.actual_qty
1588
Ankush Menat7c839c42022-05-06 12:09:08 +05301589 args["time_format"] = "%H:%i:%s"
1590
marination8418c4b2021-06-22 21:35:25 +05301591 # find difference/shift in qty caused by stock reconciliation
1592 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301593 qty_shift = get_stock_reco_qty_shift(args)
1594
1595 # find the next nearest stock reco so that we only recalculate SLEs till that point
1596 next_stock_reco_detail = get_next_stock_reco(args)
1597 if next_stock_reco_detail:
1598 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301599
marination40389772021-07-02 17:13:45 +05301600 # add condition to update SLEs before this date & time
1601 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301602
Ankush Menat494bd9e2022-03-28 18:52:46 +05301603 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301604 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301605 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301606 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301607 where
1608 item_code = %(item_code)s
1609 and warehouse = %(warehouse)s
1610 and voucher_no != %(voucher_no)s
1611 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301612 and (
1613 posting_date > %(posting_date)s or
1614 (
1615 posting_date = %(posting_date)s and
1616 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1617 )
1618 )
marination40389772021-07-02 17:13:45 +05301619 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301620 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301621 args,
1622 )
Nabin Hait186a0452021-02-18 14:14:21 +05301623
1624 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1625
Ankush Menat494bd9e2022-03-28 18:52:46 +05301626
marination40389772021-07-02 17:13:45 +05301627def get_stock_reco_qty_shift(args):
1628 stock_reco_qty_shift = 0
1629 if args.get("is_cancelled"):
1630 if args.get("previous_qty_after_transaction"):
1631 # get qty (balance) that was set at submission
1632 last_balance = args.get("previous_qty_after_transaction")
1633 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1634 else:
1635 stock_reco_qty_shift = flt(args.actual_qty)
1636 else:
1637 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301638 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301639 "qty_after_transaction"
1640 )
marination40389772021-07-02 17:13:45 +05301641
1642 if last_balance is not None:
1643 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1644 else:
1645 stock_reco_qty_shift = args.qty_after_transaction
1646
1647 return stock_reco_qty_shift
1648
Ankush Menat494bd9e2022-03-28 18:52:46 +05301649
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301650def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301651 """Returns next nearest stock reconciliaton's details."""
1652
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301653 sle = frappe.qb.DocType("Stock Ledger Entry")
1654
1655 query = (
1656 frappe.qb.from_(sle)
1657 .select(
1658 sle.name,
1659 sle.posting_date,
1660 sle.posting_time,
1661 sle.creation,
1662 sle.voucher_no,
1663 sle.item_code,
1664 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301665 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301666 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301667 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301668 )
1669 .where(
1670 (sle.item_code == kwargs.get("item_code"))
1671 & (sle.warehouse == kwargs.get("warehouse"))
1672 & (sle.voucher_type == "Stock Reconciliation")
1673 & (sle.voucher_no != kwargs.get("voucher_no"))
1674 & (sle.is_cancelled == 0)
1675 & (
1676 (
1677 CombineDatetime(sle.posting_date, sle.posting_time)
1678 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301679 )
1680 | (
1681 (
1682 CombineDatetime(sle.posting_date, sle.posting_time)
1683 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301684 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301685 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301686 )
1687 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301688 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301689 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1690 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301691 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301692 )
1693
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301694 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301695 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301696
1697 return query.run(as_dict=True)
1698
marination40389772021-07-02 17:13:45 +05301699
1700def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301701 return f"""
1702 and
1703 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1704 or (
1705 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1706 and creation < '{detail.creation}'
1707 )
1708 )"""
1709
Ankush Menat494bd9e2022-03-28 18:52:46 +05301710
Ankush Menate7109c12021-08-26 16:40:45 +05301711def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301712 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301713 return
1714 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1715 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301716
Ankush Menat5eba5752021-12-07 23:03:52 +05301717 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301718
1719 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301720 message = _(
1721 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1722 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301723 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301724 frappe.get_desk_link("Item", args.item_code),
1725 frappe.get_desk_link("Warehouse", args.warehouse),
1726 neg_sle[0]["posting_date"],
1727 neg_sle[0]["posting_time"],
1728 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1729 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301730
Ankush Menat494bd9e2022-03-28 18:52:46 +05301731 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301732
s-aga-rd9e28432023-10-27 16:35:35 +05301733 if args.batch_no:
1734 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1735 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1736 message = _(
1737 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1738 ).format(
1739 abs(neg_batch_sle[0]["cumulative_total"]),
1740 frappe.get_desk_link("Batch", args.batch_no),
1741 frappe.get_desk_link("Warehouse", args.warehouse),
1742 neg_batch_sle[0]["posting_date"],
1743 neg_batch_sle[0]["posting_time"],
1744 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1745 )
1746 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301747
s-aga-r73b65ac2023-11-01 18:35:07 +05301748 if args.reserved_stock:
1749 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301750
Nabin Haita77b8c92020-12-21 14:45:50 +05301751
Maricad6078aa2022-06-17 15:13:13 +05301752def is_negative_with_precision(neg_sle, is_batch=False):
1753 """
1754 Returns whether system precision rounded qty is insufficient.
1755 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1756 """
1757
1758 if not neg_sle:
1759 return False
1760
1761 field = "cumulative_total" if is_batch else "qty_after_transaction"
1762 precision = cint(frappe.db.get_default("float_precision")) or 2
1763 qty_deficit = flt(neg_sle[0][field], precision)
1764
1765 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1766
1767
Nabin Haita77b8c92020-12-21 14:45:50 +05301768def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301769 return frappe.db.sql(
1770 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301771 select
1772 qty_after_transaction, posting_date, posting_time,
1773 voucher_type, voucher_no
1774 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301775 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301776 item_code = %(item_code)s
1777 and warehouse = %(warehouse)s
1778 and voucher_no != %(voucher_no)s
1779 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1780 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301781 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301782 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301783 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301784 """,
1785 args,
1786 as_dict=1,
1787 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301788
Ankush Menat5eba5752021-12-07 23:03:52 +05301789
1790def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301791 return frappe.db.sql(
1792 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301793 with batch_ledger as (
1794 select
1795 posting_date, posting_time, voucher_type, voucher_no,
1796 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1797 from `tabStock Ledger Entry`
1798 where
1799 item_code = %(item_code)s
1800 and warehouse = %(warehouse)s
1801 and batch_no=%(batch_no)s
1802 and is_cancelled = 0
1803 order by posting_date, posting_time, creation
1804 )
1805 select * from batch_ledger
1806 where
1807 cumulative_total < 0.0
1808 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1809 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301810 """,
1811 args,
1812 as_dict=1,
1813 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301814
1815
s-aga-rd9e28432023-10-27 16:35:35 +05301816def validate_reserved_stock(kwargs):
1817 if kwargs.serial_no:
1818 serial_nos = kwargs.serial_no.split("\n")
1819 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1820
s-aga-re1a87a82023-10-31 18:41:58 +05301821 elif kwargs.batch_no:
1822 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1823
s-aga-rd9e28432023-10-27 16:35:35 +05301824 elif kwargs.serial_and_batch_bundle:
1825 sbb_entries = frappe.db.get_all(
1826 "Serial and Batch Entry",
1827 {
1828 "parenttype": "Serial and Batch Bundle",
1829 "parent": kwargs.serial_and_batch_bundle,
1830 "docstatus": 1,
1831 },
s-aga-re1a87a82023-10-31 18:41:58 +05301832 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301833 )
s-aga-rd9e28432023-10-27 16:35:35 +05301834
s-aga-re1a87a82023-10-31 18:41:58 +05301835 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301836 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301837 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1838 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301839
s-aga-r92317062023-11-02 10:36:00 +05301840 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1841 precision = cint(frappe.db.get_default("float_precision")) or 2
1842 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301843
s-aga-r92317062023-11-02 10:36:00 +05301844 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1845 if diff < 0 and abs(diff) > 0.0001:
1846 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1847 abs(diff),
1848 frappe.get_desk_link("Item", kwargs.item_code),
1849 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1850 nowdate(),
1851 nowtime(),
1852 )
1853 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301854
s-aga-rd9e28432023-10-27 16:35:35 +05301855
1856def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1857 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1858 item_code, warehouse, serial_nos
1859 ):
1860 if common_serial_nos := list(
1861 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1862 ):
1863 msg = _(
1864 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1865 )
1866 msg += "<br />"
1867 msg += _("Example: Serial No {0} reserved in {1}.").format(
1868 frappe.bold(common_serial_nos[0]),
1869 frappe.get_desk_link(
1870 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1871 ),
1872 )
1873 frappe.throw(msg, title=_("Reserved Serial No."))
1874
1875
s-aga-re1a87a82023-10-31 18:41:58 +05301876def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1877 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1878 available_batches = get_available_batches(
1879 frappe._dict(
1880 {
1881 "item_code": item_code,
1882 "warehouse": warehouse,
1883 "posting_date": nowdate(),
1884 "posting_time": nowtime(),
1885 }
1886 )
1887 )
1888 available_batches_map = {row.batch_no: row.qty for row in available_batches}
1889 precision = cint(frappe.db.get_default("float_precision")) or 2
1890
1891 for batch_no in batch_nos:
1892 diff = flt(
1893 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
1894 )
1895 if diff < 0 and abs(diff) > 0.0001:
1896 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1897 abs(diff),
1898 frappe.get_desk_link("Batch", batch_no),
1899 frappe.get_desk_link("Warehouse", warehouse),
1900 nowdate(),
1901 nowtime(),
1902 )
1903 frappe.throw(msg, title=_("Reserved Stock for Batch"))
1904
1905
Ankush Menateb8b4242022-02-12 13:08:28 +05301906def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1907 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1908 return True
1909 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1910 return True
1911 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301912
1913
1914def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1915 """
1916 For inter company transfer, incoming rate is the average of the outgoing rate
1917 """
1918 rate = 0.0
1919
1920 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1921
1922 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1923
1924 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1925
1926 if reference_name:
1927 rate = frappe.get_cached_value(
1928 doctype,
1929 reference_name,
1930 "incoming_rate",
1931 )
1932
1933 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301934
1935
1936def is_internal_transfer(sle):
1937 data = frappe.get_cached_value(
1938 sle.voucher_type,
1939 sle.voucher_no,
1940 ["is_internal_supplier", "represents_company", "company"],
1941 as_dict=True,
1942 )
1943
1944 if data.is_internal_supplier and data.represents_company == data.company:
1945 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05301946
1947
1948def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
1949 table = frappe.qb.DocType("Stock Ledger Entry")
1950
1951 query = (
1952 frappe.qb.from_(table)
1953 .select(Sum(table.stock_value_difference).as_("value"))
1954 .where(
1955 (table.is_cancelled == 0)
1956 & (table.item_code == item_code)
1957 & (table.warehouse == warehouse)
1958 & (
1959 (table.posting_date < posting_date)
1960 | ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
1961 )
1962 )
1963 )
1964
1965 if voucher_no:
1966 query = query.where(table.voucher_no != voucher_no)
1967
1968 difference_amount = query.run()
1969 return flt(difference_amount[0][0]) if difference_amount else 0