blob: 63908945c30970a57463f41a7eb5dff778daf0d1 [file] [log] [blame]
Maricad6078aa2022-06-17 15:13:13 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait902e8602013-01-08 18:29:24 +05303
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05304import copy
Akhil Narang21c3d9c2023-10-21 11:19:45 +05305import gzip
Nabin Hait26d46552013-01-09 15:23:05 +05306import json
Ankush Menatecdb4932022-04-17 19:06:13 +05307from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05308
9import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053010from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053011from frappe.model.meta import get_field_precision
Rohit Waghchaurebb954512023-06-02 00:11:43 +053012from frappe.query_builder import Case
Ankush Menate1c16872022-04-21 20:01:48 +053013from frappe.query_builder.functions import CombineDatetime, Sum
s-aga-re1a87a82023-10-31 18:41:58 +053014from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json
Achilles Rasquinha361366e2018-02-14 17:08:59 +053015
Chillar Anand915b3432021-09-02 16:44:59 +053016import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053017from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
mergify[bot]27a1e3b2023-10-16 19:15:18 +053018from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
s-aga-re1a87a82023-10-31 18:41:58 +053019from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
20 get_available_batches,
21)
22from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
23 get_sre_reserved_batch_nos_details,
s-aga-rd9e28432023-10-27 16:35:35 +053024 get_sre_reserved_serial_nos_details,
25)
Chillar Anand915b3432021-09-02 16:44:59 +053026from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053027 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053028 get_or_make_bin,
s-aga-r73b65ac2023-11-01 18:35:07 +053029 get_stock_balance,
Chillar Anand915b3432021-09-02 16:44:59 +053030 get_valuation_method,
31)
Ankush Menatb534fee2022-02-19 20:58:36 +053032from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053033
Nabin Hait97bce3a2021-07-12 13:24:43 +053034
Ankush Menat494bd9e2022-03-28 18:52:46 +053035class NegativeStockError(frappe.ValidationError):
36 pass
37
38
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053039class SerialNoExistsInFutureTransaction(frappe.ValidationError):
40 pass
Nabin Hait902e8602013-01-08 18:29:24 +053041
Anand Doshi5b004ff2013-09-25 19:55:41 +053042
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053043def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053044 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053045
Ankush Menat494bd9e2022-03-28 18:52:46 +053046 args:
47 - allow_negative_stock: disable negative stock valiations if true
48 - via_landed_cost_voucher: landed cost voucher cancels and reposts
49 entries of purchase document. This flag is used to identify if
50 cancellation and repost is happening via landed cost voucher, in
51 such cases certain validations need to be ignored (like negative
52 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053053 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053054 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053055
Nabin Haitca775742013-09-26 16:16:44 +053056 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053057 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053058 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053059 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053060 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053061
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053062 args = get_args_for_future_sle(sl_entries[0])
63 future_sle_exists(args, sl_entries)
64
Nabin Haitca775742013-09-26 16:16:44 +053065 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053066 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053067 validate_serial_no(sle)
68
Nabin Haita77b8c92020-12-21 14:45:50 +053069 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053070 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053071
Ankush Menat494bd9e2022-03-28 18:52:46 +053072 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
73 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
74 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
75 )
76 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053077
Ankush Menat494bd9e2022-03-28 18:52:46 +053078 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
79 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
80 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
81 )
82 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053083
Ankush Menat494bd9e2022-03-28 18:52:46 +053084 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053085 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053086
Nabin Haita77b8c92020-12-21 14:45:50 +053087 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053088
89 if sle.get("voucher_type") == "Stock Reconciliation":
90 # preserve previous_qty_after_transaction for qty reposting
91 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
92
Ankush Menat494bd9e2022-03-28 18:52:46 +053093 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053094 if is_stock_item:
95 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
s-aga-r73b65ac2023-11-01 18:35:07 +053096 args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
Ankush Menatcef84c22021-12-03 12:18:59 +053097 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053098 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053099 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530100 frappe.msgprint(
101 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
102 )
103
Ankush Menatcef84c22021-12-03 12:18:59 +0530104
105def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
106 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
107 if not args.get("posting_date"):
108 args["posting_date"] = nowdate()
109
marination7a5fd712022-07-04 17:46:54 +0530110 if not (args.get("is_cancelled") and via_landed_cost_voucher):
111 # Reposts only current voucher SL Entries
112 # Updates valuation rate, stock value, stock queue for current transaction
113 update_entries_after(
114 {
115 "item_code": args.get("item_code"),
116 "warehouse": args.get("warehouse"),
117 "posting_date": args.get("posting_date"),
118 "posting_time": args.get("posting_time"),
119 "voucher_type": args.get("voucher_type"),
120 "voucher_no": args.get("voucher_no"),
121 "sle_id": args.get("name"),
122 "creation": args.get("creation"),
s-aga-r73b65ac2023-11-01 18:35:07 +0530123 "reserved_stock": args.get("reserved_stock"),
marination7a5fd712022-07-04 17:46:54 +0530124 },
125 allow_negative_stock=allow_negative_stock,
126 via_landed_cost_voucher=via_landed_cost_voucher,
127 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530128
129 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530130 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530131 update_qty_in_future_sle(args, allow_negative_stock)
132
Nabin Haitadeb9762014-10-06 11:53:52 +0530133
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530134def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530135 return frappe._dict(
136 {
137 "voucher_type": row.get("voucher_type"),
138 "voucher_no": row.get("voucher_no"),
139 "posting_date": row.get("posting_date"),
140 "posting_time": row.get("posting_time"),
141 }
142 )
143
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530144
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530145def validate_serial_no(sle):
146 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530147
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530148 for sn in get_serial_nos(sle.serial_no):
149 args = copy.deepcopy(sle)
150 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530151 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530152
153 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530154 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530155 voucher_type = frappe.bold(row.voucher_type)
156 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530157 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530158
159 if vouchers:
160 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530161 msg = (
162 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
163 The list of the transactions are as below."""
164 + "<br><br><ul><li>"
165 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530166
Ankush Menat494bd9e2022-03-28 18:52:46 +0530167 msg += "</li><li>".join(vouchers)
168 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530169
Ankush Menat494bd9e2022-03-28 18:52:46 +0530170 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530171 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
172
Ankush Menat494bd9e2022-03-28 18:52:46 +0530173
Nabin Hait186a0452021-02-18 14:14:21 +0530174def validate_cancellation(args):
175 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530176 repost_entry = frappe.db.get_value(
177 "Repost Item Valuation",
178 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
179 ["name", "status"],
180 as_dict=1,
181 )
Nabin Hait186a0452021-02-18 14:14:21 +0530182
183 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530184 if repost_entry.status == "In Progress":
185 frappe.throw(
186 _(
187 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
188 )
189 )
190 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530191 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530192 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530193 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530194 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530195
Ankush Menat494bd9e2022-03-28 18:52:46 +0530196
Nabin Hait9653f602013-08-20 15:37:33 +0530197def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530198 frappe.db.sql(
199 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530200 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530201 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530202 (now(), frappe.session.user, voucher_type, voucher_no),
203 )
204
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530205
Nabin Hait54c865e2015-03-27 15:38:31 +0530206def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530207 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530208 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530209 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530210 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530211 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530212 sle.submit()
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530213
214 # Added to handle the case when the stock ledger entry is created from the repostig
215 if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
216 sle.db_set("creation", args.get("creation_time"))
217
Nabin Haita77b8c92020-12-21 14:45:50 +0530218 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530219
Ankush Menat494bd9e2022-03-28 18:52:46 +0530220
221def repost_future_sle(
222 args=None,
223 voucher_type=None,
224 voucher_no=None,
225 allow_negative_stock=None,
226 via_landed_cost_voucher=False,
227 doc=None,
228):
Nabin Haite1fa7232022-07-20 15:19:09 +0530229 if not args:
230 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530231
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530232 reposting_data = {}
233 if doc and doc.reposting_data_file:
234 reposting_data = get_reposting_data(doc.reposting_data_file)
235
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530236 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530237 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530238 )
239 if items_to_be_repost:
240 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530241
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530242 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
243 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530244
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530245 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530246 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530247 validate_item_warehouse(args[i])
248
Ankush Menat494bd9e2022-03-28 18:52:46 +0530249 obj = update_entries_after(
250 {
251 "item_code": args[i].get("item_code"),
252 "warehouse": args[i].get("warehouse"),
253 "posting_date": args[i].get("posting_date"),
254 "posting_time": args[i].get("posting_time"),
255 "creation": args[i].get("creation"),
256 "distinct_item_warehouses": distinct_item_warehouses,
257 },
258 allow_negative_stock=allow_negative_stock,
259 via_landed_cost_voucher=via_landed_cost_voucher,
260 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530261 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530262
Ankush Menat494bd9e2022-03-28 18:52:46 +0530263 distinct_item_warehouses[
264 (args[i].get("item_code"), args[i].get("warehouse"))
265 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530266
Nabin Hait97bce3a2021-07-12 13:24:43 +0530267 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530268 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530269 if ("args_idx" not in data and not data.reposting_status) or (
270 data.sle_changed and data.reposting_status
271 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530272 data.args_idx = len(args)
273 args.append(data.sle)
274 elif data.sle_changed and not data.reposting_status:
275 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530276
Nabin Hait97bce3a2021-07-12 13:24:43 +0530277 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530278 i += 1
279
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530280 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530281 update_args_in_repost_item_valuation(
282 doc, i, args, distinct_item_warehouses, affected_transactions
283 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530284
Ankush Menat494bd9e2022-03-28 18:52:46 +0530285
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530286def get_reposting_data(file_path) -> dict:
287 file_name = frappe.db.get_value(
288 "File",
289 {
290 "file_url": file_path,
291 "attached_to_field": "reposting_data_file",
292 },
293 "name",
294 )
295
296 if not file_name:
297 return frappe._dict()
298
299 attached_file = frappe.get_doc("File", file_name)
300
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530301 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530302 if data := json.loads(data.decode("utf-8")):
303 data = data
304
305 return parse_json(data)
306
307
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530308def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530309 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530310 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530311 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530312 frappe.throw(_(validation_msg))
313
Ankush Menat494bd9e2022-03-28 18:52:46 +0530314
Ankush Menatecdb4932022-04-17 19:06:13 +0530315def update_args_in_repost_item_valuation(
316 doc, index, args, distinct_item_warehouses, affected_transactions
317):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530318 if not doc.items_to_be_repost:
319 file_name = ""
320 if doc.reposting_data_file:
321 file_name = get_reposting_file_name(doc.doctype, doc.name)
322 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
323
324 doc.reposting_data_file = create_json_gz_file(
325 {
326 "items_to_be_repost": args,
327 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
328 "affected_transactions": affected_transactions,
329 },
330 doc,
331 file_name,
332 )
333
334 doc.db_set(
335 {
336 "current_index": index,
337 "total_reposting_count": len(args),
338 "reposting_data_file": doc.reposting_data_file,
339 }
340 )
341
342 else:
343 doc.db_set(
344 {
345 "items_to_be_repost": json.dumps(args, default=str),
346 "distinct_item_and_warehouse": json.dumps(
347 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
348 ),
349 "current_index": index,
350 "affected_transactions": frappe.as_json(affected_transactions),
351 }
352 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530353
Ankush Menatecdb4932022-04-17 19:06:13 +0530354 if not frappe.flags.in_test:
355 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530356
Ankush Menat494bd9e2022-03-28 18:52:46 +0530357 frappe.publish_realtime(
358 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530359 {
360 "name": doc.name,
361 "items_to_be_repost": json.dumps(args, default=str),
362 "current_index": index,
363 "total_reposting_count": len(args),
364 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530365 doctype=doc.doctype,
366 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530367 )
368
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530369
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530370def get_reposting_file_name(dt, dn):
371 return frappe.db.get_value(
372 "File",
373 {
374 "attached_to_doctype": dt,
375 "attached_to_name": dn,
376 "attached_to_field": "reposting_data_file",
377 },
378 "name",
379 )
380
381
382def create_json_gz_file(data, doc, file_name=None) -> str:
383 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530384 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530385
386 if not file_name:
387 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
388 _file = frappe.get_doc(
389 {
390 "doctype": "File",
391 "file_name": json_filename,
392 "attached_to_doctype": doc.doctype,
393 "attached_to_name": doc.name,
394 "attached_to_field": "reposting_data_file",
395 "content": compressed_content,
396 "is_private": 1,
397 }
398 )
399 _file.save(ignore_permissions=True)
400
401 return _file.file_url
402 else:
403 file_doc = frappe.get_doc("File", file_name)
404 path = file_doc.get_full_path()
405
406 with open(path, "wb") as f:
407 f.write(compressed_content)
408
409 return doc.reposting_data_file
410
411
412def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
413 if not reposting_data and doc and doc.reposting_data_file:
414 reposting_data = get_reposting_data(doc.reposting_data_file)
415
416 if reposting_data and reposting_data.items_to_be_repost:
417 return reposting_data.items_to_be_repost
418
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530419 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530420
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530421 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530422 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530423
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530424 if not items_to_be_repost and voucher_type and voucher_no:
425 items_to_be_repost = frappe.db.get_all(
426 "Stock Ledger Entry",
427 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
428 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
429 order_by="creation asc",
430 group_by="item_code, warehouse",
431 )
432
Nabin Haite1fa7232022-07-20 15:19:09 +0530433 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530434
Ankush Menat494bd9e2022-03-28 18:52:46 +0530435
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530436def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
437 if not reposting_data and doc and doc.reposting_data_file:
438 reposting_data = get_reposting_data(doc.reposting_data_file)
439
440 if reposting_data and reposting_data.distinct_item_and_warehouse:
441 return reposting_data.distinct_item_and_warehouse
442
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530443 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530444
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530445 if doc and doc.distinct_item_and_warehouse:
446 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530447 distinct_item_warehouses = {
448 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
449 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530450 else:
451 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530452 distinct_item_warehouses.setdefault(
453 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
454 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530455
456 return distinct_item_warehouses
457
Ankush Menat494bd9e2022-03-28 18:52:46 +0530458
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530459def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
460 if not reposting_data and doc and doc.reposting_data_file:
461 reposting_data = get_reposting_data(doc.reposting_data_file)
462
463 if reposting_data and reposting_data.affected_transactions:
464 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
465
Ankush Menatecdb4932022-04-17 19:06:13 +0530466 if not doc.affected_transactions:
467 return set()
468
469 transactions = frappe.parse_json(doc.affected_transactions)
470 return {tuple(transaction) for transaction in transactions}
471
472
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530473def get_current_index(doc=None):
474 if doc and doc.current_index:
475 return doc.current_index
476
Ankush Menat494bd9e2022-03-28 18:52:46 +0530477
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530478class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530479 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530480 update valution rate and qty after transaction
481 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530482
Ankush Menat494bd9e2022-03-28 18:52:46 +0530483 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530484
Ankush Menat494bd9e2022-03-28 18:52:46 +0530485 args = {
486 "item_code": "ABC",
487 "warehouse": "XYZ",
488 "posting_date": "2012-12-12",
489 "posting_time": "12:00"
490 }
Nabin Hait902e8602013-01-08 18:29:24 +0530491 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530492
493 def __init__(
494 self,
495 args,
496 allow_zero_rate=False,
497 allow_negative_stock=None,
498 via_landed_cost_voucher=False,
499 verbose=1,
500 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530501 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530502 self.verbose = verbose
503 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530504 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530505 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530506 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
507 item_code=self.item_code
508 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530509
Nabin Haita77b8c92020-12-21 14:45:50 +0530510 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530511 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530512 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530513
Nabin Haita77b8c92020-12-21 14:45:50 +0530514 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530515 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530516 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530517
518 self.new_items_found = False
519 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530520 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-r73b65ac2023-11-01 18:35:07 +0530521 self.reserved_stock = flt(self.args.reserved_stock)
Nabin Haita77b8c92020-12-21 14:45:50 +0530522
523 self.data = frappe._dict()
524 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530525 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530526
Maricad6078aa2022-06-17 15:13:13 +0530527 def set_precision(self):
528 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
529 self.currency_precision = get_field_precision(
530 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530531 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530532
533 def initialize_previous_data(self, args):
534 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530535 Get previous sl entries for current item for each related warehouse
536 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530537
Ankush Menat494bd9e2022-03-28 18:52:46 +0530538 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530539
Ankush Menat494bd9e2022-03-28 18:52:46 +0530540 self.data = {
541 warehouse1: {
542 'previus_sle': {},
543 'qty_after_transaction': 10,
544 'valuation_rate': 100,
545 'stock_value': 1000,
546 'prev_stock_value': 1000,
547 'stock_queue': '[[10, 100]]',
548 'stock_value_difference': 1000
549 }
550 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530551
552 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530553 self.data.setdefault(args.warehouse, frappe._dict())
554 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530555 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530556 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530557
Ankush Menatc1d986a2021-08-31 19:43:42 +0530558 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
559 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
560
Ankush Menat494bd9e2022-03-28 18:52:46 +0530561 warehouse_dict.update(
562 {
563 "prev_stock_value": previous_sle.stock_value or 0.0,
564 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
565 "stock_value_difference": 0.0,
566 }
567 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530568
Nabin Haita77b8c92020-12-21 14:45:50 +0530569 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530570 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530571
Nabin Haita77b8c92020-12-21 14:45:50 +0530572 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530573 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530574 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530575 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530576 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530577 entries_to_fix = self.get_future_entries_to_fix()
578
579 i = 0
580 while i < len(entries_to_fix):
581 sle = entries_to_fix[i]
582 i += 1
583
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530584 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530585 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530586
Nabin Haita77b8c92020-12-21 14:45:50 +0530587 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530588 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530589
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530590 if self.exceptions:
591 self.raise_exceptions()
592
Nabin Hait186a0452021-02-18 14:14:21 +0530593 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530594 sl_entries = self.get_sle_against_current_voucher()
595 for sle in sl_entries:
596 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530597
Nabin Haita77b8c92020-12-21 14:45:50 +0530598 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530599 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530600
Ankush Menat494bd9e2022-03-28 18:52:46 +0530601 return frappe.db.sql(
602 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530603 select
604 *, timestamp(posting_date, posting_time) as "timestamp"
605 from
606 `tabStock Ledger Entry`
607 where
608 item_code = %(item_code)s
609 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530610 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530611 and (
612 posting_date = %(posting_date)s and
613 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
614 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530615 order by
616 creation ASC
617 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530618 """,
619 self.args,
620 as_dict=1,
621 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530622
Nabin Haita77b8c92020-12-21 14:45:50 +0530623 def get_future_entries_to_fix(self):
624 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530625 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
626 {"item_code": self.item_code, "warehouse": self.args.warehouse}
627 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530628
Nabin Haita77b8c92020-12-21 14:45:50 +0530629 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530630
Nabin Haita77b8c92020-12-21 14:45:50 +0530631 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530632 dependant_sle = get_sle_by_voucher_detail_no(
633 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
634 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530635
Nabin Haita77b8c92020-12-21 14:45:50 +0530636 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530637 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530638 elif (
639 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
640 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530641 return entries_to_fix
642 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530643 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530644 return entries_to_fix
645 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
646 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530647 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530648 self.initialize_previous_data(dependant_sle)
649 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530650 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530651
652 def update_distinct_item_warehouses(self, dependant_sle):
653 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530654 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530655
Nabin Hait97bce3a2021-07-12 13:24:43 +0530656 if key not in self.distinct_item_warehouses:
657 self.distinct_item_warehouses[key] = val
658 self.new_items_found = True
659 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530660 existing_sle_posting_date = (
661 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
662 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530663
664 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
665
Nabin Hait97bce3a2021-07-12 13:24:43 +0530666 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
667 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530668 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
669 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530670 self.distinct_item_warehouses[key] = val
671 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530672 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
673 # Future dependent voucher needs to be repost to get the correct stock value
674 # If dependent voucher has not reposted, then add it to the list
675 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530676 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530677 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
678 self.distinct_item_warehouses[key] = val
679
680 def get_dependent_voucher_detail_nos(self, key):
681 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
682 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
683
684 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530685
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530686 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530687 # previous sle data for this warehouse
688 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530689 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530690
Anand Doshi0dc79f42015-04-06 12:59:34 +0530691 if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530692 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530693 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530694 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530695 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530696 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530697
Nabin Haita77b8c92020-12-21 14:45:50 +0530698 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530699 if not self.args.get("sle_id"):
700 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530701
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530702 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530703 sle.voucher_type == "Stock Reconciliation"
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530704 and (
705 sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
706 )
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530707 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530708 and not self.args.get("sle_id")
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530709 ):
710 self.reset_actual_qty_for_stock_reco(sle)
711
712 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530713 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
714 and sle.voucher_detail_no
715 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530716 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530717 ):
718 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
719
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530720 dimensions = get_inventory_dimensions()
721 has_dimensions = False
722 if dimensions:
723 for dimension in dimensions:
724 if sle.get(dimension.get("fieldname")):
725 has_dimensions = True
726
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530727 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530728 self.calculate_valuation_for_serial_batch_bundle(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530729 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530730 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530731 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530732 self.wh_data.valuation_rate = sle.valuation_rate
733 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530734 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
735 self.wh_data.valuation_rate
736 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530737 if self.valuation_method != "Moving Average":
738 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530739 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530740 if self.valuation_method == "Moving Average":
741 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530742 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530743 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
744 self.wh_data.valuation_rate
745 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530746 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530747 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530748
Rushabh Mehta54047782013-12-26 11:07:46 +0530749 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530750 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530751 if not self.wh_data.qty_after_transaction:
752 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530753
Nabin Haita77b8c92020-12-21 14:45:50 +0530754 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
755 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530756
Nabin Hait902e8602013-01-08 18:29:24 +0530757 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530758 sle.qty_after_transaction = self.wh_data.qty_after_transaction
759 sle.valuation_rate = self.wh_data.valuation_rate
760 sle.stock_value = self.wh_data.stock_value
761 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530762
rohitwaghchaurea8216b92023-11-09 12:22:26 +0530763 if not sle.is_adjustment_entry or not self.args.get("sle_id"):
764 sle.stock_value_difference = stock_value_difference
765
766 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530767 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530768
Ankush Menat701878f2022-03-01 18:08:29 +0530769 if not self.args.get("sle_id"):
770 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530771
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530772 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530773 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
774 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
775
776 if sle.actual_qty < 0:
777 sle.actual_qty = (
778 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
779 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530780 )
781
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530782 if abs(sle.actual_qty) == 0.0:
783 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530784
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530785 def calculate_valuation_for_serial_batch_bundle(self, sle):
786 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
787
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530788 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530789 doc.calculate_qty_and_amount(save=True)
790
791 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
792
793 self.wh_data.qty_after_transaction += doc.total_qty
794 if self.wh_data.qty_after_transaction:
795 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
796
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530797 def validate_negative_stock(self, sle):
798 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530799 validate negative stock for entries current datetime onwards
800 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530801 """
s-aga-rf0acb202023-04-12 14:13:54 +0530802 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530803 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530804
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530805 if diff < 0 and abs(diff) > 0.0001:
806 # negative stock!
807 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530808 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530809 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530810 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530811 return True
812
Nabin Haita77b8c92020-12-21 14:45:50 +0530813 def get_dynamic_incoming_outgoing_rate(self, sle):
814 # Get updated incoming/outgoing rate from transaction
815 if sle.recalculate_rate:
816 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
817
818 if flt(sle.actual_qty) >= 0:
819 sle.incoming_rate = rate
820 else:
821 sle.outgoing_rate = rate
822
823 def get_incoming_outgoing_rate_from_transaction(self, sle):
824 rate = 0
825 # Material Transfer, Repack, Manufacturing
826 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530827 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530828 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
829 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530830 elif sle.voucher_type in (
831 "Purchase Receipt",
832 "Purchase Invoice",
833 "Delivery Note",
834 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530835 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530836 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530837 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530838 from erpnext.controllers.sales_and_purchase_return import (
839 get_rate_for_return, # don't move this import to top
840 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530841
842 rate = get_rate_for_return(
843 sle.voucher_type,
844 sle.voucher_no,
845 sle.item_code,
846 voucher_detail_no=sle.voucher_detail_no,
847 sle=sle,
848 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530849
850 elif (
851 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530852 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530853 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530854 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530855 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530856 else:
857 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530858 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530859 elif sle.voucher_type == "Subcontracting Receipt":
860 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530861 else:
862 rate_field = "incoming_rate"
863
864 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530865 item_code, incoming_rate = frappe.db.get_value(
866 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
867 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530868
869 if item_code == sle.item_code:
870 rate = incoming_rate
871 else:
872 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
873 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530874 elif sle == "Subcontracting Receipt":
875 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530876 else:
877 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530878
Ankush Menat494bd9e2022-03-28 18:52:46 +0530879 rate = frappe.db.get_value(
880 ref_doctype,
881 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
882 rate_field,
883 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530884
885 return rate
886
887 def update_outgoing_rate_on_transaction(self, sle):
888 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530889 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
890 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530891 """
892 if sle.actual_qty and sle.voucher_detail_no:
893 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
894
895 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
896 self.update_rate_on_stock_entry(sle, outgoing_rate)
897 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
898 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
899 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
900 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530901 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
902 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530903 elif sle.voucher_type == "Stock Reconciliation":
904 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530905
906 def update_rate_on_stock_entry(self, sle, outgoing_rate):
907 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
908
Ankush Menat701878f2022-03-01 18:08:29 +0530909 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
910 if not sle.dependant_sle_voucher_detail_no:
911 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530912
913 def recalculate_amounts_in_stock_entry(self, voucher_no):
914 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530915 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
916 stock_entry.db_update()
917 for d in stock_entry.items:
918 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530919
Nabin Haita77b8c92020-12-21 14:45:50 +0530920 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
921 # Update item's incoming rate on transaction
922 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
923 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530924 frappe.db.set_value(
925 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
926 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530927 else:
928 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530929 frappe.db.set_value(
930 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530931 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530932 "incoming_rate",
933 outgoing_rate,
934 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530935
936 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
937 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530938 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
939 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
940 ):
941 frappe.db.set_value(
942 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
943 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530944 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530945 frappe.db.set_value(
946 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
947 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530948
949 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530950 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530951 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530952 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530953 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530954 d.db_update()
955
Sagar Sharma323bdf82022-05-17 15:14:07 +0530956 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530957 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
958 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530959 else:
960 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530961 "Subcontracting Receipt Supplied Item",
962 sle.voucher_detail_no,
963 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530964 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530965
s-aga-ra6cb6c62023-05-03 09:51:58 +0530966 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530967 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530968 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530969 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530970 d.db_update()
971
s-aga-r88a3f652023-05-30 16:54:28 +0530972 def update_rate_on_stock_reconciliation(self, sle):
973 if not sle.serial_no and not sle.batch_no:
974 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
975
976 for item in sr.items:
977 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +0530978 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +0530979 continue
980
981 previous_sle = get_previous_sle(
982 {
983 "item_code": item.item_code,
984 "warehouse": item.warehouse,
985 "posting_date": sr.posting_date,
986 "posting_time": sr.posting_time,
987 "sle": sle.name,
988 }
989 )
990
991 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
992 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
993 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
994
995 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +0530996 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +0530997 item.amount_difference = item.amount - item.current_amount
998 else:
999 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1000 sr.db_update()
1001
1002 for item in sr.items:
1003 item.db_update()
1004
Nabin Hait328c4f92020-01-02 19:00:32 +05301005 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1006 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301007 all_serial_nos = frappe.get_all(
1008 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1009 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301010
Ankush Menat494bd9e2022-03-28 18:52:46 +05301011 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 +05301012
1013 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301014 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301015 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301016 incoming_rate = frappe.db.sql(
1017 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301018 select incoming_rate
1019 from `tabStock Ledger Entry`
1020 where
1021 company = %s
1022 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301023 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301024 and (serial_no = %s
1025 or serial_no like %s
1026 or serial_no like %s
1027 or serial_no like %s
1028 )
1029 order by posting_date desc
1030 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301031 """,
1032 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1033 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301034
1035 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1036
1037 return incoming_values
1038
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301039 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301040 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301041 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301042 if new_stock_qty >= 0:
1043 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301044 if flt(self.wh_data.qty_after_transaction) <= 0:
1045 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301046 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301047 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1048 actual_qty * sle.incoming_rate
1049 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301050
Nabin Haita77b8c92020-12-21 14:45:50 +05301051 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301052
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301053 elif sle.outgoing_rate:
1054 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301055 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1056 actual_qty * sle.outgoing_rate
1057 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301058
Nabin Haita77b8c92020-12-21 14:45:50 +05301059 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301060 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301061 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301062 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301063 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1064 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301065
Nabin Haita77b8c92020-12-21 14:45:50 +05301066 if not self.wh_data.valuation_rate and actual_qty > 0:
1067 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301068
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301069 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001070 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301071 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301072 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1073 sle.voucher_type, sle.voucher_detail_no
1074 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001075 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301076 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301077
Ankush Menatf089d392022-02-02 12:51:21 +05301078 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301079 incoming_rate = flt(sle.incoming_rate)
1080 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301081 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301082
Ankush Menat494bd9e2022-03-28 18:52:46 +05301083 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1084 self.wh_data.qty_after_transaction + actual_qty
1085 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301086
Ankush Menat97e18a12022-01-15 17:42:25 +05301087 if self.valuation_method == "LIFO":
1088 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1089 else:
1090 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1091
Ankush Menatb534fee2022-02-19 20:58:36 +05301092 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1093
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301094 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301095 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301096 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301097
Ankush Menat4b29fb62021-12-18 18:40:22 +05301098 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301099 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1100 sle.voucher_type, sle.voucher_detail_no
1101 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301102 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301103 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301104 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301105 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301106
Ankush Menat494bd9e2022-03-28 18:52:46 +05301107 stock_queue.remove_stock(
1108 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1109 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301110
Ankush Menatb534fee2022-02-19 20:58:36 +05301111 _qty, stock_value = stock_queue.get_total_stock_and_value()
1112
1113 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301114
Ankush Menat97e18a12022-01-15 17:42:25 +05301115 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301116 self.wh_data.stock_value = round_off_if_near_zero(
1117 self.wh_data.stock_value + stock_value_difference
1118 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301119
Nabin Haita77b8c92020-12-21 14:45:50 +05301120 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301121 self.wh_data.stock_queue.append(
1122 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1123 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301124
Ankush Menatb534fee2022-02-19 20:58:36 +05301125 if self.wh_data.qty_after_transaction:
1126 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1127
Ankush Menatce0514c2022-02-15 11:41:41 +05301128 def update_batched_values(self, sle):
1129 incoming_rate = flt(sle.incoming_rate)
1130 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301131
Ankush Menat494bd9e2022-03-28 18:52:46 +05301132 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1133 self.wh_data.qty_after_transaction + actual_qty
1134 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301135
1136 if actual_qty > 0:
1137 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301138 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301139 outgoing_rate = get_batch_incoming_rate(
1140 item_code=sle.item_code,
1141 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301142 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301143 posting_date=sle.posting_date,
1144 posting_time=sle.posting_time,
1145 creation=sle.creation,
1146 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301147 if outgoing_rate is None:
1148 # This can *only* happen if qty available for the batch is zero.
1149 # in such case fall back various other rates.
1150 # future entries will correct the overall accounting as each
1151 # batch individually uses moving average rates.
1152 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301153 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301154
Ankush Menat494bd9e2022-03-28 18:52:46 +05301155 self.wh_data.stock_value = round_off_if_near_zero(
1156 self.wh_data.stock_value + stock_value_difference
1157 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301158 if self.wh_data.qty_after_transaction:
1159 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301160
Javier Wong9b11d9b2017-04-14 18:24:04 +08001161 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301162 ref_item_dt = ""
1163
1164 if voucher_type == "Stock Entry":
1165 ref_item_dt = voucher_type + " Detail"
1166 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1167 ref_item_dt = voucher_type + " Item"
1168
1169 if ref_item_dt:
1170 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1171 else:
1172 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301173
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301174 def get_fallback_rate(self, sle) -> float:
1175 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301176 This should only get used for negative stock."""
1177 return get_valuation_rate(
1178 sle.item_code,
1179 sle.warehouse,
1180 sle.voucher_type,
1181 sle.voucher_no,
1182 self.allow_zero_rate,
1183 currency=erpnext.get_company_currency(sle.company),
1184 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301185 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301186
Nabin Haita77b8c92020-12-21 14:45:50 +05301187 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301188 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301189 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1190 sle = sle[0] if sle else frappe._dict()
1191 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301192
Nabin Haita77b8c92020-12-21 14:45:50 +05301193 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301194 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301195 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301196
1197 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301198 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301199 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301200 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301201
Ankush Menat494bd9e2022-03-28 18:52:46 +05301202 if (
1203 exceptions[0]["voucher_type"],
1204 exceptions[0]["voucher_no"],
1205 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301206
Nabin Haita77b8c92020-12-21 14:45:50 +05301207 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301208 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301209 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1210 frappe.get_desk_link("Warehouse", warehouse),
1211 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301212 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301213 msg = _(
1214 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1215 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301216 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301217 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1218 frappe.get_desk_link("Warehouse", warehouse),
1219 exceptions[0]["posting_date"],
1220 exceptions[0]["posting_time"],
1221 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1222 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301223
Nabin Haita77b8c92020-12-21 14:45:50 +05301224 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301225 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301226 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301227
1228 if allowed_qty > 0:
1229 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1230 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1231 )
1232 else:
1233 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1234 msg,
1235 )
s-aga-rf0acb202023-04-12 14:13:54 +05301236
Nabin Haita77b8c92020-12-21 14:45:50 +05301237 msg_list.append(msg)
1238
1239 if msg_list:
1240 message = "\n\n".join(msg_list)
1241 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301242 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301243 else:
1244 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301245
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301246 def update_bin_data(self, sle):
1247 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301248 values_to_update = {
1249 "actual_qty": sle.qty_after_transaction,
1250 "stock_value": sle.stock_value,
1251 }
1252
1253 if sle.valuation_rate is not None:
1254 values_to_update["valuation_rate"] = sle.valuation_rate
1255
1256 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301257
Nabin Haita77b8c92020-12-21 14:45:50 +05301258 def update_bin(self):
1259 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301260 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301261 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301262
Ankush Menat494bd9e2022-03-28 18:52:46 +05301263 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301264 if data.valuation_rate is not None:
1265 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301266 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301267
marination8418c4b2021-06-22 21:35:25 +05301268
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301269def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301270 """get stock ledger entries filtered by specific posting datetime conditions"""
1271
Ankush Menat494bd9e2022-03-28 18:52:46 +05301272 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301273 if not args.get("posting_date"):
1274 args["posting_date"] = "1900-01-01"
1275 if not args.get("posting_time"):
1276 args["posting_time"] = "00:00"
1277
1278 voucher_condition = ""
1279 if exclude_current_voucher:
1280 voucher_no = args.get("voucher_no")
1281 voucher_condition = f"and voucher_no != '{voucher_no}'"
1282
Ankush Menat494bd9e2022-03-28 18:52:46 +05301283 sle = frappe.db.sql(
1284 """
marination8418c4b2021-06-22 21:35:25 +05301285 select *, timestamp(posting_date, posting_time) as "timestamp"
1286 from `tabStock Ledger Entry`
1287 where item_code = %(item_code)s
1288 and warehouse = %(warehouse)s
1289 and is_cancelled = 0
1290 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301291 and (
1292 posting_date < %(posting_date)s or
1293 (
1294 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301295 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301296 )
1297 )
marination8418c4b2021-06-22 21:35:25 +05301298 order by timestamp(posting_date, posting_time) desc, creation desc
1299 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301300 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301301 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301302 ),
1303 args,
1304 as_dict=1,
1305 )
marination8418c4b2021-06-22 21:35:25 +05301306
1307 return sle[0] if sle else frappe._dict()
1308
Ankush Menat494bd9e2022-03-28 18:52:46 +05301309
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301310def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301311 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301312 get the last sle on or before the current time-bucket,
1313 to get actual qty before transaction, this function
1314 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301315
Ankush Menat494bd9e2022-03-28 18:52:46 +05301316 args = {
1317 "item_code": "ABC",
1318 "warehouse": "XYZ",
1319 "posting_date": "2012-12-12",
1320 "posting_time": "12:00",
1321 "sle": "name of reference Stock Ledger Entry"
1322 }
Anand Doshi1b531862013-01-10 19:29:51 +05301323 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301324 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301325 sle = get_stock_ledger_entries(
1326 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1327 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301328 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301329
Ankush Menat494bd9e2022-03-28 18:52:46 +05301330
1331def get_stock_ledger_entries(
1332 previous_sle,
1333 operator=None,
1334 order="desc",
1335 limit=None,
1336 for_update=False,
1337 debug=False,
1338 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301339 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301340):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301341 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301342 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1343 operator
1344 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301345 if previous_sle.get("warehouse"):
1346 conditions += " and warehouse = %(warehouse)s"
1347 elif previous_sle.get("warehouse_condition"):
1348 conditions += " and " + previous_sle.get("warehouse_condition")
1349
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301350 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301351 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1352 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301353 conditions += (
1354 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301355 (
1356 serial_no = {0}
1357 or serial_no like {1}
1358 or serial_no like {2}
1359 or serial_no like {3}
1360 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301361 """
1362 ).format(
1363 frappe.db.escape(serial_no),
1364 frappe.db.escape("{}\n%".format(serial_no)),
1365 frappe.db.escape("%\n{}".format(serial_no)),
1366 frappe.db.escape("%\n{}\n%".format(serial_no)),
1367 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301368
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301369 if not previous_sle.get("posting_date"):
1370 previous_sle["posting_date"] = "1900-01-01"
1371 if not previous_sle.get("posting_time"):
1372 previous_sle["posting_time"] = "00:00"
1373
1374 if operator in (">", "<=") and previous_sle.get("name"):
1375 conditions += " and name!=%(name)s"
1376
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301377 if extra_cond:
1378 conditions += f"{extra_cond}"
1379
Ankush Menat494bd9e2022-03-28 18:52:46 +05301380 return frappe.db.sql(
1381 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301382 select *, timestamp(posting_date, posting_time) as "timestamp"
1383 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301384 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301385 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301386 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301387 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301388 %(limit)s %(for_update)s"""
1389 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301390 "conditions": conditions,
1391 "limit": limit or "",
1392 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301393 "order": order,
1394 },
1395 previous_sle,
1396 as_dict=1,
1397 debug=debug,
1398 )
1399
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301400
Nabin Haita77b8c92020-12-21 14:45:50 +05301401def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301402 return frappe.db.get_value(
1403 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301404 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301405 [
1406 "item_code",
1407 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301408 "actual_qty",
1409 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301410 "posting_date",
1411 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301412 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301413 "timestamp(posting_date, posting_time) as timestamp",
1414 ],
1415 as_dict=1,
1416 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301417
Ankush Menatce0514c2022-02-15 11:41:41 +05301418
Ankush Menat494bd9e2022-03-28 18:52:46 +05301419def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301420 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301421):
1422
Ankush Menat102fff22022-02-19 15:51:04 +05301423 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301424 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301425
Ankush Menate1c16872022-04-21 20:01:48 +05301426 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301427 posting_date, posting_time
1428 )
Ankush Menat102fff22022-02-19 15:51:04 +05301429 if creation:
1430 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301431 CombineDatetime(sle.posting_date, sle.posting_time)
1432 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301433 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301434
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301435 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301436 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301437 )
1438
Ankush Menat102fff22022-02-19 15:51:04 +05301439 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301440 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301441 .inner_join(batch_ledger)
1442 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1443 .select(
1444 Sum(
1445 Case()
1446 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1447 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1448 ).as_("batch_value"),
1449 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1450 "batch_qty"
1451 ),
1452 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301453 .where(
1454 (sle.item_code == item_code)
1455 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301456 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301457 & (sle.is_cancelled == 0)
1458 )
1459 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301460 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301461
1462 if batch_details and batch_details[0].batch_qty:
1463 return batch_details[0].batch_value / batch_details[0].batch_qty
1464
1465
Ankush Menat494bd9e2022-03-28 18:52:46 +05301466def get_valuation_rate(
1467 item_code,
1468 warehouse,
1469 voucher_type,
1470 voucher_no,
1471 allow_zero_rate=False,
1472 currency=None,
1473 company=None,
1474 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301475 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301476 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301477):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301478
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301479 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1480
Ankush Menatf7ffe042021-11-01 13:21:14 +05301481 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301482 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301483
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301484 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1485 table = frappe.qb.DocType("Stock Ledger Entry")
1486 query = (
1487 frappe.qb.from_(table)
1488 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1489 .where(
1490 (table.item_code == item_code)
1491 & (table.warehouse == warehouse)
1492 & (table.batch_no == batch_no)
1493 & (table.is_cancelled == 0)
1494 & (table.voucher_no != voucher_no)
1495 & (table.voucher_type != voucher_type)
1496 )
1497 )
1498
1499 last_valuation_rate = query.run()
1500 if last_valuation_rate:
1501 return flt(last_valuation_rate[0][0])
1502
Ankush Menat342d09a2022-02-19 14:28:51 +05301503 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301504 if warehouse and serial_and_batch_bundle:
1505 batch_obj = BatchNoValuation(
1506 sle=frappe._dict(
1507 {
1508 "item_code": item_code,
1509 "warehouse": warehouse,
1510 "actual_qty": -1,
1511 "serial_and_batch_bundle": serial_and_batch_bundle,
1512 }
1513 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301514 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301515
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301516 return batch_obj.get_incoming_rate()
1517
Ankush Menatf7ffe042021-11-01 13:21:14 +05301518 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301519 if last_valuation_rate := frappe.db.sql(
1520 """select valuation_rate
1521 from `tabStock Ledger Entry` force index (item_warehouse)
1522 where
1523 item_code = %s
1524 AND warehouse = %s
1525 AND valuation_rate >= 0
1526 AND is_cancelled = 0
1527 AND NOT (voucher_no = %s AND voucher_type = %s)
1528 order by posting_date desc, posting_time desc, name desc limit 1""",
1529 (item_code, warehouse, voucher_no, voucher_type),
1530 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301531 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301532
1533 # If negative stock allowed, and item delivered without any incoming entry,
1534 # system does not found any SLE, then take valuation rate from Item
1535 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301536
1537 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301538 # try Item Standard rate
1539 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301540
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301541 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301542 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301543 valuation_rate = frappe.db.get_value(
1544 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1545 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301546
Ankush Menat494bd9e2022-03-28 18:52:46 +05301547 if (
1548 not allow_zero_rate
1549 and not valuation_rate
1550 and raise_error_if_no_rate
1551 and cint(erpnext.is_perpetual_inventory_enabled(company))
1552 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301553 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301554
Ankush Menat494bd9e2022-03-28 18:52:46 +05301555 message = _(
1556 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1557 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301558 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301559 solutions = (
1560 "<li>"
1561 + _(
1562 "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."
1563 ).format(voucher_type)
1564 + "</li>"
1565 )
1566 solutions += (
1567 "<li>"
1568 + _("If not, you can Cancel / Submit this entry")
1569 + " {0} ".format(frappe.bold("after"))
1570 + _("performing either one below:")
1571 + "</li>"
1572 )
Marica97715f22020-05-11 20:45:37 +05301573 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1574 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1575 msg = message + solutions + sub_solutions + "</li>"
1576
1577 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301578
1579 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301580
Ankush Menat494bd9e2022-03-28 18:52:46 +05301581
Ankush Menate7109c12021-08-26 16:40:45 +05301582def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301583 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301584 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301585 qty_shift = args.actual_qty
1586
Ankush Menat7c839c42022-05-06 12:09:08 +05301587 args["time_format"] = "%H:%i:%s"
1588
marination8418c4b2021-06-22 21:35:25 +05301589 # find difference/shift in qty caused by stock reconciliation
1590 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301591 qty_shift = get_stock_reco_qty_shift(args)
1592
1593 # find the next nearest stock reco so that we only recalculate SLEs till that point
1594 next_stock_reco_detail = get_next_stock_reco(args)
1595 if next_stock_reco_detail:
1596 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301597
marination40389772021-07-02 17:13:45 +05301598 # add condition to update SLEs before this date & time
1599 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301600
Ankush Menat494bd9e2022-03-28 18:52:46 +05301601 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301602 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301603 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301604 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301605 where
1606 item_code = %(item_code)s
1607 and warehouse = %(warehouse)s
1608 and voucher_no != %(voucher_no)s
1609 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301610 and (
1611 posting_date > %(posting_date)s or
1612 (
1613 posting_date = %(posting_date)s and
1614 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1615 )
1616 )
marination40389772021-07-02 17:13:45 +05301617 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301618 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301619 args,
1620 )
Nabin Hait186a0452021-02-18 14:14:21 +05301621
1622 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1623
Ankush Menat494bd9e2022-03-28 18:52:46 +05301624
marination40389772021-07-02 17:13:45 +05301625def get_stock_reco_qty_shift(args):
1626 stock_reco_qty_shift = 0
1627 if args.get("is_cancelled"):
1628 if args.get("previous_qty_after_transaction"):
1629 # get qty (balance) that was set at submission
1630 last_balance = args.get("previous_qty_after_transaction")
1631 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1632 else:
1633 stock_reco_qty_shift = flt(args.actual_qty)
1634 else:
1635 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301636 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301637 "qty_after_transaction"
1638 )
marination40389772021-07-02 17:13:45 +05301639
1640 if last_balance is not None:
1641 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1642 else:
1643 stock_reco_qty_shift = args.qty_after_transaction
1644
1645 return stock_reco_qty_shift
1646
Ankush Menat494bd9e2022-03-28 18:52:46 +05301647
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301648def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301649 """Returns next nearest stock reconciliaton's details."""
1650
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301651 sle = frappe.qb.DocType("Stock Ledger Entry")
1652
1653 query = (
1654 frappe.qb.from_(sle)
1655 .select(
1656 sle.name,
1657 sle.posting_date,
1658 sle.posting_time,
1659 sle.creation,
1660 sle.voucher_no,
1661 sle.item_code,
1662 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301663 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301664 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301665 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301666 )
1667 .where(
1668 (sle.item_code == kwargs.get("item_code"))
1669 & (sle.warehouse == kwargs.get("warehouse"))
1670 & (sle.voucher_type == "Stock Reconciliation")
1671 & (sle.voucher_no != kwargs.get("voucher_no"))
1672 & (sle.is_cancelled == 0)
1673 & (
1674 (
1675 CombineDatetime(sle.posting_date, sle.posting_time)
1676 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301677 )
1678 | (
1679 (
1680 CombineDatetime(sle.posting_date, sle.posting_time)
1681 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301682 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301683 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301684 )
1685 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301686 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301687 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1688 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301689 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301690 )
1691
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301692 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301693 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301694
1695 return query.run(as_dict=True)
1696
marination40389772021-07-02 17:13:45 +05301697
1698def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301699 return f"""
1700 and
1701 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1702 or (
1703 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1704 and creation < '{detail.creation}'
1705 )
1706 )"""
1707
Ankush Menat494bd9e2022-03-28 18:52:46 +05301708
Ankush Menate7109c12021-08-26 16:40:45 +05301709def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301710 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301711 return
1712 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1713 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301714
Ankush Menat5eba5752021-12-07 23:03:52 +05301715 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301716
1717 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301718 message = _(
1719 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1720 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301721 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301722 frappe.get_desk_link("Item", args.item_code),
1723 frappe.get_desk_link("Warehouse", args.warehouse),
1724 neg_sle[0]["posting_date"],
1725 neg_sle[0]["posting_time"],
1726 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1727 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301728
Ankush Menat494bd9e2022-03-28 18:52:46 +05301729 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301730
s-aga-rd9e28432023-10-27 16:35:35 +05301731 if args.batch_no:
1732 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1733 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1734 message = _(
1735 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1736 ).format(
1737 abs(neg_batch_sle[0]["cumulative_total"]),
1738 frappe.get_desk_link("Batch", args.batch_no),
1739 frappe.get_desk_link("Warehouse", args.warehouse),
1740 neg_batch_sle[0]["posting_date"],
1741 neg_batch_sle[0]["posting_time"],
1742 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1743 )
1744 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301745
s-aga-r73b65ac2023-11-01 18:35:07 +05301746 if args.reserved_stock:
1747 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301748
Nabin Haita77b8c92020-12-21 14:45:50 +05301749
Maricad6078aa2022-06-17 15:13:13 +05301750def is_negative_with_precision(neg_sle, is_batch=False):
1751 """
1752 Returns whether system precision rounded qty is insufficient.
1753 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1754 """
1755
1756 if not neg_sle:
1757 return False
1758
1759 field = "cumulative_total" if is_batch else "qty_after_transaction"
1760 precision = cint(frappe.db.get_default("float_precision")) or 2
1761 qty_deficit = flt(neg_sle[0][field], precision)
1762
1763 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1764
1765
Nabin Haita77b8c92020-12-21 14:45:50 +05301766def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301767 return frappe.db.sql(
1768 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301769 select
1770 qty_after_transaction, posting_date, posting_time,
1771 voucher_type, voucher_no
1772 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301773 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301774 item_code = %(item_code)s
1775 and warehouse = %(warehouse)s
1776 and voucher_no != %(voucher_no)s
1777 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1778 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301779 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301780 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301781 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301782 """,
1783 args,
1784 as_dict=1,
1785 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301786
Ankush Menat5eba5752021-12-07 23:03:52 +05301787
1788def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301789 return frappe.db.sql(
1790 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301791 with batch_ledger as (
1792 select
1793 posting_date, posting_time, voucher_type, voucher_no,
1794 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1795 from `tabStock Ledger Entry`
1796 where
1797 item_code = %(item_code)s
1798 and warehouse = %(warehouse)s
1799 and batch_no=%(batch_no)s
1800 and is_cancelled = 0
1801 order by posting_date, posting_time, creation
1802 )
1803 select * from batch_ledger
1804 where
1805 cumulative_total < 0.0
1806 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1807 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301808 """,
1809 args,
1810 as_dict=1,
1811 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301812
1813
s-aga-rd9e28432023-10-27 16:35:35 +05301814def validate_reserved_stock(kwargs):
1815 if kwargs.serial_no:
1816 serial_nos = kwargs.serial_no.split("\n")
1817 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1818
s-aga-re1a87a82023-10-31 18:41:58 +05301819 elif kwargs.batch_no:
1820 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1821
s-aga-rd9e28432023-10-27 16:35:35 +05301822 elif kwargs.serial_and_batch_bundle:
1823 sbb_entries = frappe.db.get_all(
1824 "Serial and Batch Entry",
1825 {
1826 "parenttype": "Serial and Batch Bundle",
1827 "parent": kwargs.serial_and_batch_bundle,
1828 "docstatus": 1,
1829 },
s-aga-re1a87a82023-10-31 18:41:58 +05301830 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301831 )
s-aga-rd9e28432023-10-27 16:35:35 +05301832
s-aga-re1a87a82023-10-31 18:41:58 +05301833 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301834 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301835 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1836 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301837
s-aga-r92317062023-11-02 10:36:00 +05301838 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1839 precision = cint(frappe.db.get_default("float_precision")) or 2
1840 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301841
s-aga-r92317062023-11-02 10:36:00 +05301842 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1843 if diff < 0 and abs(diff) > 0.0001:
1844 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1845 abs(diff),
1846 frappe.get_desk_link("Item", kwargs.item_code),
1847 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1848 nowdate(),
1849 nowtime(),
1850 )
1851 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301852
s-aga-rd9e28432023-10-27 16:35:35 +05301853
1854def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1855 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1856 item_code, warehouse, serial_nos
1857 ):
1858 if common_serial_nos := list(
1859 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1860 ):
1861 msg = _(
1862 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1863 )
1864 msg += "<br />"
1865 msg += _("Example: Serial No {0} reserved in {1}.").format(
1866 frappe.bold(common_serial_nos[0]),
1867 frappe.get_desk_link(
1868 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1869 ),
1870 )
1871 frappe.throw(msg, title=_("Reserved Serial No."))
1872
1873
s-aga-re1a87a82023-10-31 18:41:58 +05301874def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1875 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1876 available_batches = get_available_batches(
1877 frappe._dict(
1878 {
1879 "item_code": item_code,
1880 "warehouse": warehouse,
1881 "posting_date": nowdate(),
1882 "posting_time": nowtime(),
1883 }
1884 )
1885 )
1886 available_batches_map = {row.batch_no: row.qty for row in available_batches}
1887 precision = cint(frappe.db.get_default("float_precision")) or 2
1888
1889 for batch_no in batch_nos:
1890 diff = flt(
1891 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
1892 )
1893 if diff < 0 and abs(diff) > 0.0001:
1894 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1895 abs(diff),
1896 frappe.get_desk_link("Batch", batch_no),
1897 frappe.get_desk_link("Warehouse", warehouse),
1898 nowdate(),
1899 nowtime(),
1900 )
1901 frappe.throw(msg, title=_("Reserved Stock for Batch"))
1902
1903
Ankush Menateb8b4242022-02-12 13:08:28 +05301904def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1905 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1906 return True
1907 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1908 return True
1909 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301910
1911
1912def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1913 """
1914 For inter company transfer, incoming rate is the average of the outgoing rate
1915 """
1916 rate = 0.0
1917
1918 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1919
1920 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1921
1922 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1923
1924 if reference_name:
1925 rate = frappe.get_cached_value(
1926 doctype,
1927 reference_name,
1928 "incoming_rate",
1929 )
1930
1931 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301932
1933
1934def is_internal_transfer(sle):
1935 data = frappe.get_cached_value(
1936 sle.voucher_type,
1937 sle.voucher_no,
1938 ["is_internal_supplier", "represents_company", "company"],
1939 as_dict=True,
1940 )
1941
1942 if data.is_internal_supplier and data.represents_company == data.company:
1943 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05301944
1945
1946def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
1947 table = frappe.qb.DocType("Stock Ledger Entry")
1948
1949 query = (
1950 frappe.qb.from_(table)
1951 .select(Sum(table.stock_value_difference).as_("value"))
1952 .where(
1953 (table.is_cancelled == 0)
1954 & (table.item_code == item_code)
1955 & (table.warehouse == warehouse)
1956 & (
1957 (table.posting_date < posting_date)
1958 | ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
1959 )
1960 )
1961 )
1962
1963 if voucher_no:
1964 query = query.where(table.voucher_no != voucher_no)
1965
1966 difference_amount = query.run()
1967 return flt(difference_amount[0][0]) if difference_amount else 0