blob: e9381d42b92b507075bee15c4e64b85211675715 [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)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530762 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530763 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530764
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530765 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530766
Ankush Menat701878f2022-03-01 18:08:29 +0530767 if not self.args.get("sle_id"):
768 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530769
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530770 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530771 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
772 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
773
774 if sle.actual_qty < 0:
775 sle.actual_qty = (
776 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
777 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530778 )
779
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530780 if abs(sle.actual_qty) == 0.0:
781 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530782
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530783 def calculate_valuation_for_serial_batch_bundle(self, sle):
784 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
785
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530786 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530787 doc.calculate_qty_and_amount(save=True)
788
789 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
790
791 self.wh_data.qty_after_transaction += doc.total_qty
792 if self.wh_data.qty_after_transaction:
793 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
794
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530795 def validate_negative_stock(self, sle):
796 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530797 validate negative stock for entries current datetime onwards
798 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530799 """
s-aga-rf0acb202023-04-12 14:13:54 +0530800 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530801 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530802
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530803 if diff < 0 and abs(diff) > 0.0001:
804 # negative stock!
805 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530806 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530807 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530808 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530809 return True
810
Nabin Haita77b8c92020-12-21 14:45:50 +0530811 def get_dynamic_incoming_outgoing_rate(self, sle):
812 # Get updated incoming/outgoing rate from transaction
813 if sle.recalculate_rate:
814 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
815
816 if flt(sle.actual_qty) >= 0:
817 sle.incoming_rate = rate
818 else:
819 sle.outgoing_rate = rate
820
821 def get_incoming_outgoing_rate_from_transaction(self, sle):
822 rate = 0
823 # Material Transfer, Repack, Manufacturing
824 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530825 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530826 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
827 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530828 elif sle.voucher_type in (
829 "Purchase Receipt",
830 "Purchase Invoice",
831 "Delivery Note",
832 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530833 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530834 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530835 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530836 from erpnext.controllers.sales_and_purchase_return import (
837 get_rate_for_return, # don't move this import to top
838 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530839
840 rate = get_rate_for_return(
841 sle.voucher_type,
842 sle.voucher_no,
843 sle.item_code,
844 voucher_detail_no=sle.voucher_detail_no,
845 sle=sle,
846 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530847
848 elif (
849 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530850 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530851 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530852 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530853 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530854 else:
855 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530856 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530857 elif sle.voucher_type == "Subcontracting Receipt":
858 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530859 else:
860 rate_field = "incoming_rate"
861
862 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530863 item_code, incoming_rate = frappe.db.get_value(
864 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
865 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530866
867 if item_code == sle.item_code:
868 rate = incoming_rate
869 else:
870 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
871 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530872 elif sle == "Subcontracting Receipt":
873 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530874 else:
875 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530876
Ankush Menat494bd9e2022-03-28 18:52:46 +0530877 rate = frappe.db.get_value(
878 ref_doctype,
879 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
880 rate_field,
881 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530882
883 return rate
884
885 def update_outgoing_rate_on_transaction(self, sle):
886 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530887 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
888 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530889 """
890 if sle.actual_qty and sle.voucher_detail_no:
891 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
892
893 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
894 self.update_rate_on_stock_entry(sle, outgoing_rate)
895 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
896 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
897 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
898 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530899 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
900 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530901 elif sle.voucher_type == "Stock Reconciliation":
902 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530903
904 def update_rate_on_stock_entry(self, sle, outgoing_rate):
905 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
906
Ankush Menat701878f2022-03-01 18:08:29 +0530907 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
908 if not sle.dependant_sle_voucher_detail_no:
909 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530910
911 def recalculate_amounts_in_stock_entry(self, voucher_no):
912 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530913 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
914 stock_entry.db_update()
915 for d in stock_entry.items:
916 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530917
Nabin Haita77b8c92020-12-21 14:45:50 +0530918 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
919 # Update item's incoming rate on transaction
920 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
921 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530922 frappe.db.set_value(
923 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
924 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530925 else:
926 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530927 frappe.db.set_value(
928 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530929 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530930 "incoming_rate",
931 outgoing_rate,
932 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530933
934 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
935 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530936 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
937 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
938 ):
939 frappe.db.set_value(
940 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
941 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530942 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530943 frappe.db.set_value(
944 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
945 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530946
947 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530948 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530949 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530950 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530951 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530952 d.db_update()
953
Sagar Sharma323bdf82022-05-17 15:14:07 +0530954 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530955 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
956 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530957 else:
958 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530959 "Subcontracting Receipt Supplied Item",
960 sle.voucher_detail_no,
961 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530962 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530963
s-aga-ra6cb6c62023-05-03 09:51:58 +0530964 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530965 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530966 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530967 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530968 d.db_update()
969
s-aga-r88a3f652023-05-30 16:54:28 +0530970 def update_rate_on_stock_reconciliation(self, sle):
971 if not sle.serial_no and not sle.batch_no:
972 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
973
974 for item in sr.items:
975 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +0530976 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +0530977 continue
978
979 previous_sle = get_previous_sle(
980 {
981 "item_code": item.item_code,
982 "warehouse": item.warehouse,
983 "posting_date": sr.posting_date,
984 "posting_time": sr.posting_time,
985 "sle": sle.name,
986 }
987 )
988
989 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
990 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
991 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
992
993 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +0530994 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +0530995 item.amount_difference = item.amount - item.current_amount
996 else:
997 sr.difference_amount = sum([item.amount_difference for item in sr.items])
998 sr.db_update()
999
1000 for item in sr.items:
1001 item.db_update()
1002
Nabin Hait328c4f92020-01-02 19:00:32 +05301003 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1004 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301005 all_serial_nos = frappe.get_all(
1006 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1007 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301008
Ankush Menat494bd9e2022-03-28 18:52:46 +05301009 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 +05301010
1011 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301012 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301013 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301014 incoming_rate = frappe.db.sql(
1015 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301016 select incoming_rate
1017 from `tabStock Ledger Entry`
1018 where
1019 company = %s
1020 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301021 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301022 and (serial_no = %s
1023 or serial_no like %s
1024 or serial_no like %s
1025 or serial_no like %s
1026 )
1027 order by posting_date desc
1028 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301029 """,
1030 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1031 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301032
1033 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1034
1035 return incoming_values
1036
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301037 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301038 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301039 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301040 if new_stock_qty >= 0:
1041 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301042 if flt(self.wh_data.qty_after_transaction) <= 0:
1043 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301044 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301045 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1046 actual_qty * sle.incoming_rate
1047 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301048
Nabin Haita77b8c92020-12-21 14:45:50 +05301049 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301050
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301051 elif sle.outgoing_rate:
1052 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301053 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1054 actual_qty * sle.outgoing_rate
1055 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301056
Nabin Haita77b8c92020-12-21 14:45:50 +05301057 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301058 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301059 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301060 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301061 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1062 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301063
Nabin Haita77b8c92020-12-21 14:45:50 +05301064 if not self.wh_data.valuation_rate and actual_qty > 0:
1065 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301066
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301067 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001068 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301069 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301070 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1071 sle.voucher_type, sle.voucher_detail_no
1072 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001073 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301074 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301075
Ankush Menatf089d392022-02-02 12:51:21 +05301076 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301077 incoming_rate = flt(sle.incoming_rate)
1078 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301079 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301080
Ankush Menat494bd9e2022-03-28 18:52:46 +05301081 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1082 self.wh_data.qty_after_transaction + actual_qty
1083 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301084
Ankush Menat97e18a12022-01-15 17:42:25 +05301085 if self.valuation_method == "LIFO":
1086 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1087 else:
1088 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1089
Ankush Menatb534fee2022-02-19 20:58:36 +05301090 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1091
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301092 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301093 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301094 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301095
Ankush Menat4b29fb62021-12-18 18:40:22 +05301096 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301097 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1098 sle.voucher_type, sle.voucher_detail_no
1099 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301100 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301101 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301102 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301103 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301104
Ankush Menat494bd9e2022-03-28 18:52:46 +05301105 stock_queue.remove_stock(
1106 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1107 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301108
Ankush Menatb534fee2022-02-19 20:58:36 +05301109 _qty, stock_value = stock_queue.get_total_stock_and_value()
1110
1111 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301112
Ankush Menat97e18a12022-01-15 17:42:25 +05301113 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301114 self.wh_data.stock_value = round_off_if_near_zero(
1115 self.wh_data.stock_value + stock_value_difference
1116 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301117
Nabin Haita77b8c92020-12-21 14:45:50 +05301118 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301119 self.wh_data.stock_queue.append(
1120 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1121 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301122
Ankush Menatb534fee2022-02-19 20:58:36 +05301123 if self.wh_data.qty_after_transaction:
1124 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1125
Ankush Menatce0514c2022-02-15 11:41:41 +05301126 def update_batched_values(self, sle):
1127 incoming_rate = flt(sle.incoming_rate)
1128 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301129
Ankush Menat494bd9e2022-03-28 18:52:46 +05301130 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1131 self.wh_data.qty_after_transaction + actual_qty
1132 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301133
1134 if actual_qty > 0:
1135 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301136 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301137 outgoing_rate = get_batch_incoming_rate(
1138 item_code=sle.item_code,
1139 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301140 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301141 posting_date=sle.posting_date,
1142 posting_time=sle.posting_time,
1143 creation=sle.creation,
1144 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301145 if outgoing_rate is None:
1146 # This can *only* happen if qty available for the batch is zero.
1147 # in such case fall back various other rates.
1148 # future entries will correct the overall accounting as each
1149 # batch individually uses moving average rates.
1150 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301151 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301152
Ankush Menat494bd9e2022-03-28 18:52:46 +05301153 self.wh_data.stock_value = round_off_if_near_zero(
1154 self.wh_data.stock_value + stock_value_difference
1155 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301156 if self.wh_data.qty_after_transaction:
1157 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301158
Javier Wong9b11d9b2017-04-14 18:24:04 +08001159 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301160 ref_item_dt = ""
1161
1162 if voucher_type == "Stock Entry":
1163 ref_item_dt = voucher_type + " Detail"
1164 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1165 ref_item_dt = voucher_type + " Item"
1166
1167 if ref_item_dt:
1168 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1169 else:
1170 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301171
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301172 def get_fallback_rate(self, sle) -> float:
1173 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301174 This should only get used for negative stock."""
1175 return get_valuation_rate(
1176 sle.item_code,
1177 sle.warehouse,
1178 sle.voucher_type,
1179 sle.voucher_no,
1180 self.allow_zero_rate,
1181 currency=erpnext.get_company_currency(sle.company),
1182 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301183 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301184
Nabin Haita77b8c92020-12-21 14:45:50 +05301185 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301186 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301187 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1188 sle = sle[0] if sle else frappe._dict()
1189 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301190
Nabin Haita77b8c92020-12-21 14:45:50 +05301191 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301192 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301193 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301194
1195 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301196 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301197 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301198 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301199
Ankush Menat494bd9e2022-03-28 18:52:46 +05301200 if (
1201 exceptions[0]["voucher_type"],
1202 exceptions[0]["voucher_no"],
1203 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301204
Nabin Haita77b8c92020-12-21 14:45:50 +05301205 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301206 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301207 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1208 frappe.get_desk_link("Warehouse", warehouse),
1209 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301210 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301211 msg = _(
1212 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1213 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301214 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301215 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1216 frappe.get_desk_link("Warehouse", warehouse),
1217 exceptions[0]["posting_date"],
1218 exceptions[0]["posting_time"],
1219 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1220 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301221
Nabin Haita77b8c92020-12-21 14:45:50 +05301222 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301223 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301224 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301225
1226 if allowed_qty > 0:
1227 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1228 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1229 )
1230 else:
1231 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1232 msg,
1233 )
s-aga-rf0acb202023-04-12 14:13:54 +05301234
Nabin Haita77b8c92020-12-21 14:45:50 +05301235 msg_list.append(msg)
1236
1237 if msg_list:
1238 message = "\n\n".join(msg_list)
1239 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301240 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301241 else:
1242 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301243
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301244 def update_bin_data(self, sle):
1245 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301246 values_to_update = {
1247 "actual_qty": sle.qty_after_transaction,
1248 "stock_value": sle.stock_value,
1249 }
1250
1251 if sle.valuation_rate is not None:
1252 values_to_update["valuation_rate"] = sle.valuation_rate
1253
1254 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301255
Nabin Haita77b8c92020-12-21 14:45:50 +05301256 def update_bin(self):
1257 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301258 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301259 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301260
Ankush Menat494bd9e2022-03-28 18:52:46 +05301261 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301262 if data.valuation_rate is not None:
1263 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301264 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301265
marination8418c4b2021-06-22 21:35:25 +05301266
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301267def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301268 """get stock ledger entries filtered by specific posting datetime conditions"""
1269
Ankush Menat494bd9e2022-03-28 18:52:46 +05301270 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301271 if not args.get("posting_date"):
1272 args["posting_date"] = "1900-01-01"
1273 if not args.get("posting_time"):
1274 args["posting_time"] = "00:00"
1275
1276 voucher_condition = ""
1277 if exclude_current_voucher:
1278 voucher_no = args.get("voucher_no")
1279 voucher_condition = f"and voucher_no != '{voucher_no}'"
1280
Ankush Menat494bd9e2022-03-28 18:52:46 +05301281 sle = frappe.db.sql(
1282 """
marination8418c4b2021-06-22 21:35:25 +05301283 select *, timestamp(posting_date, posting_time) as "timestamp"
1284 from `tabStock Ledger Entry`
1285 where item_code = %(item_code)s
1286 and warehouse = %(warehouse)s
1287 and is_cancelled = 0
1288 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301289 and (
1290 posting_date < %(posting_date)s or
1291 (
1292 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301293 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301294 )
1295 )
marination8418c4b2021-06-22 21:35:25 +05301296 order by timestamp(posting_date, posting_time) desc, creation desc
1297 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301298 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301299 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301300 ),
1301 args,
1302 as_dict=1,
1303 )
marination8418c4b2021-06-22 21:35:25 +05301304
1305 return sle[0] if sle else frappe._dict()
1306
Ankush Menat494bd9e2022-03-28 18:52:46 +05301307
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301308def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301309 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301310 get the last sle on or before the current time-bucket,
1311 to get actual qty before transaction, this function
1312 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301313
Ankush Menat494bd9e2022-03-28 18:52:46 +05301314 args = {
1315 "item_code": "ABC",
1316 "warehouse": "XYZ",
1317 "posting_date": "2012-12-12",
1318 "posting_time": "12:00",
1319 "sle": "name of reference Stock Ledger Entry"
1320 }
Anand Doshi1b531862013-01-10 19:29:51 +05301321 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301322 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301323 sle = get_stock_ledger_entries(
1324 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1325 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301326 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301327
Ankush Menat494bd9e2022-03-28 18:52:46 +05301328
1329def get_stock_ledger_entries(
1330 previous_sle,
1331 operator=None,
1332 order="desc",
1333 limit=None,
1334 for_update=False,
1335 debug=False,
1336 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301337 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301338):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301339 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301340 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1341 operator
1342 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301343 if previous_sle.get("warehouse"):
1344 conditions += " and warehouse = %(warehouse)s"
1345 elif previous_sle.get("warehouse_condition"):
1346 conditions += " and " + previous_sle.get("warehouse_condition")
1347
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301348 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301349 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1350 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301351 conditions += (
1352 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301353 (
1354 serial_no = {0}
1355 or serial_no like {1}
1356 or serial_no like {2}
1357 or serial_no like {3}
1358 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301359 """
1360 ).format(
1361 frappe.db.escape(serial_no),
1362 frappe.db.escape("{}\n%".format(serial_no)),
1363 frappe.db.escape("%\n{}".format(serial_no)),
1364 frappe.db.escape("%\n{}\n%".format(serial_no)),
1365 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301366
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301367 if not previous_sle.get("posting_date"):
1368 previous_sle["posting_date"] = "1900-01-01"
1369 if not previous_sle.get("posting_time"):
1370 previous_sle["posting_time"] = "00:00"
1371
1372 if operator in (">", "<=") and previous_sle.get("name"):
1373 conditions += " and name!=%(name)s"
1374
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301375 if extra_cond:
1376 conditions += f"{extra_cond}"
1377
Ankush Menat494bd9e2022-03-28 18:52:46 +05301378 return frappe.db.sql(
1379 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301380 select *, timestamp(posting_date, posting_time) as "timestamp"
1381 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301382 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301383 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301384 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301385 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301386 %(limit)s %(for_update)s"""
1387 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301388 "conditions": conditions,
1389 "limit": limit or "",
1390 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301391 "order": order,
1392 },
1393 previous_sle,
1394 as_dict=1,
1395 debug=debug,
1396 )
1397
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301398
Nabin Haita77b8c92020-12-21 14:45:50 +05301399def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301400 return frappe.db.get_value(
1401 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301402 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301403 [
1404 "item_code",
1405 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301406 "actual_qty",
1407 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301408 "posting_date",
1409 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301410 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301411 "timestamp(posting_date, posting_time) as timestamp",
1412 ],
1413 as_dict=1,
1414 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301415
Ankush Menatce0514c2022-02-15 11:41:41 +05301416
Ankush Menat494bd9e2022-03-28 18:52:46 +05301417def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301418 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301419):
1420
Ankush Menat102fff22022-02-19 15:51:04 +05301421 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301422 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301423
Ankush Menate1c16872022-04-21 20:01:48 +05301424 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301425 posting_date, posting_time
1426 )
Ankush Menat102fff22022-02-19 15:51:04 +05301427 if creation:
1428 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301429 CombineDatetime(sle.posting_date, sle.posting_time)
1430 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301431 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301432
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301433 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301434 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301435 )
1436
Ankush Menat102fff22022-02-19 15:51:04 +05301437 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301438 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301439 .inner_join(batch_ledger)
1440 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1441 .select(
1442 Sum(
1443 Case()
1444 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1445 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1446 ).as_("batch_value"),
1447 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1448 "batch_qty"
1449 ),
1450 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301451 .where(
1452 (sle.item_code == item_code)
1453 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301454 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301455 & (sle.is_cancelled == 0)
1456 )
1457 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301458 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301459
1460 if batch_details and batch_details[0].batch_qty:
1461 return batch_details[0].batch_value / batch_details[0].batch_qty
1462
1463
Ankush Menat494bd9e2022-03-28 18:52:46 +05301464def get_valuation_rate(
1465 item_code,
1466 warehouse,
1467 voucher_type,
1468 voucher_no,
1469 allow_zero_rate=False,
1470 currency=None,
1471 company=None,
1472 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301473 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301474 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301475):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301476
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301477 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1478
Ankush Menatf7ffe042021-11-01 13:21:14 +05301479 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301480 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301481
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301482 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1483 table = frappe.qb.DocType("Stock Ledger Entry")
1484 query = (
1485 frappe.qb.from_(table)
1486 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1487 .where(
1488 (table.item_code == item_code)
1489 & (table.warehouse == warehouse)
1490 & (table.batch_no == batch_no)
1491 & (table.is_cancelled == 0)
1492 & (table.voucher_no != voucher_no)
1493 & (table.voucher_type != voucher_type)
1494 )
1495 )
1496
1497 last_valuation_rate = query.run()
1498 if last_valuation_rate:
1499 return flt(last_valuation_rate[0][0])
1500
Ankush Menat342d09a2022-02-19 14:28:51 +05301501 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301502 if warehouse and serial_and_batch_bundle:
1503 batch_obj = BatchNoValuation(
1504 sle=frappe._dict(
1505 {
1506 "item_code": item_code,
1507 "warehouse": warehouse,
1508 "actual_qty": -1,
1509 "serial_and_batch_bundle": serial_and_batch_bundle,
1510 }
1511 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301512 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301513
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301514 return batch_obj.get_incoming_rate()
1515
Ankush Menatf7ffe042021-11-01 13:21:14 +05301516 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301517 if last_valuation_rate := frappe.db.sql(
1518 """select valuation_rate
1519 from `tabStock Ledger Entry` force index (item_warehouse)
1520 where
1521 item_code = %s
1522 AND warehouse = %s
1523 AND valuation_rate >= 0
1524 AND is_cancelled = 0
1525 AND NOT (voucher_no = %s AND voucher_type = %s)
1526 order by posting_date desc, posting_time desc, name desc limit 1""",
1527 (item_code, warehouse, voucher_no, voucher_type),
1528 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301529 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301530
1531 # If negative stock allowed, and item delivered without any incoming entry,
1532 # system does not found any SLE, then take valuation rate from Item
1533 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301534
1535 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301536 # try Item Standard rate
1537 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301538
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301539 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301540 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301541 valuation_rate = frappe.db.get_value(
1542 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1543 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301544
Ankush Menat494bd9e2022-03-28 18:52:46 +05301545 if (
1546 not allow_zero_rate
1547 and not valuation_rate
1548 and raise_error_if_no_rate
1549 and cint(erpnext.is_perpetual_inventory_enabled(company))
1550 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301551 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301552
Ankush Menat494bd9e2022-03-28 18:52:46 +05301553 message = _(
1554 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1555 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301556 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301557 solutions = (
1558 "<li>"
1559 + _(
1560 "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."
1561 ).format(voucher_type)
1562 + "</li>"
1563 )
1564 solutions += (
1565 "<li>"
1566 + _("If not, you can Cancel / Submit this entry")
1567 + " {0} ".format(frappe.bold("after"))
1568 + _("performing either one below:")
1569 + "</li>"
1570 )
Marica97715f22020-05-11 20:45:37 +05301571 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1572 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1573 msg = message + solutions + sub_solutions + "</li>"
1574
1575 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301576
1577 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301578
Ankush Menat494bd9e2022-03-28 18:52:46 +05301579
Ankush Menate7109c12021-08-26 16:40:45 +05301580def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301581 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301582 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301583 qty_shift = args.actual_qty
1584
Ankush Menat7c839c42022-05-06 12:09:08 +05301585 args["time_format"] = "%H:%i:%s"
1586
marination8418c4b2021-06-22 21:35:25 +05301587 # find difference/shift in qty caused by stock reconciliation
1588 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301589 qty_shift = get_stock_reco_qty_shift(args)
1590
1591 # find the next nearest stock reco so that we only recalculate SLEs till that point
1592 next_stock_reco_detail = get_next_stock_reco(args)
1593 if next_stock_reco_detail:
1594 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301595
marination40389772021-07-02 17:13:45 +05301596 # add condition to update SLEs before this date & time
1597 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301598
Ankush Menat494bd9e2022-03-28 18:52:46 +05301599 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301600 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301601 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301602 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301603 where
1604 item_code = %(item_code)s
1605 and warehouse = %(warehouse)s
1606 and voucher_no != %(voucher_no)s
1607 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301608 and (
1609 posting_date > %(posting_date)s or
1610 (
1611 posting_date = %(posting_date)s and
1612 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1613 )
1614 )
marination40389772021-07-02 17:13:45 +05301615 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301616 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301617 args,
1618 )
Nabin Hait186a0452021-02-18 14:14:21 +05301619
1620 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1621
Ankush Menat494bd9e2022-03-28 18:52:46 +05301622
marination40389772021-07-02 17:13:45 +05301623def get_stock_reco_qty_shift(args):
1624 stock_reco_qty_shift = 0
1625 if args.get("is_cancelled"):
1626 if args.get("previous_qty_after_transaction"):
1627 # get qty (balance) that was set at submission
1628 last_balance = args.get("previous_qty_after_transaction")
1629 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1630 else:
1631 stock_reco_qty_shift = flt(args.actual_qty)
1632 else:
1633 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301634 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301635 "qty_after_transaction"
1636 )
marination40389772021-07-02 17:13:45 +05301637
1638 if last_balance is not None:
1639 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1640 else:
1641 stock_reco_qty_shift = args.qty_after_transaction
1642
1643 return stock_reco_qty_shift
1644
Ankush Menat494bd9e2022-03-28 18:52:46 +05301645
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301646def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301647 """Returns next nearest stock reconciliaton's details."""
1648
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301649 sle = frappe.qb.DocType("Stock Ledger Entry")
1650
1651 query = (
1652 frappe.qb.from_(sle)
1653 .select(
1654 sle.name,
1655 sle.posting_date,
1656 sle.posting_time,
1657 sle.creation,
1658 sle.voucher_no,
1659 sle.item_code,
1660 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301661 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301662 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301663 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301664 )
1665 .where(
1666 (sle.item_code == kwargs.get("item_code"))
1667 & (sle.warehouse == kwargs.get("warehouse"))
1668 & (sle.voucher_type == "Stock Reconciliation")
1669 & (sle.voucher_no != kwargs.get("voucher_no"))
1670 & (sle.is_cancelled == 0)
1671 & (
1672 (
1673 CombineDatetime(sle.posting_date, sle.posting_time)
1674 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301675 )
1676 | (
1677 (
1678 CombineDatetime(sle.posting_date, sle.posting_time)
1679 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301680 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301681 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301682 )
1683 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301684 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301685 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1686 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301687 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301688 )
1689
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301690 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301691 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301692
1693 return query.run(as_dict=True)
1694
marination40389772021-07-02 17:13:45 +05301695
1696def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301697 return f"""
1698 and
1699 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1700 or (
1701 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1702 and creation < '{detail.creation}'
1703 )
1704 )"""
1705
Ankush Menat494bd9e2022-03-28 18:52:46 +05301706
Ankush Menate7109c12021-08-26 16:40:45 +05301707def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301708 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301709 return
1710 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1711 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301712
Ankush Menat5eba5752021-12-07 23:03:52 +05301713 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301714
1715 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301716 message = _(
1717 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1718 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301719 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301720 frappe.get_desk_link("Item", args.item_code),
1721 frappe.get_desk_link("Warehouse", args.warehouse),
1722 neg_sle[0]["posting_date"],
1723 neg_sle[0]["posting_time"],
1724 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1725 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301726
Ankush Menat494bd9e2022-03-28 18:52:46 +05301727 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301728
s-aga-rd9e28432023-10-27 16:35:35 +05301729 if args.batch_no:
1730 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1731 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1732 message = _(
1733 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1734 ).format(
1735 abs(neg_batch_sle[0]["cumulative_total"]),
1736 frappe.get_desk_link("Batch", args.batch_no),
1737 frappe.get_desk_link("Warehouse", args.warehouse),
1738 neg_batch_sle[0]["posting_date"],
1739 neg_batch_sle[0]["posting_time"],
1740 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1741 )
1742 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301743
s-aga-r73b65ac2023-11-01 18:35:07 +05301744 if args.reserved_stock:
1745 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301746
Nabin Haita77b8c92020-12-21 14:45:50 +05301747
Maricad6078aa2022-06-17 15:13:13 +05301748def is_negative_with_precision(neg_sle, is_batch=False):
1749 """
1750 Returns whether system precision rounded qty is insufficient.
1751 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1752 """
1753
1754 if not neg_sle:
1755 return False
1756
1757 field = "cumulative_total" if is_batch else "qty_after_transaction"
1758 precision = cint(frappe.db.get_default("float_precision")) or 2
1759 qty_deficit = flt(neg_sle[0][field], precision)
1760
1761 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1762
1763
Nabin Haita77b8c92020-12-21 14:45:50 +05301764def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301765 return frappe.db.sql(
1766 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301767 select
1768 qty_after_transaction, posting_date, posting_time,
1769 voucher_type, voucher_no
1770 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301771 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301772 item_code = %(item_code)s
1773 and warehouse = %(warehouse)s
1774 and voucher_no != %(voucher_no)s
1775 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1776 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301777 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301778 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301779 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301780 """,
1781 args,
1782 as_dict=1,
1783 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301784
Ankush Menat5eba5752021-12-07 23:03:52 +05301785
1786def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301787 return frappe.db.sql(
1788 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301789 with batch_ledger as (
1790 select
1791 posting_date, posting_time, voucher_type, voucher_no,
1792 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1793 from `tabStock Ledger Entry`
1794 where
1795 item_code = %(item_code)s
1796 and warehouse = %(warehouse)s
1797 and batch_no=%(batch_no)s
1798 and is_cancelled = 0
1799 order by posting_date, posting_time, creation
1800 )
1801 select * from batch_ledger
1802 where
1803 cumulative_total < 0.0
1804 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1805 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301806 """,
1807 args,
1808 as_dict=1,
1809 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301810
1811
s-aga-rd9e28432023-10-27 16:35:35 +05301812def validate_reserved_stock(kwargs):
1813 if kwargs.serial_no:
1814 serial_nos = kwargs.serial_no.split("\n")
1815 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1816
s-aga-re1a87a82023-10-31 18:41:58 +05301817 elif kwargs.batch_no:
1818 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1819
s-aga-rd9e28432023-10-27 16:35:35 +05301820 elif kwargs.serial_and_batch_bundle:
1821 sbb_entries = frappe.db.get_all(
1822 "Serial and Batch Entry",
1823 {
1824 "parenttype": "Serial and Batch Bundle",
1825 "parent": kwargs.serial_and_batch_bundle,
1826 "docstatus": 1,
1827 },
s-aga-re1a87a82023-10-31 18:41:58 +05301828 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301829 )
s-aga-rd9e28432023-10-27 16:35:35 +05301830
s-aga-re1a87a82023-10-31 18:41:58 +05301831 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301832 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301833 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1834 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301835
s-aga-r92317062023-11-02 10:36:00 +05301836 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1837 precision = cint(frappe.db.get_default("float_precision")) or 2
1838 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301839
s-aga-r92317062023-11-02 10:36:00 +05301840 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1841 if diff < 0 and abs(diff) > 0.0001:
1842 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1843 abs(diff),
1844 frappe.get_desk_link("Item", kwargs.item_code),
1845 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1846 nowdate(),
1847 nowtime(),
1848 )
1849 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301850
s-aga-rd9e28432023-10-27 16:35:35 +05301851
1852def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1853 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1854 item_code, warehouse, serial_nos
1855 ):
1856 if common_serial_nos := list(
1857 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1858 ):
1859 msg = _(
1860 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1861 )
1862 msg += "<br />"
1863 msg += _("Example: Serial No {0} reserved in {1}.").format(
1864 frappe.bold(common_serial_nos[0]),
1865 frappe.get_desk_link(
1866 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1867 ),
1868 )
1869 frappe.throw(msg, title=_("Reserved Serial No."))
1870
1871
s-aga-re1a87a82023-10-31 18:41:58 +05301872def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1873 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1874 available_batches = get_available_batches(
1875 frappe._dict(
1876 {
1877 "item_code": item_code,
1878 "warehouse": warehouse,
1879 "posting_date": nowdate(),
1880 "posting_time": nowtime(),
1881 }
1882 )
1883 )
1884 available_batches_map = {row.batch_no: row.qty for row in available_batches}
1885 precision = cint(frappe.db.get_default("float_precision")) or 2
1886
1887 for batch_no in batch_nos:
1888 diff = flt(
1889 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
1890 )
1891 if diff < 0 and abs(diff) > 0.0001:
1892 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1893 abs(diff),
1894 frappe.get_desk_link("Batch", batch_no),
1895 frappe.get_desk_link("Warehouse", warehouse),
1896 nowdate(),
1897 nowtime(),
1898 )
1899 frappe.throw(msg, title=_("Reserved Stock for Batch"))
1900
1901
Ankush Menateb8b4242022-02-12 13:08:28 +05301902def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1903 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1904 return True
1905 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1906 return True
1907 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301908
1909
1910def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1911 """
1912 For inter company transfer, incoming rate is the average of the outgoing rate
1913 """
1914 rate = 0.0
1915
1916 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1917
1918 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1919
1920 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1921
1922 if reference_name:
1923 rate = frappe.get_cached_value(
1924 doctype,
1925 reference_name,
1926 "incoming_rate",
1927 )
1928
1929 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301930
1931
1932def is_internal_transfer(sle):
1933 data = frappe.get_cached_value(
1934 sle.voucher_type,
1935 sle.voucher_no,
1936 ["is_internal_supplier", "represents_company", "company"],
1937 as_dict=True,
1938 )
1939
1940 if data.is_internal_supplier and data.represents_company == data.company:
1941 return True