blob: a6206ac8dcf8c6c08fc4cd91e2fafe19d38a5c9b [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,
mergify[bot]07175362023-12-21 14:40:52 +053028 get_incoming_rate,
Deepesh Garg6f107da2021-10-12 20:15:55 +053029 get_or_make_bin,
s-aga-r73b65ac2023-11-01 18:35:07 +053030 get_stock_balance,
Chillar Anand915b3432021-09-02 16:44:59 +053031 get_valuation_method,
32)
Ankush Menatb534fee2022-02-19 20:58:36 +053033from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053034
Nabin Hait97bce3a2021-07-12 13:24:43 +053035
Ankush Menat494bd9e2022-03-28 18:52:46 +053036class NegativeStockError(frappe.ValidationError):
37 pass
38
39
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053040class SerialNoExistsInFutureTransaction(frappe.ValidationError):
41 pass
Nabin Hait902e8602013-01-08 18:29:24 +053042
Anand Doshi5b004ff2013-09-25 19:55:41 +053043
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053044def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053045 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053046
Ankush Menat494bd9e2022-03-28 18:52:46 +053047 args:
48 - allow_negative_stock: disable negative stock valiations if true
49 - via_landed_cost_voucher: landed cost voucher cancels and reposts
50 entries of purchase document. This flag is used to identify if
51 cancellation and repost is happening via landed cost voucher, in
52 such cases certain validations need to be ignored (like negative
53 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053054 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053055 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053056
Nabin Haitca775742013-09-26 16:16:44 +053057 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053058 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053059 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053060 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053061 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053062
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053063 args = get_args_for_future_sle(sl_entries[0])
64 future_sle_exists(args, sl_entries)
65
Nabin Haitca775742013-09-26 16:16:44 +053066 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053067 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053068 validate_serial_no(sle)
69
Nabin Haita77b8c92020-12-21 14:45:50 +053070 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053071 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053072
Ankush Menat494bd9e2022-03-28 18:52:46 +053073 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
74 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
75 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
76 )
77 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053078
Ankush Menat494bd9e2022-03-28 18:52:46 +053079 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
80 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
81 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
82 )
83 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053084
Ankush Menat494bd9e2022-03-28 18:52:46 +053085 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053086 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053087
Nabin Haita77b8c92020-12-21 14:45:50 +053088 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053089
90 if sle.get("voucher_type") == "Stock Reconciliation":
91 # preserve previous_qty_after_transaction for qty reposting
92 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
93
Ankush Menat494bd9e2022-03-28 18:52:46 +053094 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053095 if is_stock_item:
96 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
s-aga-r73b65ac2023-11-01 18:35:07 +053097 args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
Ankush Menatcef84c22021-12-03 12:18:59 +053098 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053099 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +0530100 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530101 frappe.msgprint(
102 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
103 )
104
Ankush Menatcef84c22021-12-03 12:18:59 +0530105
106def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
107 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
108 if not args.get("posting_date"):
109 args["posting_date"] = nowdate()
110
marination7a5fd712022-07-04 17:46:54 +0530111 if not (args.get("is_cancelled") and via_landed_cost_voucher):
112 # Reposts only current voucher SL Entries
113 # Updates valuation rate, stock value, stock queue for current transaction
114 update_entries_after(
115 {
116 "item_code": args.get("item_code"),
117 "warehouse": args.get("warehouse"),
118 "posting_date": args.get("posting_date"),
119 "posting_time": args.get("posting_time"),
120 "voucher_type": args.get("voucher_type"),
121 "voucher_no": args.get("voucher_no"),
122 "sle_id": args.get("name"),
123 "creation": args.get("creation"),
s-aga-r73b65ac2023-11-01 18:35:07 +0530124 "reserved_stock": args.get("reserved_stock"),
marination7a5fd712022-07-04 17:46:54 +0530125 },
126 allow_negative_stock=allow_negative_stock,
127 via_landed_cost_voucher=via_landed_cost_voucher,
128 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530129
130 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530131 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530132 update_qty_in_future_sle(args, allow_negative_stock)
133
Nabin Haitadeb9762014-10-06 11:53:52 +0530134
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530135def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530136 return frappe._dict(
137 {
138 "voucher_type": row.get("voucher_type"),
139 "voucher_no": row.get("voucher_no"),
140 "posting_date": row.get("posting_date"),
141 "posting_time": row.get("posting_time"),
142 }
143 )
144
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530145
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530146def validate_serial_no(sle):
147 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530148
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530149 for sn in get_serial_nos(sle.serial_no):
150 args = copy.deepcopy(sle)
151 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530152 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530153
154 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530155 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530156 voucher_type = frappe.bold(row.voucher_type)
157 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530158 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530159
160 if vouchers:
161 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530162 msg = (
163 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
164 The list of the transactions are as below."""
165 + "<br><br><ul><li>"
166 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530167
Ankush Menat494bd9e2022-03-28 18:52:46 +0530168 msg += "</li><li>".join(vouchers)
169 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530170
Ankush Menat494bd9e2022-03-28 18:52:46 +0530171 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530172 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
173
Ankush Menat494bd9e2022-03-28 18:52:46 +0530174
Nabin Hait186a0452021-02-18 14:14:21 +0530175def validate_cancellation(args):
176 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530177 repost_entry = frappe.db.get_value(
178 "Repost Item Valuation",
179 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
180 ["name", "status"],
181 as_dict=1,
182 )
Nabin Hait186a0452021-02-18 14:14:21 +0530183
184 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530185 if repost_entry.status == "In Progress":
186 frappe.throw(
187 _(
188 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
189 )
190 )
191 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530192 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530193 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530194 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530195 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530196
Ankush Menat494bd9e2022-03-28 18:52:46 +0530197
Nabin Hait9653f602013-08-20 15:37:33 +0530198def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530199 frappe.db.sql(
200 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530201 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530202 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530203 (now(), frappe.session.user, voucher_type, voucher_no),
204 )
205
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530206
Nabin Hait54c865e2015-03-27 15:38:31 +0530207def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530208 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530209 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530210 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530211 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530212 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530213 sle.submit()
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530214
215 # Added to handle the case when the stock ledger entry is created from the repostig
216 if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
217 sle.db_set("creation", args.get("creation_time"))
218
Nabin Haita77b8c92020-12-21 14:45:50 +0530219 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530220
Ankush Menat494bd9e2022-03-28 18:52:46 +0530221
222def repost_future_sle(
223 args=None,
224 voucher_type=None,
225 voucher_no=None,
226 allow_negative_stock=None,
227 via_landed_cost_voucher=False,
228 doc=None,
229):
Nabin Haite1fa7232022-07-20 15:19:09 +0530230 if not args:
231 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530232
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530233 reposting_data = {}
234 if doc and doc.reposting_data_file:
235 reposting_data = get_reposting_data(doc.reposting_data_file)
236
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530237 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530238 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530239 )
240 if items_to_be_repost:
241 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530242
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530243 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
244 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530245
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530246 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530247 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530248 validate_item_warehouse(args[i])
249
Ankush Menat494bd9e2022-03-28 18:52:46 +0530250 obj = update_entries_after(
251 {
252 "item_code": args[i].get("item_code"),
253 "warehouse": args[i].get("warehouse"),
254 "posting_date": args[i].get("posting_date"),
255 "posting_time": args[i].get("posting_time"),
256 "creation": args[i].get("creation"),
257 "distinct_item_warehouses": distinct_item_warehouses,
258 },
259 allow_negative_stock=allow_negative_stock,
260 via_landed_cost_voucher=via_landed_cost_voucher,
261 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530262 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530263
Ankush Menat494bd9e2022-03-28 18:52:46 +0530264 distinct_item_warehouses[
265 (args[i].get("item_code"), args[i].get("warehouse"))
266 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530267
Nabin Hait97bce3a2021-07-12 13:24:43 +0530268 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530269 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530270 if ("args_idx" not in data and not data.reposting_status) or (
271 data.sle_changed and data.reposting_status
272 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530273 data.args_idx = len(args)
274 args.append(data.sle)
275 elif data.sle_changed and not data.reposting_status:
276 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530277
Nabin Hait97bce3a2021-07-12 13:24:43 +0530278 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530279 i += 1
280
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530281 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530282 update_args_in_repost_item_valuation(
283 doc, i, args, distinct_item_warehouses, affected_transactions
284 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530285
Ankush Menat494bd9e2022-03-28 18:52:46 +0530286
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530287def get_reposting_data(file_path) -> dict:
288 file_name = frappe.db.get_value(
289 "File",
290 {
291 "file_url": file_path,
292 "attached_to_field": "reposting_data_file",
293 },
294 "name",
295 )
296
297 if not file_name:
298 return frappe._dict()
299
300 attached_file = frappe.get_doc("File", file_name)
301
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530302 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530303 if data := json.loads(data.decode("utf-8")):
304 data = data
305
306 return parse_json(data)
307
308
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530309def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530310 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530311 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530312 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530313 frappe.throw(_(validation_msg))
314
Ankush Menat494bd9e2022-03-28 18:52:46 +0530315
Ankush Menatecdb4932022-04-17 19:06:13 +0530316def update_args_in_repost_item_valuation(
317 doc, index, args, distinct_item_warehouses, affected_transactions
318):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530319 if not doc.items_to_be_repost:
320 file_name = ""
321 if doc.reposting_data_file:
322 file_name = get_reposting_file_name(doc.doctype, doc.name)
323 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
324
325 doc.reposting_data_file = create_json_gz_file(
326 {
327 "items_to_be_repost": args,
328 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
329 "affected_transactions": affected_transactions,
330 },
331 doc,
332 file_name,
333 )
334
335 doc.db_set(
336 {
337 "current_index": index,
338 "total_reposting_count": len(args),
339 "reposting_data_file": doc.reposting_data_file,
340 }
341 )
342
343 else:
344 doc.db_set(
345 {
346 "items_to_be_repost": json.dumps(args, default=str),
347 "distinct_item_and_warehouse": json.dumps(
348 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
349 ),
350 "current_index": index,
351 "affected_transactions": frappe.as_json(affected_transactions),
352 }
353 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530354
Ankush Menatecdb4932022-04-17 19:06:13 +0530355 if not frappe.flags.in_test:
356 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530357
Ankush Menat494bd9e2022-03-28 18:52:46 +0530358 frappe.publish_realtime(
359 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530360 {
361 "name": doc.name,
362 "items_to_be_repost": json.dumps(args, default=str),
363 "current_index": index,
364 "total_reposting_count": len(args),
365 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530366 doctype=doc.doctype,
367 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530368 )
369
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530370
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530371def get_reposting_file_name(dt, dn):
372 return frappe.db.get_value(
373 "File",
374 {
375 "attached_to_doctype": dt,
376 "attached_to_name": dn,
377 "attached_to_field": "reposting_data_file",
378 },
379 "name",
380 )
381
382
383def create_json_gz_file(data, doc, file_name=None) -> str:
384 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530385 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530386
387 if not file_name:
388 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
389 _file = frappe.get_doc(
390 {
391 "doctype": "File",
392 "file_name": json_filename,
393 "attached_to_doctype": doc.doctype,
394 "attached_to_name": doc.name,
395 "attached_to_field": "reposting_data_file",
396 "content": compressed_content,
397 "is_private": 1,
398 }
399 )
400 _file.save(ignore_permissions=True)
401
402 return _file.file_url
403 else:
404 file_doc = frappe.get_doc("File", file_name)
405 path = file_doc.get_full_path()
406
407 with open(path, "wb") as f:
408 f.write(compressed_content)
409
410 return doc.reposting_data_file
411
412
413def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
414 if not reposting_data and doc and doc.reposting_data_file:
415 reposting_data = get_reposting_data(doc.reposting_data_file)
416
417 if reposting_data and reposting_data.items_to_be_repost:
418 return reposting_data.items_to_be_repost
419
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530420 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530421
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530422 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530423 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530424
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530425 if not items_to_be_repost and voucher_type and voucher_no:
426 items_to_be_repost = frappe.db.get_all(
427 "Stock Ledger Entry",
428 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
429 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
430 order_by="creation asc",
431 group_by="item_code, warehouse",
432 )
433
Nabin Haite1fa7232022-07-20 15:19:09 +0530434 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530435
Ankush Menat494bd9e2022-03-28 18:52:46 +0530436
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530437def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
438 if not reposting_data and doc and doc.reposting_data_file:
439 reposting_data = get_reposting_data(doc.reposting_data_file)
440
441 if reposting_data and reposting_data.distinct_item_and_warehouse:
442 return reposting_data.distinct_item_and_warehouse
443
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530444 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530445
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530446 if doc and doc.distinct_item_and_warehouse:
447 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530448 distinct_item_warehouses = {
449 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
450 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530451 else:
452 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530453 distinct_item_warehouses.setdefault(
454 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
455 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530456
457 return distinct_item_warehouses
458
Ankush Menat494bd9e2022-03-28 18:52:46 +0530459
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530460def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
461 if not reposting_data and doc and doc.reposting_data_file:
462 reposting_data = get_reposting_data(doc.reposting_data_file)
463
464 if reposting_data and reposting_data.affected_transactions:
465 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
466
Ankush Menatecdb4932022-04-17 19:06:13 +0530467 if not doc.affected_transactions:
468 return set()
469
470 transactions = frappe.parse_json(doc.affected_transactions)
471 return {tuple(transaction) for transaction in transactions}
472
473
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530474def get_current_index(doc=None):
475 if doc and doc.current_index:
476 return doc.current_index
477
Ankush Menat494bd9e2022-03-28 18:52:46 +0530478
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530479class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530480 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530481 update valution rate and qty after transaction
482 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530483
Ankush Menat494bd9e2022-03-28 18:52:46 +0530484 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530485
Ankush Menat494bd9e2022-03-28 18:52:46 +0530486 args = {
487 "item_code": "ABC",
488 "warehouse": "XYZ",
489 "posting_date": "2012-12-12",
490 "posting_time": "12:00"
491 }
Nabin Hait902e8602013-01-08 18:29:24 +0530492 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530493
494 def __init__(
495 self,
496 args,
497 allow_zero_rate=False,
498 allow_negative_stock=None,
499 via_landed_cost_voucher=False,
500 verbose=1,
501 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530502 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530503 self.verbose = verbose
504 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530505 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530506 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530507 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
508 item_code=self.item_code
509 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530510
Nabin Haita77b8c92020-12-21 14:45:50 +0530511 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530512 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530513 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530514
Nabin Haita77b8c92020-12-21 14:45:50 +0530515 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530516 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530517 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530518
519 self.new_items_found = False
520 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530521 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-r73b65ac2023-11-01 18:35:07 +0530522 self.reserved_stock = flt(self.args.reserved_stock)
Nabin Haita77b8c92020-12-21 14:45:50 +0530523
524 self.data = frappe._dict()
525 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530526 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530527
Maricad6078aa2022-06-17 15:13:13 +0530528 def set_precision(self):
529 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
530 self.currency_precision = get_field_precision(
531 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530532 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530533
534 def initialize_previous_data(self, args):
535 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530536 Get previous sl entries for current item for each related warehouse
537 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530538
Ankush Menat494bd9e2022-03-28 18:52:46 +0530539 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530540
Ankush Menat494bd9e2022-03-28 18:52:46 +0530541 self.data = {
542 warehouse1: {
543 'previus_sle': {},
544 'qty_after_transaction': 10,
545 'valuation_rate': 100,
546 'stock_value': 1000,
547 'prev_stock_value': 1000,
548 'stock_queue': '[[10, 100]]',
549 'stock_value_difference': 1000
550 }
551 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530552
553 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530554 self.data.setdefault(args.warehouse, frappe._dict())
555 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530556 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530557 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530558
Ankush Menatc1d986a2021-08-31 19:43:42 +0530559 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
560 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
561
Ankush Menat494bd9e2022-03-28 18:52:46 +0530562 warehouse_dict.update(
563 {
564 "prev_stock_value": previous_sle.stock_value or 0.0,
565 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
566 "stock_value_difference": 0.0,
567 }
568 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530569
Nabin Haita77b8c92020-12-21 14:45:50 +0530570 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530571 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530572
Nabin Haita77b8c92020-12-21 14:45:50 +0530573 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530574 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530575 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530576 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530577 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530578 entries_to_fix = self.get_future_entries_to_fix()
579
580 i = 0
581 while i < len(entries_to_fix):
582 sle = entries_to_fix[i]
583 i += 1
584
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530585 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530586 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530587
Nabin Haita77b8c92020-12-21 14:45:50 +0530588 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530589 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530590
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530591 if self.exceptions:
592 self.raise_exceptions()
593
Nabin Hait186a0452021-02-18 14:14:21 +0530594 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530595 sl_entries = self.get_sle_against_current_voucher()
596 for sle in sl_entries:
597 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530598
Nabin Haita77b8c92020-12-21 14:45:50 +0530599 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530600 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530601
Ankush Menat494bd9e2022-03-28 18:52:46 +0530602 return frappe.db.sql(
603 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530604 select
605 *, timestamp(posting_date, posting_time) as "timestamp"
606 from
607 `tabStock Ledger Entry`
608 where
609 item_code = %(item_code)s
610 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530611 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530612 and (
613 posting_date = %(posting_date)s and
614 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
615 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530616 order by
617 creation ASC
618 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530619 """,
620 self.args,
621 as_dict=1,
622 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530623
Nabin Haita77b8c92020-12-21 14:45:50 +0530624 def get_future_entries_to_fix(self):
625 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530626 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
627 {"item_code": self.item_code, "warehouse": self.args.warehouse}
628 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530629
Nabin Haita77b8c92020-12-21 14:45:50 +0530630 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530631
Nabin Haita77b8c92020-12-21 14:45:50 +0530632 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530633 dependant_sle = get_sle_by_voucher_detail_no(
634 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
635 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530636
Nabin Haita77b8c92020-12-21 14:45:50 +0530637 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530638 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530639 elif (
640 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
641 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530642 return entries_to_fix
643 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530644 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530645 return entries_to_fix
646 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
647 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530648 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530649 self.initialize_previous_data(dependant_sle)
650 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530651 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530652
653 def update_distinct_item_warehouses(self, dependant_sle):
654 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530655 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530656
Nabin Hait97bce3a2021-07-12 13:24:43 +0530657 if key not in self.distinct_item_warehouses:
658 self.distinct_item_warehouses[key] = val
659 self.new_items_found = True
660 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530661 existing_sle_posting_date = (
662 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
663 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530664
665 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
666
Nabin Hait97bce3a2021-07-12 13:24:43 +0530667 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
668 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530669 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
670 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530671 self.distinct_item_warehouses[key] = val
672 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530673 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
674 # Future dependent voucher needs to be repost to get the correct stock value
675 # If dependent voucher has not reposted, then add it to the list
676 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530677 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530678 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
679 self.distinct_item_warehouses[key] = val
680
681 def get_dependent_voucher_detail_nos(self, key):
682 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
683 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
684
685 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530686
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530687 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530688 # previous sle data for this warehouse
689 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530690 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530691
Anand Doshi0dc79f42015-04-06 12:59:34 +0530692 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 +0530693 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530694 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530695 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530696 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530697 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530698
Nabin Haita77b8c92020-12-21 14:45:50 +0530699 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530700 if not self.args.get("sle_id"):
701 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530702
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530703 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530704 sle.voucher_type == "Stock Reconciliation"
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530705 and (
706 sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
707 )
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530708 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530709 and not self.args.get("sle_id")
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530710 ):
711 self.reset_actual_qty_for_stock_reco(sle)
712
713 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530714 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
715 and sle.voucher_detail_no
716 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530717 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530718 ):
719 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
720
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530721 dimensions = get_inventory_dimensions()
722 has_dimensions = False
723 if dimensions:
724 for dimension in dimensions:
725 if sle.get(dimension.get("fieldname")):
726 has_dimensions = True
727
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530728 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530729 self.calculate_valuation_for_serial_batch_bundle(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530730 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530731 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530732 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530733 self.wh_data.valuation_rate = sle.valuation_rate
734 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530735 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
736 self.wh_data.valuation_rate
737 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530738 if self.valuation_method != "Moving Average":
739 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530740 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530741 if self.valuation_method == "Moving Average":
742 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530743 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530744 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
745 self.wh_data.valuation_rate
746 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530747 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530748 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530749
Rushabh Mehta54047782013-12-26 11:07:46 +0530750 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530751 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530752 if not self.wh_data.qty_after_transaction:
753 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530754
Nabin Haita77b8c92020-12-21 14:45:50 +0530755 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
756 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530757
Nabin Hait902e8602013-01-08 18:29:24 +0530758 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530759 sle.qty_after_transaction = self.wh_data.qty_after_transaction
760 sle.valuation_rate = self.wh_data.valuation_rate
761 sle.stock_value = self.wh_data.stock_value
762 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530763
rohitwaghchaurea8216b92023-11-09 12:22:26 +0530764 if not sle.is_adjustment_entry or not self.args.get("sle_id"):
765 sle.stock_value_difference = stock_value_difference
766
767 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530768 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530769
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530770 if not self.args.get("sle_id") or (
771 sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
772 ):
Ankush Menat701878f2022-03-01 18:08:29 +0530773 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530774
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530775 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530776 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
777 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
778
779 if sle.actual_qty < 0:
780 sle.actual_qty = (
781 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
782 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530783 )
784
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530785 if abs(sle.actual_qty) == 0.0:
786 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530787
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530788 def calculate_valuation_for_serial_batch_bundle(self, sle):
789 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
790
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530791 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530792 doc.calculate_qty_and_amount(save=True)
793
794 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
795
796 self.wh_data.qty_after_transaction += doc.total_qty
797 if self.wh_data.qty_after_transaction:
798 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
799
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530800 def validate_negative_stock(self, sle):
801 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530802 validate negative stock for entries current datetime onwards
803 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530804 """
s-aga-rf0acb202023-04-12 14:13:54 +0530805 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530806 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530807
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530808 if diff < 0 and abs(diff) > 0.0001:
809 # negative stock!
810 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530811 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530812 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530813 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530814 return True
815
Nabin Haita77b8c92020-12-21 14:45:50 +0530816 def get_dynamic_incoming_outgoing_rate(self, sle):
817 # Get updated incoming/outgoing rate from transaction
818 if sle.recalculate_rate:
819 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
820
821 if flt(sle.actual_qty) >= 0:
822 sle.incoming_rate = rate
823 else:
824 sle.outgoing_rate = rate
825
826 def get_incoming_outgoing_rate_from_transaction(self, sle):
827 rate = 0
828 # Material Transfer, Repack, Manufacturing
829 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530830 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530831 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
832 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530833 elif sle.voucher_type in (
834 "Purchase Receipt",
835 "Purchase Invoice",
836 "Delivery Note",
837 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530838 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530839 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530840 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530841 from erpnext.controllers.sales_and_purchase_return import (
842 get_rate_for_return, # don't move this import to top
843 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530844
mergify[bot]07175362023-12-21 14:40:52 +0530845 if self.valuation_method == "Moving Average":
846 rate = get_incoming_rate(
847 {
848 "item_code": sle.item_code,
849 "warehouse": sle.warehouse,
850 "posting_date": sle.posting_date,
851 "posting_time": sle.posting_time,
852 "qty": sle.actual_qty,
853 "serial_no": sle.get("serial_no"),
854 "batch_no": sle.get("batch_no"),
855 "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
856 "company": sle.company,
857 "voucher_type": sle.voucher_type,
858 "voucher_no": sle.voucher_no,
859 "allow_zero_valuation": self.allow_zero_rate,
860 "sle": sle.name,
861 }
862 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530863
mergify[bot]07175362023-12-21 14:40:52 +0530864 else:
865 rate = get_rate_for_return(
866 sle.voucher_type,
867 sle.voucher_no,
868 sle.item_code,
869 voucher_detail_no=sle.voucher_detail_no,
870 sle=sle,
871 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530872 elif (
873 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530874 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530875 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530876 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530877 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530878 else:
879 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530880 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530881 elif sle.voucher_type == "Subcontracting Receipt":
882 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530883 else:
884 rate_field = "incoming_rate"
885
886 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530887 item_code, incoming_rate = frappe.db.get_value(
888 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
889 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530890
891 if item_code == sle.item_code:
892 rate = incoming_rate
893 else:
894 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
895 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530896 elif sle == "Subcontracting Receipt":
897 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530898 else:
899 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530900
Ankush Menat494bd9e2022-03-28 18:52:46 +0530901 rate = frappe.db.get_value(
902 ref_doctype,
903 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
904 rate_field,
905 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530906
907 return rate
908
909 def update_outgoing_rate_on_transaction(self, sle):
910 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530911 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
912 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530913 """
914 if sle.actual_qty and sle.voucher_detail_no:
915 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
916
917 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
918 self.update_rate_on_stock_entry(sle, outgoing_rate)
919 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
920 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
921 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
922 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530923 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
924 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530925 elif sle.voucher_type == "Stock Reconciliation":
926 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530927
928 def update_rate_on_stock_entry(self, sle, outgoing_rate):
929 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
930
Ankush Menat701878f2022-03-01 18:08:29 +0530931 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
932 if not sle.dependant_sle_voucher_detail_no:
933 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530934
935 def recalculate_amounts_in_stock_entry(self, voucher_no):
936 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530937 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
938 stock_entry.db_update()
939 for d in stock_entry.items:
940 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530941
Nabin Haita77b8c92020-12-21 14:45:50 +0530942 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
943 # Update item's incoming rate on transaction
944 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
945 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530946 frappe.db.set_value(
947 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
948 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530949 else:
950 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530951 frappe.db.set_value(
952 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530953 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530954 "incoming_rate",
955 outgoing_rate,
956 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530957
958 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
959 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530960 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
961 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
962 ):
963 frappe.db.set_value(
964 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
965 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530966 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530967 frappe.db.set_value(
968 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
969 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530970
971 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530972 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530973 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530974 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530975 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530976 d.db_update()
977
Sagar Sharma323bdf82022-05-17 15:14:07 +0530978 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530979 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
980 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530981 else:
982 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530983 "Subcontracting Receipt Supplied Item",
984 sle.voucher_detail_no,
985 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530986 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530987
s-aga-ra6cb6c62023-05-03 09:51:58 +0530988 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530989 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530990 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530991 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530992 d.db_update()
993
s-aga-r88a3f652023-05-30 16:54:28 +0530994 def update_rate_on_stock_reconciliation(self, sle):
995 if not sle.serial_no and not sle.batch_no:
996 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
997
998 for item in sr.items:
999 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +05301000 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +05301001 continue
1002
1003 previous_sle = get_previous_sle(
1004 {
1005 "item_code": item.item_code,
1006 "warehouse": item.warehouse,
1007 "posting_date": sr.posting_date,
1008 "posting_time": sr.posting_time,
1009 "sle": sle.name,
1010 }
1011 )
1012
1013 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
1014 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
1015 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
1016
1017 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +05301018 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +05301019 item.amount_difference = item.amount - item.current_amount
1020 else:
1021 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1022 sr.db_update()
1023
1024 for item in sr.items:
1025 item.db_update()
1026
Nabin Hait328c4f92020-01-02 19:00:32 +05301027 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1028 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301029 all_serial_nos = frappe.get_all(
1030 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1031 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301032
Ankush Menat494bd9e2022-03-28 18:52:46 +05301033 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 +05301034
1035 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301036 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301037 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301038 incoming_rate = frappe.db.sql(
1039 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301040 select incoming_rate
1041 from `tabStock Ledger Entry`
1042 where
1043 company = %s
1044 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301045 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301046 and (serial_no = %s
1047 or serial_no like %s
1048 or serial_no like %s
1049 or serial_no like %s
1050 )
1051 order by posting_date desc
1052 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301053 """,
1054 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1055 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301056
1057 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1058
1059 return incoming_values
1060
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301061 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301062 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301063 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301064 if new_stock_qty >= 0:
1065 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301066 if flt(self.wh_data.qty_after_transaction) <= 0:
1067 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301068 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301069 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1070 actual_qty * sle.incoming_rate
1071 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301072
Nabin Haita77b8c92020-12-21 14:45:50 +05301073 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301074
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301075 elif sle.outgoing_rate:
1076 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301077 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1078 actual_qty * sle.outgoing_rate
1079 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301080
Nabin Haita77b8c92020-12-21 14:45:50 +05301081 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301082 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301083 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301084 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301085 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1086 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301087
Nabin Haita77b8c92020-12-21 14:45:50 +05301088 if not self.wh_data.valuation_rate and actual_qty > 0:
1089 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301090
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301091 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001092 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301093 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301094 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1095 sle.voucher_type, sle.voucher_detail_no
1096 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001097 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301098 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301099
Ankush Menatf089d392022-02-02 12:51:21 +05301100 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301101 incoming_rate = flt(sle.incoming_rate)
1102 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301103 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301104
Ankush Menat494bd9e2022-03-28 18:52:46 +05301105 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1106 self.wh_data.qty_after_transaction + actual_qty
1107 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301108
Ankush Menat97e18a12022-01-15 17:42:25 +05301109 if self.valuation_method == "LIFO":
1110 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1111 else:
1112 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1113
Ankush Menatb534fee2022-02-19 20:58:36 +05301114 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1115
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301116 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301117 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301118 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301119
Ankush Menat4b29fb62021-12-18 18:40:22 +05301120 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301121 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1122 sle.voucher_type, sle.voucher_detail_no
1123 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301124 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301125 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301126 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301127 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301128
Ankush Menat494bd9e2022-03-28 18:52:46 +05301129 stock_queue.remove_stock(
1130 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1131 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301132
Ankush Menatb534fee2022-02-19 20:58:36 +05301133 _qty, stock_value = stock_queue.get_total_stock_and_value()
1134
1135 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301136
Ankush Menat97e18a12022-01-15 17:42:25 +05301137 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301138 self.wh_data.stock_value = round_off_if_near_zero(
1139 self.wh_data.stock_value + stock_value_difference
1140 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301141
Nabin Haita77b8c92020-12-21 14:45:50 +05301142 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301143 self.wh_data.stock_queue.append(
1144 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1145 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301146
Ankush Menatb534fee2022-02-19 20:58:36 +05301147 if self.wh_data.qty_after_transaction:
1148 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1149
Ankush Menatce0514c2022-02-15 11:41:41 +05301150 def update_batched_values(self, sle):
1151 incoming_rate = flt(sle.incoming_rate)
1152 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301153
Ankush Menat494bd9e2022-03-28 18:52:46 +05301154 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1155 self.wh_data.qty_after_transaction + actual_qty
1156 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301157
1158 if actual_qty > 0:
1159 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301160 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301161 outgoing_rate = get_batch_incoming_rate(
1162 item_code=sle.item_code,
1163 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301164 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301165 posting_date=sle.posting_date,
1166 posting_time=sle.posting_time,
1167 creation=sle.creation,
1168 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301169 if outgoing_rate is None:
1170 # This can *only* happen if qty available for the batch is zero.
1171 # in such case fall back various other rates.
1172 # future entries will correct the overall accounting as each
1173 # batch individually uses moving average rates.
1174 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301175 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301176
Ankush Menat494bd9e2022-03-28 18:52:46 +05301177 self.wh_data.stock_value = round_off_if_near_zero(
1178 self.wh_data.stock_value + stock_value_difference
1179 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301180 if self.wh_data.qty_after_transaction:
1181 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301182
Javier Wong9b11d9b2017-04-14 18:24:04 +08001183 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301184 ref_item_dt = ""
1185
1186 if voucher_type == "Stock Entry":
1187 ref_item_dt = voucher_type + " Detail"
1188 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1189 ref_item_dt = voucher_type + " Item"
1190
1191 if ref_item_dt:
1192 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1193 else:
1194 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301195
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301196 def get_fallback_rate(self, sle) -> float:
1197 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301198 This should only get used for negative stock."""
1199 return get_valuation_rate(
1200 sle.item_code,
1201 sle.warehouse,
1202 sle.voucher_type,
1203 sle.voucher_no,
1204 self.allow_zero_rate,
1205 currency=erpnext.get_company_currency(sle.company),
1206 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301207 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301208
Nabin Haita77b8c92020-12-21 14:45:50 +05301209 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301210 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301211 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1212 sle = sle[0] if sle else frappe._dict()
1213 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301214
Nabin Haita77b8c92020-12-21 14:45:50 +05301215 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301216 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301217 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301218
1219 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301220 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301221 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301222 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301223
Ankush Menat494bd9e2022-03-28 18:52:46 +05301224 if (
1225 exceptions[0]["voucher_type"],
1226 exceptions[0]["voucher_no"],
1227 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301228
Nabin Haita77b8c92020-12-21 14:45:50 +05301229 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301230 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301231 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1232 frappe.get_desk_link("Warehouse", warehouse),
1233 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301234 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301235 msg = _(
1236 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1237 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301238 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301239 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1240 frappe.get_desk_link("Warehouse", warehouse),
1241 exceptions[0]["posting_date"],
1242 exceptions[0]["posting_time"],
1243 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1244 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301245
Nabin Haita77b8c92020-12-21 14:45:50 +05301246 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301247 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301248 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301249
1250 if allowed_qty > 0:
1251 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1252 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1253 )
1254 else:
1255 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1256 msg,
1257 )
s-aga-rf0acb202023-04-12 14:13:54 +05301258
Nabin Haita77b8c92020-12-21 14:45:50 +05301259 msg_list.append(msg)
1260
1261 if msg_list:
1262 message = "\n\n".join(msg_list)
1263 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301264 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301265 else:
1266 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301267
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301268 def update_bin_data(self, sle):
1269 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301270 values_to_update = {
1271 "actual_qty": sle.qty_after_transaction,
1272 "stock_value": sle.stock_value,
1273 }
1274
1275 if sle.valuation_rate is not None:
1276 values_to_update["valuation_rate"] = sle.valuation_rate
1277
1278 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301279
Nabin Haita77b8c92020-12-21 14:45:50 +05301280 def update_bin(self):
1281 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301282 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301283 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301284
Ankush Menat494bd9e2022-03-28 18:52:46 +05301285 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301286 if data.valuation_rate is not None:
1287 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301288 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301289
marination8418c4b2021-06-22 21:35:25 +05301290
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301291def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301292 """get stock ledger entries filtered by specific posting datetime conditions"""
1293
Ankush Menat494bd9e2022-03-28 18:52:46 +05301294 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301295 if not args.get("posting_date"):
1296 args["posting_date"] = "1900-01-01"
1297 if not args.get("posting_time"):
1298 args["posting_time"] = "00:00"
1299
1300 voucher_condition = ""
1301 if exclude_current_voucher:
1302 voucher_no = args.get("voucher_no")
1303 voucher_condition = f"and voucher_no != '{voucher_no}'"
1304
Ankush Menat494bd9e2022-03-28 18:52:46 +05301305 sle = frappe.db.sql(
1306 """
marination8418c4b2021-06-22 21:35:25 +05301307 select *, timestamp(posting_date, posting_time) as "timestamp"
1308 from `tabStock Ledger Entry`
1309 where item_code = %(item_code)s
1310 and warehouse = %(warehouse)s
1311 and is_cancelled = 0
1312 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301313 and (
1314 posting_date < %(posting_date)s or
1315 (
1316 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301317 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301318 )
1319 )
marination8418c4b2021-06-22 21:35:25 +05301320 order by timestamp(posting_date, posting_time) desc, creation desc
1321 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301322 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301323 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301324 ),
1325 args,
1326 as_dict=1,
1327 )
marination8418c4b2021-06-22 21:35:25 +05301328
1329 return sle[0] if sle else frappe._dict()
1330
Ankush Menat494bd9e2022-03-28 18:52:46 +05301331
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301332def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301333 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301334 get the last sle on or before the current time-bucket,
1335 to get actual qty before transaction, this function
1336 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301337
Ankush Menat494bd9e2022-03-28 18:52:46 +05301338 args = {
1339 "item_code": "ABC",
1340 "warehouse": "XYZ",
1341 "posting_date": "2012-12-12",
1342 "posting_time": "12:00",
1343 "sle": "name of reference Stock Ledger Entry"
1344 }
Anand Doshi1b531862013-01-10 19:29:51 +05301345 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301346 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301347 sle = get_stock_ledger_entries(
1348 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1349 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301350 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301351
Ankush Menat494bd9e2022-03-28 18:52:46 +05301352
1353def get_stock_ledger_entries(
1354 previous_sle,
1355 operator=None,
1356 order="desc",
1357 limit=None,
1358 for_update=False,
1359 debug=False,
1360 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301361 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301362):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301363 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301364 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1365 operator
1366 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301367 if previous_sle.get("warehouse"):
1368 conditions += " and warehouse = %(warehouse)s"
1369 elif previous_sle.get("warehouse_condition"):
1370 conditions += " and " + previous_sle.get("warehouse_condition")
1371
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301372 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301373 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1374 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301375 conditions += (
1376 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301377 (
1378 serial_no = {0}
1379 or serial_no like {1}
1380 or serial_no like {2}
1381 or serial_no like {3}
1382 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301383 """
1384 ).format(
1385 frappe.db.escape(serial_no),
1386 frappe.db.escape("{}\n%".format(serial_no)),
1387 frappe.db.escape("%\n{}".format(serial_no)),
1388 frappe.db.escape("%\n{}\n%".format(serial_no)),
1389 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301390
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301391 if not previous_sle.get("posting_date"):
1392 previous_sle["posting_date"] = "1900-01-01"
1393 if not previous_sle.get("posting_time"):
1394 previous_sle["posting_time"] = "00:00"
1395
1396 if operator in (">", "<=") and previous_sle.get("name"):
1397 conditions += " and name!=%(name)s"
1398
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301399 if extra_cond:
1400 conditions += f"{extra_cond}"
1401
Ankush Menat494bd9e2022-03-28 18:52:46 +05301402 return frappe.db.sql(
1403 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301404 select *, timestamp(posting_date, posting_time) as "timestamp"
1405 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301406 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301407 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301408 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301409 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301410 %(limit)s %(for_update)s"""
1411 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301412 "conditions": conditions,
1413 "limit": limit or "",
1414 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301415 "order": order,
1416 },
1417 previous_sle,
1418 as_dict=1,
1419 debug=debug,
1420 )
1421
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301422
Nabin Haita77b8c92020-12-21 14:45:50 +05301423def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301424 return frappe.db.get_value(
1425 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301426 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301427 [
1428 "item_code",
1429 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301430 "actual_qty",
1431 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301432 "posting_date",
1433 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301434 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301435 "timestamp(posting_date, posting_time) as timestamp",
1436 ],
1437 as_dict=1,
1438 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301439
Ankush Menatce0514c2022-02-15 11:41:41 +05301440
Ankush Menat494bd9e2022-03-28 18:52:46 +05301441def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301442 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301443):
1444
Ankush Menat102fff22022-02-19 15:51:04 +05301445 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301446 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301447
Ankush Menate1c16872022-04-21 20:01:48 +05301448 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301449 posting_date, posting_time
1450 )
Ankush Menat102fff22022-02-19 15:51:04 +05301451 if creation:
1452 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301453 CombineDatetime(sle.posting_date, sle.posting_time)
1454 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301455 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301456
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301457 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301458 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301459 )
1460
Ankush Menat102fff22022-02-19 15:51:04 +05301461 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301462 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301463 .inner_join(batch_ledger)
1464 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1465 .select(
1466 Sum(
1467 Case()
1468 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1469 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1470 ).as_("batch_value"),
1471 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1472 "batch_qty"
1473 ),
1474 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301475 .where(
1476 (sle.item_code == item_code)
1477 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301478 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301479 & (sle.is_cancelled == 0)
1480 )
1481 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301482 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301483
1484 if batch_details and batch_details[0].batch_qty:
1485 return batch_details[0].batch_value / batch_details[0].batch_qty
1486
1487
Ankush Menat494bd9e2022-03-28 18:52:46 +05301488def get_valuation_rate(
1489 item_code,
1490 warehouse,
1491 voucher_type,
1492 voucher_no,
1493 allow_zero_rate=False,
1494 currency=None,
1495 company=None,
1496 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301497 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301498 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301499):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301500
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301501 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1502
Ankush Menatf7ffe042021-11-01 13:21:14 +05301503 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301504 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301505
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301506 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1507 table = frappe.qb.DocType("Stock Ledger Entry")
1508 query = (
1509 frappe.qb.from_(table)
1510 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1511 .where(
1512 (table.item_code == item_code)
1513 & (table.warehouse == warehouse)
1514 & (table.batch_no == batch_no)
1515 & (table.is_cancelled == 0)
1516 & (table.voucher_no != voucher_no)
1517 & (table.voucher_type != voucher_type)
1518 )
1519 )
1520
1521 last_valuation_rate = query.run()
1522 if last_valuation_rate:
1523 return flt(last_valuation_rate[0][0])
1524
Ankush Menat342d09a2022-02-19 14:28:51 +05301525 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301526 if warehouse and serial_and_batch_bundle:
1527 batch_obj = BatchNoValuation(
1528 sle=frappe._dict(
1529 {
1530 "item_code": item_code,
1531 "warehouse": warehouse,
1532 "actual_qty": -1,
1533 "serial_and_batch_bundle": serial_and_batch_bundle,
1534 }
1535 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301536 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301537
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301538 return batch_obj.get_incoming_rate()
1539
Ankush Menatf7ffe042021-11-01 13:21:14 +05301540 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301541 if last_valuation_rate := frappe.db.sql(
1542 """select valuation_rate
1543 from `tabStock Ledger Entry` force index (item_warehouse)
1544 where
1545 item_code = %s
1546 AND warehouse = %s
1547 AND valuation_rate >= 0
1548 AND is_cancelled = 0
1549 AND NOT (voucher_no = %s AND voucher_type = %s)
1550 order by posting_date desc, posting_time desc, name desc limit 1""",
1551 (item_code, warehouse, voucher_no, voucher_type),
1552 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301553 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301554
1555 # If negative stock allowed, and item delivered without any incoming entry,
1556 # system does not found any SLE, then take valuation rate from Item
1557 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301558
1559 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301560 # try Item Standard rate
1561 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301562
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301563 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301564 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301565 valuation_rate = frappe.db.get_value(
1566 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1567 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301568
Ankush Menat494bd9e2022-03-28 18:52:46 +05301569 if (
1570 not allow_zero_rate
1571 and not valuation_rate
1572 and raise_error_if_no_rate
1573 and cint(erpnext.is_perpetual_inventory_enabled(company))
1574 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301575 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301576
Ankush Menat494bd9e2022-03-28 18:52:46 +05301577 message = _(
1578 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1579 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301580 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301581 solutions = (
1582 "<li>"
1583 + _(
1584 "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."
1585 ).format(voucher_type)
1586 + "</li>"
1587 )
1588 solutions += (
1589 "<li>"
1590 + _("If not, you can Cancel / Submit this entry")
1591 + " {0} ".format(frappe.bold("after"))
1592 + _("performing either one below:")
1593 + "</li>"
1594 )
Marica97715f22020-05-11 20:45:37 +05301595 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1596 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1597 msg = message + solutions + sub_solutions + "</li>"
1598
1599 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301600
1601 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301602
Ankush Menat494bd9e2022-03-28 18:52:46 +05301603
Ankush Menate7109c12021-08-26 16:40:45 +05301604def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301605 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301606 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301607 qty_shift = args.actual_qty
1608
Ankush Menat7c839c42022-05-06 12:09:08 +05301609 args["time_format"] = "%H:%i:%s"
1610
marination8418c4b2021-06-22 21:35:25 +05301611 # find difference/shift in qty caused by stock reconciliation
1612 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301613 qty_shift = get_stock_reco_qty_shift(args)
1614
1615 # find the next nearest stock reco so that we only recalculate SLEs till that point
1616 next_stock_reco_detail = get_next_stock_reco(args)
1617 if next_stock_reco_detail:
1618 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301619
marination40389772021-07-02 17:13:45 +05301620 # add condition to update SLEs before this date & time
1621 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301622
Ankush Menat494bd9e2022-03-28 18:52:46 +05301623 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301624 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301625 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301626 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301627 where
1628 item_code = %(item_code)s
1629 and warehouse = %(warehouse)s
1630 and voucher_no != %(voucher_no)s
1631 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301632 and (
1633 posting_date > %(posting_date)s or
1634 (
1635 posting_date = %(posting_date)s and
1636 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1637 )
1638 )
marination40389772021-07-02 17:13:45 +05301639 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301640 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301641 args,
1642 )
Nabin Hait186a0452021-02-18 14:14:21 +05301643
1644 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1645
Ankush Menat494bd9e2022-03-28 18:52:46 +05301646
marination40389772021-07-02 17:13:45 +05301647def get_stock_reco_qty_shift(args):
1648 stock_reco_qty_shift = 0
1649 if args.get("is_cancelled"):
1650 if args.get("previous_qty_after_transaction"):
1651 # get qty (balance) that was set at submission
1652 last_balance = args.get("previous_qty_after_transaction")
1653 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1654 else:
1655 stock_reco_qty_shift = flt(args.actual_qty)
1656 else:
1657 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301658 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301659 "qty_after_transaction"
1660 )
marination40389772021-07-02 17:13:45 +05301661
1662 if last_balance is not None:
1663 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1664 else:
1665 stock_reco_qty_shift = args.qty_after_transaction
1666
1667 return stock_reco_qty_shift
1668
Ankush Menat494bd9e2022-03-28 18:52:46 +05301669
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301670def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301671 """Returns next nearest stock reconciliaton's details."""
1672
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301673 sle = frappe.qb.DocType("Stock Ledger Entry")
1674
1675 query = (
1676 frappe.qb.from_(sle)
1677 .select(
1678 sle.name,
1679 sle.posting_date,
1680 sle.posting_time,
1681 sle.creation,
1682 sle.voucher_no,
1683 sle.item_code,
1684 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301685 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301686 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301687 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301688 )
1689 .where(
1690 (sle.item_code == kwargs.get("item_code"))
1691 & (sle.warehouse == kwargs.get("warehouse"))
1692 & (sle.voucher_type == "Stock Reconciliation")
1693 & (sle.voucher_no != kwargs.get("voucher_no"))
1694 & (sle.is_cancelled == 0)
1695 & (
1696 (
1697 CombineDatetime(sle.posting_date, sle.posting_time)
1698 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301699 )
1700 | (
1701 (
1702 CombineDatetime(sle.posting_date, sle.posting_time)
1703 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301704 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301705 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301706 )
1707 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301708 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301709 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1710 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301711 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301712 )
1713
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301714 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301715 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301716
1717 return query.run(as_dict=True)
1718
marination40389772021-07-02 17:13:45 +05301719
1720def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301721 return f"""
1722 and
1723 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1724 or (
1725 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1726 and creation < '{detail.creation}'
1727 )
1728 )"""
1729
Ankush Menat494bd9e2022-03-28 18:52:46 +05301730
Ankush Menate7109c12021-08-26 16:40:45 +05301731def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301732 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301733 return
barredterraeb9ee3f2023-12-05 11:22:55 +01001734 if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
Ankush Menat5eba5752021-12-07 23:03:52 +05301735 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301736
Ankush Menat5eba5752021-12-07 23:03:52 +05301737 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301738
1739 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301740 message = _(
1741 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1742 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301743 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301744 frappe.get_desk_link("Item", args.item_code),
1745 frappe.get_desk_link("Warehouse", args.warehouse),
1746 neg_sle[0]["posting_date"],
1747 neg_sle[0]["posting_time"],
1748 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1749 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301750
Ankush Menat494bd9e2022-03-28 18:52:46 +05301751 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301752
s-aga-rd9e28432023-10-27 16:35:35 +05301753 if args.batch_no:
1754 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1755 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1756 message = _(
1757 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1758 ).format(
1759 abs(neg_batch_sle[0]["cumulative_total"]),
1760 frappe.get_desk_link("Batch", args.batch_no),
1761 frappe.get_desk_link("Warehouse", args.warehouse),
1762 neg_batch_sle[0]["posting_date"],
1763 neg_batch_sle[0]["posting_time"],
1764 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1765 )
1766 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301767
s-aga-r73b65ac2023-11-01 18:35:07 +05301768 if args.reserved_stock:
1769 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301770
Nabin Haita77b8c92020-12-21 14:45:50 +05301771
Maricad6078aa2022-06-17 15:13:13 +05301772def is_negative_with_precision(neg_sle, is_batch=False):
1773 """
1774 Returns whether system precision rounded qty is insufficient.
1775 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1776 """
1777
1778 if not neg_sle:
1779 return False
1780
1781 field = "cumulative_total" if is_batch else "qty_after_transaction"
1782 precision = cint(frappe.db.get_default("float_precision")) or 2
1783 qty_deficit = flt(neg_sle[0][field], precision)
1784
1785 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1786
1787
Nabin Haita77b8c92020-12-21 14:45:50 +05301788def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301789 return frappe.db.sql(
1790 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301791 select
1792 qty_after_transaction, posting_date, posting_time,
1793 voucher_type, voucher_no
1794 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301795 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301796 item_code = %(item_code)s
1797 and warehouse = %(warehouse)s
1798 and voucher_no != %(voucher_no)s
1799 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1800 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301801 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301802 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301803 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301804 """,
1805 args,
1806 as_dict=1,
1807 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301808
Ankush Menat5eba5752021-12-07 23:03:52 +05301809
1810def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301811 return frappe.db.sql(
1812 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301813 with batch_ledger as (
1814 select
1815 posting_date, posting_time, voucher_type, voucher_no,
1816 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1817 from `tabStock Ledger Entry`
1818 where
1819 item_code = %(item_code)s
1820 and warehouse = %(warehouse)s
1821 and batch_no=%(batch_no)s
1822 and is_cancelled = 0
1823 order by posting_date, posting_time, creation
1824 )
1825 select * from batch_ledger
1826 where
1827 cumulative_total < 0.0
1828 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1829 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301830 """,
1831 args,
1832 as_dict=1,
1833 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301834
1835
s-aga-rd9e28432023-10-27 16:35:35 +05301836def validate_reserved_stock(kwargs):
1837 if kwargs.serial_no:
1838 serial_nos = kwargs.serial_no.split("\n")
1839 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1840
s-aga-re1a87a82023-10-31 18:41:58 +05301841 elif kwargs.batch_no:
1842 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1843
s-aga-rd9e28432023-10-27 16:35:35 +05301844 elif kwargs.serial_and_batch_bundle:
1845 sbb_entries = frappe.db.get_all(
1846 "Serial and Batch Entry",
1847 {
1848 "parenttype": "Serial and Batch Bundle",
1849 "parent": kwargs.serial_and_batch_bundle,
1850 "docstatus": 1,
1851 },
s-aga-re1a87a82023-10-31 18:41:58 +05301852 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301853 )
s-aga-rd9e28432023-10-27 16:35:35 +05301854
s-aga-re1a87a82023-10-31 18:41:58 +05301855 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301856 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301857 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1858 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301859
s-aga-r92317062023-11-02 10:36:00 +05301860 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1861 precision = cint(frappe.db.get_default("float_precision")) or 2
1862 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301863
s-aga-r92317062023-11-02 10:36:00 +05301864 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1865 if diff < 0 and abs(diff) > 0.0001:
1866 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1867 abs(diff),
1868 frappe.get_desk_link("Item", kwargs.item_code),
1869 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1870 nowdate(),
1871 nowtime(),
1872 )
1873 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301874
s-aga-rd9e28432023-10-27 16:35:35 +05301875
1876def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1877 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1878 item_code, warehouse, serial_nos
1879 ):
1880 if common_serial_nos := list(
1881 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1882 ):
1883 msg = _(
1884 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1885 )
1886 msg += "<br />"
1887 msg += _("Example: Serial No {0} reserved in {1}.").format(
1888 frappe.bold(common_serial_nos[0]),
1889 frappe.get_desk_link(
1890 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1891 ),
1892 )
1893 frappe.throw(msg, title=_("Reserved Serial No."))
1894
1895
s-aga-re1a87a82023-10-31 18:41:58 +05301896def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1897 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1898 available_batches = get_available_batches(
1899 frappe._dict(
1900 {
1901 "item_code": item_code,
1902 "warehouse": warehouse,
1903 "posting_date": nowdate(),
1904 "posting_time": nowtime(),
1905 }
1906 )
1907 )
1908 available_batches_map = {row.batch_no: row.qty for row in available_batches}
1909 precision = cint(frappe.db.get_default("float_precision")) or 2
1910
1911 for batch_no in batch_nos:
1912 diff = flt(
1913 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
1914 )
1915 if diff < 0 and abs(diff) > 0.0001:
1916 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1917 abs(diff),
1918 frappe.get_desk_link("Batch", batch_no),
1919 frappe.get_desk_link("Warehouse", warehouse),
1920 nowdate(),
1921 nowtime(),
1922 )
1923 frappe.throw(msg, title=_("Reserved Stock for Batch"))
1924
1925
Ankush Menateb8b4242022-02-12 13:08:28 +05301926def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1927 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1928 return True
1929 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1930 return True
1931 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301932
1933
1934def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1935 """
1936 For inter company transfer, incoming rate is the average of the outgoing rate
1937 """
1938 rate = 0.0
1939
1940 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1941
1942 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1943
1944 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1945
1946 if reference_name:
1947 rate = frappe.get_cached_value(
1948 doctype,
1949 reference_name,
1950 "incoming_rate",
1951 )
1952
1953 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301954
1955
1956def is_internal_transfer(sle):
1957 data = frappe.get_cached_value(
1958 sle.voucher_type,
1959 sle.voucher_no,
1960 ["is_internal_supplier", "represents_company", "company"],
1961 as_dict=True,
1962 )
1963
1964 if data.is_internal_supplier and data.represents_company == data.company:
1965 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05301966
1967
1968def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
1969 table = frappe.qb.DocType("Stock Ledger Entry")
1970
1971 query = (
1972 frappe.qb.from_(table)
1973 .select(Sum(table.stock_value_difference).as_("value"))
1974 .where(
1975 (table.is_cancelled == 0)
1976 & (table.item_code == item_code)
1977 & (table.warehouse == warehouse)
1978 & (
1979 (table.posting_date < posting_date)
1980 | ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
1981 )
1982 )
1983 )
1984
1985 if voucher_no:
1986 query = query.where(table.voucher_no != voucher_no)
1987
1988 difference_amount = query.run()
1989 return flt(difference_amount[0][0]) if difference_amount else 0