blob: 0370666263f70b05168eea73c0386a4dbd9f3702 [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:
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530442 return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530443
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 Waghchaureebc82302024-01-21 18:05:20 +0530460def parse_distinct_items_and_warehouses(distinct_items_and_warehouses):
461 new_dict = frappe._dict({})
462
463 # convert string keys to tuple
464 for k, v in distinct_items_and_warehouses.items():
465 new_dict[frappe.safe_eval(k)] = frappe._dict(v)
466
467 return new_dict
468
469
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530470def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
471 if not reposting_data and doc and doc.reposting_data_file:
472 reposting_data = get_reposting_data(doc.reposting_data_file)
473
474 if reposting_data and reposting_data.affected_transactions:
475 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
476
Ankush Menatecdb4932022-04-17 19:06:13 +0530477 if not doc.affected_transactions:
478 return set()
479
480 transactions = frappe.parse_json(doc.affected_transactions)
481 return {tuple(transaction) for transaction in transactions}
482
483
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530484def get_current_index(doc=None):
485 if doc and doc.current_index:
486 return doc.current_index
487
Ankush Menat494bd9e2022-03-28 18:52:46 +0530488
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530489class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530490 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530491 update valution rate and qty after transaction
492 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530493
Ankush Menat494bd9e2022-03-28 18:52:46 +0530494 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530495
Ankush Menat494bd9e2022-03-28 18:52:46 +0530496 args = {
497 "item_code": "ABC",
498 "warehouse": "XYZ",
499 "posting_date": "2012-12-12",
500 "posting_time": "12:00"
501 }
Nabin Hait902e8602013-01-08 18:29:24 +0530502 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530503
504 def __init__(
505 self,
506 args,
507 allow_zero_rate=False,
508 allow_negative_stock=None,
509 via_landed_cost_voucher=False,
510 verbose=1,
511 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530512 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530513 self.verbose = verbose
514 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530515 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530516 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530517 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
518 item_code=self.item_code
519 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530520
Nabin Haita77b8c92020-12-21 14:45:50 +0530521 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530522 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530523 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530524
Nabin Haita77b8c92020-12-21 14:45:50 +0530525 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530526 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530527 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530528
529 self.new_items_found = False
530 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530531 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-r73b65ac2023-11-01 18:35:07 +0530532 self.reserved_stock = flt(self.args.reserved_stock)
Nabin Haita77b8c92020-12-21 14:45:50 +0530533
534 self.data = frappe._dict()
535 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530536 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530537
Maricad6078aa2022-06-17 15:13:13 +0530538 def set_precision(self):
539 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
540 self.currency_precision = get_field_precision(
541 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530542 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530543
544 def initialize_previous_data(self, args):
545 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530546 Get previous sl entries for current item for each related warehouse
547 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530548
Ankush Menat494bd9e2022-03-28 18:52:46 +0530549 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530550
Ankush Menat494bd9e2022-03-28 18:52:46 +0530551 self.data = {
552 warehouse1: {
553 'previus_sle': {},
554 'qty_after_transaction': 10,
555 'valuation_rate': 100,
556 'stock_value': 1000,
557 'prev_stock_value': 1000,
558 'stock_queue': '[[10, 100]]',
559 'stock_value_difference': 1000
560 }
561 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530562
563 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530564 self.data.setdefault(args.warehouse, frappe._dict())
565 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530566 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530567 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530568
Ankush Menatc1d986a2021-08-31 19:43:42 +0530569 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
570 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
571
Ankush Menat494bd9e2022-03-28 18:52:46 +0530572 warehouse_dict.update(
573 {
574 "prev_stock_value": previous_sle.stock_value or 0.0,
575 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
576 "stock_value_difference": 0.0,
577 }
578 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530579
Nabin Haita77b8c92020-12-21 14:45:50 +0530580 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530581 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530582
Nabin Haita77b8c92020-12-21 14:45:50 +0530583 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530584 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530585 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530586 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530587 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530588 entries_to_fix = self.get_future_entries_to_fix()
589
590 i = 0
591 while i < len(entries_to_fix):
592 sle = entries_to_fix[i]
593 i += 1
594
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530595 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530596 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530597
Nabin Haita77b8c92020-12-21 14:45:50 +0530598 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530599 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530600
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530601 if self.exceptions:
602 self.raise_exceptions()
603
Nabin Hait186a0452021-02-18 14:14:21 +0530604 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530605 sl_entries = self.get_sle_against_current_voucher()
606 for sle in sl_entries:
607 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530608
Nabin Haita77b8c92020-12-21 14:45:50 +0530609 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530610 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530611
Ankush Menat494bd9e2022-03-28 18:52:46 +0530612 return frappe.db.sql(
613 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530614 select
615 *, timestamp(posting_date, posting_time) as "timestamp"
616 from
617 `tabStock Ledger Entry`
618 where
619 item_code = %(item_code)s
620 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530621 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530622 and (
623 posting_date = %(posting_date)s and
624 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
625 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530626 order by
627 creation ASC
628 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530629 """,
630 self.args,
631 as_dict=1,
632 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530633
Nabin Haita77b8c92020-12-21 14:45:50 +0530634 def get_future_entries_to_fix(self):
635 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530636 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
637 {"item_code": self.item_code, "warehouse": self.args.warehouse}
638 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530639
Nabin Haita77b8c92020-12-21 14:45:50 +0530640 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530641
Nabin Haita77b8c92020-12-21 14:45:50 +0530642 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530643 dependant_sle = get_sle_by_voucher_detail_no(
644 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
645 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530646
Nabin Haita77b8c92020-12-21 14:45:50 +0530647 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530648 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530649 elif (
650 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
651 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530652 return entries_to_fix
653 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530654 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530655 return entries_to_fix
656 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
657 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530658 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530659 self.initialize_previous_data(dependant_sle)
660 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530661 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530662
663 def update_distinct_item_warehouses(self, dependant_sle):
664 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530665 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530666
Nabin Hait97bce3a2021-07-12 13:24:43 +0530667 if key not in self.distinct_item_warehouses:
668 self.distinct_item_warehouses[key] = val
669 self.new_items_found = True
670 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530671 existing_sle_posting_date = (
672 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
673 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530674
675 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
676
Nabin Hait97bce3a2021-07-12 13:24:43 +0530677 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
678 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530679 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
680 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530681 self.distinct_item_warehouses[key] = val
682 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530683 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
684 # Future dependent voucher needs to be repost to get the correct stock value
685 # If dependent voucher has not reposted, then add it to the list
686 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530687 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530688 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
689 self.distinct_item_warehouses[key] = val
690
691 def get_dependent_voucher_detail_nos(self, key):
692 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
693 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
694
695 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530696
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530697 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530698 # previous sle data for this warehouse
699 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530700 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530701
Anand Doshi0dc79f42015-04-06 12:59:34 +0530702 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 +0530703 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530704 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530705 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530706 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530707 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530708
Nabin Haita77b8c92020-12-21 14:45:50 +0530709 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530710 if not self.args.get("sle_id"):
711 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530712
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530713 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530714 sle.voucher_type == "Stock Reconciliation"
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530715 and (
716 sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
717 )
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530718 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530719 and not self.args.get("sle_id")
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530720 ):
721 self.reset_actual_qty_for_stock_reco(sle)
722
723 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530724 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
725 and sle.voucher_detail_no
726 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530727 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530728 ):
729 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
730
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530731 dimensions = get_inventory_dimensions()
732 has_dimensions = False
733 if dimensions:
734 for dimension in dimensions:
735 if sle.get(dimension.get("fieldname")):
736 has_dimensions = True
737
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530738 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530739 self.calculate_valuation_for_serial_batch_bundle(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530740 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530741 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530742 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530743 self.wh_data.valuation_rate = sle.valuation_rate
744 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530745 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
746 self.wh_data.valuation_rate
747 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530748 if self.valuation_method != "Moving Average":
749 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530750 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530751 if self.valuation_method == "Moving Average":
752 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530753 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530754 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
755 self.wh_data.valuation_rate
756 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530757 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530758 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530759
Rushabh Mehta54047782013-12-26 11:07:46 +0530760 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530761 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530762 if not self.wh_data.qty_after_transaction:
763 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530764
Nabin Haita77b8c92020-12-21 14:45:50 +0530765 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
766 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530767
Nabin Hait902e8602013-01-08 18:29:24 +0530768 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530769 sle.qty_after_transaction = self.wh_data.qty_after_transaction
770 sle.valuation_rate = self.wh_data.valuation_rate
771 sle.stock_value = self.wh_data.stock_value
772 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530773
rohitwaghchaurea8216b92023-11-09 12:22:26 +0530774 if not sle.is_adjustment_entry or not self.args.get("sle_id"):
775 sle.stock_value_difference = stock_value_difference
776
777 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530778 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530779
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530780 if not self.args.get("sle_id") or (
781 sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
782 ):
Ankush Menat701878f2022-03-01 18:08:29 +0530783 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530784
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530785 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530786 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
787 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
788
789 if sle.actual_qty < 0:
790 sle.actual_qty = (
791 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
792 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530793 )
794
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530795 if abs(sle.actual_qty) == 0.0:
796 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530797
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530798 def calculate_valuation_for_serial_batch_bundle(self, sle):
799 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
800
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530801 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530802 doc.calculate_qty_and_amount(save=True)
803
804 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
805
806 self.wh_data.qty_after_transaction += doc.total_qty
807 if self.wh_data.qty_after_transaction:
808 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
809
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530810 def validate_negative_stock(self, sle):
811 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530812 validate negative stock for entries current datetime onwards
813 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530814 """
s-aga-rf0acb202023-04-12 14:13:54 +0530815 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530816 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530817
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530818 if diff < 0 and abs(diff) > 0.0001:
819 # negative stock!
820 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530821 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530822 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530823 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530824 return True
825
Nabin Haita77b8c92020-12-21 14:45:50 +0530826 def get_dynamic_incoming_outgoing_rate(self, sle):
827 # Get updated incoming/outgoing rate from transaction
828 if sle.recalculate_rate:
829 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
830
831 if flt(sle.actual_qty) >= 0:
832 sle.incoming_rate = rate
833 else:
834 sle.outgoing_rate = rate
835
836 def get_incoming_outgoing_rate_from_transaction(self, sle):
837 rate = 0
838 # Material Transfer, Repack, Manufacturing
839 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530840 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530841 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
842 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530843 elif sle.voucher_type in (
844 "Purchase Receipt",
845 "Purchase Invoice",
846 "Delivery Note",
847 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530848 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530849 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530850 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530851 from erpnext.controllers.sales_and_purchase_return import (
852 get_rate_for_return, # don't move this import to top
853 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530854
mergify[bot]07175362023-12-21 14:40:52 +0530855 if self.valuation_method == "Moving Average":
856 rate = get_incoming_rate(
857 {
858 "item_code": sle.item_code,
859 "warehouse": sle.warehouse,
860 "posting_date": sle.posting_date,
861 "posting_time": sle.posting_time,
862 "qty": sle.actual_qty,
863 "serial_no": sle.get("serial_no"),
864 "batch_no": sle.get("batch_no"),
865 "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
866 "company": sle.company,
867 "voucher_type": sle.voucher_type,
868 "voucher_no": sle.voucher_no,
869 "allow_zero_valuation": self.allow_zero_rate,
870 "sle": sle.name,
871 }
872 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530873
mergify[bot]07175362023-12-21 14:40:52 +0530874 else:
875 rate = get_rate_for_return(
876 sle.voucher_type,
877 sle.voucher_no,
878 sle.item_code,
879 voucher_detail_no=sle.voucher_detail_no,
880 sle=sle,
881 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530882 elif (
883 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530884 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530885 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530886 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530887 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530888 else:
889 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530890 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530891 elif sle.voucher_type == "Subcontracting Receipt":
892 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530893 else:
894 rate_field = "incoming_rate"
895
896 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530897 item_code, incoming_rate = frappe.db.get_value(
898 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
899 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530900
901 if item_code == sle.item_code:
902 rate = incoming_rate
903 else:
904 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
905 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530906 elif sle == "Subcontracting Receipt":
907 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530908 else:
909 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530910
Ankush Menat494bd9e2022-03-28 18:52:46 +0530911 rate = frappe.db.get_value(
912 ref_doctype,
913 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
914 rate_field,
915 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530916
917 return rate
918
919 def update_outgoing_rate_on_transaction(self, sle):
920 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530921 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
922 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530923 """
924 if sle.actual_qty and sle.voucher_detail_no:
925 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
926
927 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
928 self.update_rate_on_stock_entry(sle, outgoing_rate)
929 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
930 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
931 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
932 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530933 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
934 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530935 elif sle.voucher_type == "Stock Reconciliation":
936 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530937
938 def update_rate_on_stock_entry(self, sle, outgoing_rate):
939 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
940
Ankush Menat701878f2022-03-01 18:08:29 +0530941 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
942 if not sle.dependant_sle_voucher_detail_no:
943 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530944
945 def recalculate_amounts_in_stock_entry(self, voucher_no):
946 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530947 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
948 stock_entry.db_update()
949 for d in stock_entry.items:
950 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530951
Nabin Haita77b8c92020-12-21 14:45:50 +0530952 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
953 # Update item's incoming rate on transaction
954 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
955 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530956 frappe.db.set_value(
957 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
958 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530959 else:
960 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530961 frappe.db.set_value(
962 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530963 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530964 "incoming_rate",
965 outgoing_rate,
966 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530967
968 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
969 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530970 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
971 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
972 ):
973 frappe.db.set_value(
974 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
975 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530976 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530977 frappe.db.set_value(
978 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
979 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530980
981 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530982 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530983 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530984 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530985 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530986 d.db_update()
987
Sagar Sharma323bdf82022-05-17 15:14:07 +0530988 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530989 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
990 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530991 else:
992 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530993 "Subcontracting Receipt Supplied Item",
994 sle.voucher_detail_no,
995 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530996 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530997
s-aga-ra6cb6c62023-05-03 09:51:58 +0530998 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530999 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +05301000 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301001 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +05301002 d.db_update()
1003
s-aga-r88a3f652023-05-30 16:54:28 +05301004 def update_rate_on_stock_reconciliation(self, sle):
1005 if not sle.serial_no and not sle.batch_no:
1006 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
1007
1008 for item in sr.items:
1009 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +05301010 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +05301011 continue
1012
1013 previous_sle = get_previous_sle(
1014 {
1015 "item_code": item.item_code,
1016 "warehouse": item.warehouse,
1017 "posting_date": sr.posting_date,
1018 "posting_time": sr.posting_time,
1019 "sle": sle.name,
1020 }
1021 )
1022
1023 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
1024 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
1025 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
1026
1027 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +05301028 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +05301029 item.amount_difference = item.amount - item.current_amount
1030 else:
1031 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1032 sr.db_update()
1033
1034 for item in sr.items:
1035 item.db_update()
1036
Nabin Hait328c4f92020-01-02 19:00:32 +05301037 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1038 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301039 all_serial_nos = frappe.get_all(
1040 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1041 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301042
Ankush Menat494bd9e2022-03-28 18:52:46 +05301043 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 +05301044
1045 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301046 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301047 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301048 incoming_rate = frappe.db.sql(
1049 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301050 select incoming_rate
1051 from `tabStock Ledger Entry`
1052 where
1053 company = %s
1054 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301055 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301056 and (serial_no = %s
1057 or serial_no like %s
1058 or serial_no like %s
1059 or serial_no like %s
1060 )
1061 order by posting_date desc
1062 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301063 """,
1064 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1065 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301066
1067 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1068
1069 return incoming_values
1070
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301071 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301072 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301073 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301074 if new_stock_qty >= 0:
1075 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301076 if flt(self.wh_data.qty_after_transaction) <= 0:
1077 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301078 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301079 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1080 actual_qty * sle.incoming_rate
1081 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301082
Nabin Haita77b8c92020-12-21 14:45:50 +05301083 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301084
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301085 elif sle.outgoing_rate:
1086 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301087 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1088 actual_qty * sle.outgoing_rate
1089 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301090
Nabin Haita77b8c92020-12-21 14:45:50 +05301091 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301092 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301093 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301094 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301095 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1096 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301097
Nabin Haita77b8c92020-12-21 14:45:50 +05301098 if not self.wh_data.valuation_rate and actual_qty > 0:
1099 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301100
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301101 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001102 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301103 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301104 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1105 sle.voucher_type, sle.voucher_detail_no
1106 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001107 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301108 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301109
Ankush Menatf089d392022-02-02 12:51:21 +05301110 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301111 incoming_rate = flt(sle.incoming_rate)
1112 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301113 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301114
Ankush Menat494bd9e2022-03-28 18:52:46 +05301115 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1116 self.wh_data.qty_after_transaction + actual_qty
1117 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301118
Ankush Menat97e18a12022-01-15 17:42:25 +05301119 if self.valuation_method == "LIFO":
1120 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1121 else:
1122 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1123
Ankush Menatb534fee2022-02-19 20:58:36 +05301124 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1125
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301126 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301127 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301128 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301129
Ankush Menat4b29fb62021-12-18 18:40:22 +05301130 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301131 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1132 sle.voucher_type, sle.voucher_detail_no
1133 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301134 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301135 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301136 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301137 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301138
Ankush Menat494bd9e2022-03-28 18:52:46 +05301139 stock_queue.remove_stock(
1140 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1141 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301142
Ankush Menatb534fee2022-02-19 20:58:36 +05301143 _qty, stock_value = stock_queue.get_total_stock_and_value()
1144
1145 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301146
Ankush Menat97e18a12022-01-15 17:42:25 +05301147 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301148 self.wh_data.stock_value = round_off_if_near_zero(
1149 self.wh_data.stock_value + stock_value_difference
1150 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301151
Nabin Haita77b8c92020-12-21 14:45:50 +05301152 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301153 self.wh_data.stock_queue.append(
1154 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1155 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301156
Ankush Menatb534fee2022-02-19 20:58:36 +05301157 if self.wh_data.qty_after_transaction:
1158 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1159
Ankush Menatce0514c2022-02-15 11:41:41 +05301160 def update_batched_values(self, sle):
1161 incoming_rate = flt(sle.incoming_rate)
1162 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301163
Ankush Menat494bd9e2022-03-28 18:52:46 +05301164 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1165 self.wh_data.qty_after_transaction + actual_qty
1166 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301167
1168 if actual_qty > 0:
1169 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301170 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301171 outgoing_rate = get_batch_incoming_rate(
1172 item_code=sle.item_code,
1173 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301174 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301175 posting_date=sle.posting_date,
1176 posting_time=sle.posting_time,
1177 creation=sle.creation,
1178 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301179 if outgoing_rate is None:
1180 # This can *only* happen if qty available for the batch is zero.
1181 # in such case fall back various other rates.
1182 # future entries will correct the overall accounting as each
1183 # batch individually uses moving average rates.
1184 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301185 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301186
Ankush Menat494bd9e2022-03-28 18:52:46 +05301187 self.wh_data.stock_value = round_off_if_near_zero(
1188 self.wh_data.stock_value + stock_value_difference
1189 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301190 if self.wh_data.qty_after_transaction:
1191 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301192
Javier Wong9b11d9b2017-04-14 18:24:04 +08001193 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301194 ref_item_dt = ""
1195
1196 if voucher_type == "Stock Entry":
1197 ref_item_dt = voucher_type + " Detail"
1198 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1199 ref_item_dt = voucher_type + " Item"
1200
1201 if ref_item_dt:
1202 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1203 else:
1204 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301205
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301206 def get_fallback_rate(self, sle) -> float:
1207 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301208 This should only get used for negative stock."""
1209 return get_valuation_rate(
1210 sle.item_code,
1211 sle.warehouse,
1212 sle.voucher_type,
1213 sle.voucher_no,
1214 self.allow_zero_rate,
1215 currency=erpnext.get_company_currency(sle.company),
1216 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301217 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301218
Nabin Haita77b8c92020-12-21 14:45:50 +05301219 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301220 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301221 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1222 sle = sle[0] if sle else frappe._dict()
1223 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301224
Nabin Haita77b8c92020-12-21 14:45:50 +05301225 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301226 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301227 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301228
1229 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301230 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301231 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301232 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301233
Ankush Menat494bd9e2022-03-28 18:52:46 +05301234 if (
1235 exceptions[0]["voucher_type"],
1236 exceptions[0]["voucher_no"],
1237 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301238
Nabin Haita77b8c92020-12-21 14:45:50 +05301239 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301240 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301241 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1242 frappe.get_desk_link("Warehouse", warehouse),
1243 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301244 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301245 msg = _(
1246 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1247 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301248 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301249 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1250 frappe.get_desk_link("Warehouse", warehouse),
1251 exceptions[0]["posting_date"],
1252 exceptions[0]["posting_time"],
1253 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1254 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301255
Nabin Haita77b8c92020-12-21 14:45:50 +05301256 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301257 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301258 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301259
1260 if allowed_qty > 0:
1261 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1262 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1263 )
1264 else:
1265 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1266 msg,
1267 )
s-aga-rf0acb202023-04-12 14:13:54 +05301268
Nabin Haita77b8c92020-12-21 14:45:50 +05301269 msg_list.append(msg)
1270
1271 if msg_list:
1272 message = "\n\n".join(msg_list)
1273 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301274 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301275 else:
1276 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301277
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301278 def update_bin_data(self, sle):
1279 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301280 values_to_update = {
1281 "actual_qty": sle.qty_after_transaction,
1282 "stock_value": sle.stock_value,
1283 }
1284
1285 if sle.valuation_rate is not None:
1286 values_to_update["valuation_rate"] = sle.valuation_rate
1287
1288 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301289
Nabin Haita77b8c92020-12-21 14:45:50 +05301290 def update_bin(self):
1291 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301292 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301293 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301294
Ankush Menat494bd9e2022-03-28 18:52:46 +05301295 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301296 if data.valuation_rate is not None:
1297 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301298 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301299
marination8418c4b2021-06-22 21:35:25 +05301300
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301301def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301302 """get stock ledger entries filtered by specific posting datetime conditions"""
1303
Ankush Menat494bd9e2022-03-28 18:52:46 +05301304 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301305 if not args.get("posting_date"):
1306 args["posting_date"] = "1900-01-01"
1307 if not args.get("posting_time"):
1308 args["posting_time"] = "00:00"
1309
1310 voucher_condition = ""
1311 if exclude_current_voucher:
1312 voucher_no = args.get("voucher_no")
1313 voucher_condition = f"and voucher_no != '{voucher_no}'"
1314
Ankush Menat494bd9e2022-03-28 18:52:46 +05301315 sle = frappe.db.sql(
1316 """
marination8418c4b2021-06-22 21:35:25 +05301317 select *, timestamp(posting_date, posting_time) as "timestamp"
1318 from `tabStock Ledger Entry`
1319 where item_code = %(item_code)s
1320 and warehouse = %(warehouse)s
1321 and is_cancelled = 0
1322 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301323 and (
1324 posting_date < %(posting_date)s or
1325 (
1326 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301327 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301328 )
1329 )
marination8418c4b2021-06-22 21:35:25 +05301330 order by timestamp(posting_date, posting_time) desc, creation desc
1331 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301332 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301333 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301334 ),
1335 args,
1336 as_dict=1,
1337 )
marination8418c4b2021-06-22 21:35:25 +05301338
1339 return sle[0] if sle else frappe._dict()
1340
Ankush Menat494bd9e2022-03-28 18:52:46 +05301341
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301342def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301343 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301344 get the last sle on or before the current time-bucket,
1345 to get actual qty before transaction, this function
1346 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301347
Ankush Menat494bd9e2022-03-28 18:52:46 +05301348 args = {
1349 "item_code": "ABC",
1350 "warehouse": "XYZ",
1351 "posting_date": "2012-12-12",
1352 "posting_time": "12:00",
1353 "sle": "name of reference Stock Ledger Entry"
1354 }
Anand Doshi1b531862013-01-10 19:29:51 +05301355 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301356 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301357 sle = get_stock_ledger_entries(
1358 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1359 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301360 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301361
Ankush Menat494bd9e2022-03-28 18:52:46 +05301362
1363def get_stock_ledger_entries(
1364 previous_sle,
1365 operator=None,
1366 order="desc",
1367 limit=None,
1368 for_update=False,
1369 debug=False,
1370 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301371 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301372):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301373 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301374 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1375 operator
1376 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301377 if previous_sle.get("warehouse"):
1378 conditions += " and warehouse = %(warehouse)s"
1379 elif previous_sle.get("warehouse_condition"):
1380 conditions += " and " + previous_sle.get("warehouse_condition")
1381
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301382 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301383 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1384 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301385 conditions += (
1386 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301387 (
1388 serial_no = {0}
1389 or serial_no like {1}
1390 or serial_no like {2}
1391 or serial_no like {3}
1392 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301393 """
1394 ).format(
1395 frappe.db.escape(serial_no),
1396 frappe.db.escape("{}\n%".format(serial_no)),
1397 frappe.db.escape("%\n{}".format(serial_no)),
1398 frappe.db.escape("%\n{}\n%".format(serial_no)),
1399 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301400
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301401 if not previous_sle.get("posting_date"):
1402 previous_sle["posting_date"] = "1900-01-01"
1403 if not previous_sle.get("posting_time"):
1404 previous_sle["posting_time"] = "00:00"
1405
1406 if operator in (">", "<=") and previous_sle.get("name"):
1407 conditions += " and name!=%(name)s"
1408
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301409 if extra_cond:
1410 conditions += f"{extra_cond}"
1411
Ankush Menat494bd9e2022-03-28 18:52:46 +05301412 return frappe.db.sql(
1413 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301414 select *, timestamp(posting_date, posting_time) as "timestamp"
1415 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301416 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301417 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301418 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301419 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301420 %(limit)s %(for_update)s"""
1421 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301422 "conditions": conditions,
1423 "limit": limit or "",
1424 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301425 "order": order,
1426 },
1427 previous_sle,
1428 as_dict=1,
1429 debug=debug,
1430 )
1431
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301432
Nabin Haita77b8c92020-12-21 14:45:50 +05301433def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301434 return frappe.db.get_value(
1435 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301436 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301437 [
1438 "item_code",
1439 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301440 "actual_qty",
1441 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301442 "posting_date",
1443 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301444 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301445 "timestamp(posting_date, posting_time) as timestamp",
1446 ],
1447 as_dict=1,
1448 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301449
Ankush Menatce0514c2022-02-15 11:41:41 +05301450
Ankush Menat494bd9e2022-03-28 18:52:46 +05301451def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301452 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301453):
1454
Ankush Menat102fff22022-02-19 15:51:04 +05301455 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301456 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301457
Ankush Menate1c16872022-04-21 20:01:48 +05301458 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301459 posting_date, posting_time
1460 )
Ankush Menat102fff22022-02-19 15:51:04 +05301461 if creation:
1462 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301463 CombineDatetime(sle.posting_date, sle.posting_time)
1464 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301465 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301466
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301467 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301468 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301469 )
1470
Ankush Menat102fff22022-02-19 15:51:04 +05301471 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301472 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301473 .inner_join(batch_ledger)
1474 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1475 .select(
1476 Sum(
1477 Case()
1478 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1479 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1480 ).as_("batch_value"),
1481 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1482 "batch_qty"
1483 ),
1484 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301485 .where(
1486 (sle.item_code == item_code)
1487 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301488 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301489 & (sle.is_cancelled == 0)
1490 )
1491 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301492 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301493
1494 if batch_details and batch_details[0].batch_qty:
1495 return batch_details[0].batch_value / batch_details[0].batch_qty
1496
1497
Ankush Menat494bd9e2022-03-28 18:52:46 +05301498def get_valuation_rate(
1499 item_code,
1500 warehouse,
1501 voucher_type,
1502 voucher_no,
1503 allow_zero_rate=False,
1504 currency=None,
1505 company=None,
1506 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301507 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301508 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301509):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301510
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301511 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1512
Ankush Menatf7ffe042021-11-01 13:21:14 +05301513 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301514 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301515
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301516 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1517 table = frappe.qb.DocType("Stock Ledger Entry")
1518 query = (
1519 frappe.qb.from_(table)
1520 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1521 .where(
1522 (table.item_code == item_code)
1523 & (table.warehouse == warehouse)
1524 & (table.batch_no == batch_no)
1525 & (table.is_cancelled == 0)
1526 & (table.voucher_no != voucher_no)
1527 & (table.voucher_type != voucher_type)
1528 )
1529 )
1530
1531 last_valuation_rate = query.run()
1532 if last_valuation_rate:
1533 return flt(last_valuation_rate[0][0])
1534
Ankush Menat342d09a2022-02-19 14:28:51 +05301535 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301536 if warehouse and serial_and_batch_bundle:
1537 batch_obj = BatchNoValuation(
1538 sle=frappe._dict(
1539 {
1540 "item_code": item_code,
1541 "warehouse": warehouse,
1542 "actual_qty": -1,
1543 "serial_and_batch_bundle": serial_and_batch_bundle,
1544 }
1545 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301546 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301547
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301548 return batch_obj.get_incoming_rate()
1549
Ankush Menatf7ffe042021-11-01 13:21:14 +05301550 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301551 if last_valuation_rate := frappe.db.sql(
1552 """select valuation_rate
1553 from `tabStock Ledger Entry` force index (item_warehouse)
1554 where
1555 item_code = %s
1556 AND warehouse = %s
1557 AND valuation_rate >= 0
1558 AND is_cancelled = 0
1559 AND NOT (voucher_no = %s AND voucher_type = %s)
1560 order by posting_date desc, posting_time desc, name desc limit 1""",
1561 (item_code, warehouse, voucher_no, voucher_type),
1562 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301563 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301564
1565 # If negative stock allowed, and item delivered without any incoming entry,
1566 # system does not found any SLE, then take valuation rate from Item
1567 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301568
1569 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301570 # try Item Standard rate
1571 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301572
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301573 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301574 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301575 valuation_rate = frappe.db.get_value(
1576 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1577 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301578
Ankush Menat494bd9e2022-03-28 18:52:46 +05301579 if (
1580 not allow_zero_rate
1581 and not valuation_rate
1582 and raise_error_if_no_rate
1583 and cint(erpnext.is_perpetual_inventory_enabled(company))
1584 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301585 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301586
Ankush Menat494bd9e2022-03-28 18:52:46 +05301587 message = _(
1588 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1589 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301590 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301591 solutions = (
1592 "<li>"
1593 + _(
1594 "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."
1595 ).format(voucher_type)
1596 + "</li>"
1597 )
1598 solutions += (
1599 "<li>"
1600 + _("If not, you can Cancel / Submit this entry")
1601 + " {0} ".format(frappe.bold("after"))
1602 + _("performing either one below:")
1603 + "</li>"
1604 )
Marica97715f22020-05-11 20:45:37 +05301605 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1606 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1607 msg = message + solutions + sub_solutions + "</li>"
1608
1609 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301610
1611 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301612
Ankush Menat494bd9e2022-03-28 18:52:46 +05301613
Ankush Menate7109c12021-08-26 16:40:45 +05301614def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301615 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301616 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301617 qty_shift = args.actual_qty
1618
Ankush Menat7c839c42022-05-06 12:09:08 +05301619 args["time_format"] = "%H:%i:%s"
1620
marination8418c4b2021-06-22 21:35:25 +05301621 # find difference/shift in qty caused by stock reconciliation
1622 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301623 qty_shift = get_stock_reco_qty_shift(args)
1624
1625 # find the next nearest stock reco so that we only recalculate SLEs till that point
1626 next_stock_reco_detail = get_next_stock_reco(args)
1627 if next_stock_reco_detail:
1628 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301629
marination40389772021-07-02 17:13:45 +05301630 # add condition to update SLEs before this date & time
1631 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301632
Ankush Menat494bd9e2022-03-28 18:52:46 +05301633 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301634 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301635 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301636 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301637 where
1638 item_code = %(item_code)s
1639 and warehouse = %(warehouse)s
1640 and voucher_no != %(voucher_no)s
1641 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301642 and (
1643 posting_date > %(posting_date)s or
1644 (
1645 posting_date = %(posting_date)s and
1646 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1647 )
1648 )
marination40389772021-07-02 17:13:45 +05301649 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301650 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301651 args,
1652 )
Nabin Hait186a0452021-02-18 14:14:21 +05301653
1654 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1655
Ankush Menat494bd9e2022-03-28 18:52:46 +05301656
marination40389772021-07-02 17:13:45 +05301657def get_stock_reco_qty_shift(args):
1658 stock_reco_qty_shift = 0
1659 if args.get("is_cancelled"):
1660 if args.get("previous_qty_after_transaction"):
1661 # get qty (balance) that was set at submission
1662 last_balance = args.get("previous_qty_after_transaction")
1663 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1664 else:
1665 stock_reco_qty_shift = flt(args.actual_qty)
1666 else:
1667 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301668 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301669 "qty_after_transaction"
1670 )
marination40389772021-07-02 17:13:45 +05301671
1672 if last_balance is not None:
1673 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1674 else:
1675 stock_reco_qty_shift = args.qty_after_transaction
1676
1677 return stock_reco_qty_shift
1678
Ankush Menat494bd9e2022-03-28 18:52:46 +05301679
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301680def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301681 """Returns next nearest stock reconciliaton's details."""
1682
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301683 sle = frappe.qb.DocType("Stock Ledger Entry")
1684
1685 query = (
1686 frappe.qb.from_(sle)
1687 .select(
1688 sle.name,
1689 sle.posting_date,
1690 sle.posting_time,
1691 sle.creation,
1692 sle.voucher_no,
1693 sle.item_code,
1694 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301695 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301696 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301697 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301698 )
1699 .where(
1700 (sle.item_code == kwargs.get("item_code"))
1701 & (sle.warehouse == kwargs.get("warehouse"))
1702 & (sle.voucher_type == "Stock Reconciliation")
1703 & (sle.voucher_no != kwargs.get("voucher_no"))
1704 & (sle.is_cancelled == 0)
1705 & (
1706 (
1707 CombineDatetime(sle.posting_date, sle.posting_time)
1708 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301709 )
1710 | (
1711 (
1712 CombineDatetime(sle.posting_date, sle.posting_time)
1713 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301714 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301715 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301716 )
1717 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301718 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301719 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1720 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301721 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301722 )
1723
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301724 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301725 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301726
1727 return query.run(as_dict=True)
1728
marination40389772021-07-02 17:13:45 +05301729
1730def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301731 return f"""
1732 and
1733 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1734 or (
1735 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1736 and creation < '{detail.creation}'
1737 )
1738 )"""
1739
Ankush Menat494bd9e2022-03-28 18:52:46 +05301740
Ankush Menate7109c12021-08-26 16:40:45 +05301741def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301742 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301743 return
barredterraeb9ee3f2023-12-05 11:22:55 +01001744 if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
Ankush Menat5eba5752021-12-07 23:03:52 +05301745 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301746
Ankush Menat5eba5752021-12-07 23:03:52 +05301747 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301748
1749 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301750 message = _(
1751 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1752 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301753 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301754 frappe.get_desk_link("Item", args.item_code),
1755 frappe.get_desk_link("Warehouse", args.warehouse),
1756 neg_sle[0]["posting_date"],
1757 neg_sle[0]["posting_time"],
1758 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1759 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301760
Ankush Menat494bd9e2022-03-28 18:52:46 +05301761 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301762
s-aga-rd9e28432023-10-27 16:35:35 +05301763 if args.batch_no:
1764 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1765 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1766 message = _(
1767 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1768 ).format(
1769 abs(neg_batch_sle[0]["cumulative_total"]),
1770 frappe.get_desk_link("Batch", args.batch_no),
1771 frappe.get_desk_link("Warehouse", args.warehouse),
1772 neg_batch_sle[0]["posting_date"],
1773 neg_batch_sle[0]["posting_time"],
1774 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1775 )
1776 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301777
s-aga-r73b65ac2023-11-01 18:35:07 +05301778 if args.reserved_stock:
1779 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301780
Nabin Haita77b8c92020-12-21 14:45:50 +05301781
Maricad6078aa2022-06-17 15:13:13 +05301782def is_negative_with_precision(neg_sle, is_batch=False):
1783 """
1784 Returns whether system precision rounded qty is insufficient.
1785 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1786 """
1787
1788 if not neg_sle:
1789 return False
1790
1791 field = "cumulative_total" if is_batch else "qty_after_transaction"
1792 precision = cint(frappe.db.get_default("float_precision")) or 2
1793 qty_deficit = flt(neg_sle[0][field], precision)
1794
1795 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1796
1797
Nabin Haita77b8c92020-12-21 14:45:50 +05301798def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301799 return frappe.db.sql(
1800 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301801 select
1802 qty_after_transaction, posting_date, posting_time,
1803 voucher_type, voucher_no
1804 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301805 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301806 item_code = %(item_code)s
1807 and warehouse = %(warehouse)s
1808 and voucher_no != %(voucher_no)s
1809 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1810 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301811 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301812 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301813 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301814 """,
1815 args,
1816 as_dict=1,
1817 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301818
Ankush Menat5eba5752021-12-07 23:03:52 +05301819
1820def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301821 return frappe.db.sql(
1822 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301823 with batch_ledger as (
1824 select
1825 posting_date, posting_time, voucher_type, voucher_no,
1826 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1827 from `tabStock Ledger Entry`
1828 where
1829 item_code = %(item_code)s
1830 and warehouse = %(warehouse)s
1831 and batch_no=%(batch_no)s
1832 and is_cancelled = 0
1833 order by posting_date, posting_time, creation
1834 )
1835 select * from batch_ledger
1836 where
1837 cumulative_total < 0.0
1838 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1839 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301840 """,
1841 args,
1842 as_dict=1,
1843 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301844
1845
s-aga-rd9e28432023-10-27 16:35:35 +05301846def validate_reserved_stock(kwargs):
1847 if kwargs.serial_no:
1848 serial_nos = kwargs.serial_no.split("\n")
1849 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1850
s-aga-re1a87a82023-10-31 18:41:58 +05301851 elif kwargs.batch_no:
1852 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1853
s-aga-rd9e28432023-10-27 16:35:35 +05301854 elif kwargs.serial_and_batch_bundle:
1855 sbb_entries = frappe.db.get_all(
1856 "Serial and Batch Entry",
1857 {
1858 "parenttype": "Serial and Batch Bundle",
1859 "parent": kwargs.serial_and_batch_bundle,
1860 "docstatus": 1,
1861 },
s-aga-re1a87a82023-10-31 18:41:58 +05301862 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301863 )
s-aga-rd9e28432023-10-27 16:35:35 +05301864
s-aga-re1a87a82023-10-31 18:41:58 +05301865 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301866 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301867 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1868 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301869
s-aga-r92317062023-11-02 10:36:00 +05301870 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1871 precision = cint(frappe.db.get_default("float_precision")) or 2
1872 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301873
s-aga-r92317062023-11-02 10:36:00 +05301874 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1875 if diff < 0 and abs(diff) > 0.0001:
1876 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1877 abs(diff),
1878 frappe.get_desk_link("Item", kwargs.item_code),
1879 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1880 nowdate(),
1881 nowtime(),
1882 )
1883 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301884
s-aga-rd9e28432023-10-27 16:35:35 +05301885
1886def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1887 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1888 item_code, warehouse, serial_nos
1889 ):
1890 if common_serial_nos := list(
1891 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1892 ):
1893 msg = _(
1894 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1895 )
1896 msg += "<br />"
1897 msg += _("Example: Serial No {0} reserved in {1}.").format(
1898 frappe.bold(common_serial_nos[0]),
1899 frappe.get_desk_link(
1900 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1901 ),
1902 )
1903 frappe.throw(msg, title=_("Reserved Serial No."))
1904
1905
s-aga-re1a87a82023-10-31 18:41:58 +05301906def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1907 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1908 available_batches = get_available_batches(
1909 frappe._dict(
1910 {
1911 "item_code": item_code,
1912 "warehouse": warehouse,
1913 "posting_date": nowdate(),
1914 "posting_time": nowtime(),
1915 }
1916 )
1917 )
1918 available_batches_map = {row.batch_no: row.qty for row in available_batches}
1919 precision = cint(frappe.db.get_default("float_precision")) or 2
1920
1921 for batch_no in batch_nos:
1922 diff = flt(
1923 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
1924 )
1925 if diff < 0 and abs(diff) > 0.0001:
1926 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1927 abs(diff),
1928 frappe.get_desk_link("Batch", batch_no),
1929 frappe.get_desk_link("Warehouse", warehouse),
1930 nowdate(),
1931 nowtime(),
1932 )
1933 frappe.throw(msg, title=_("Reserved Stock for Batch"))
1934
1935
Ankush Menateb8b4242022-02-12 13:08:28 +05301936def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1937 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1938 return True
1939 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1940 return True
1941 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301942
1943
1944def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1945 """
1946 For inter company transfer, incoming rate is the average of the outgoing rate
1947 """
1948 rate = 0.0
1949
1950 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1951
1952 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1953
1954 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1955
1956 if reference_name:
1957 rate = frappe.get_cached_value(
1958 doctype,
1959 reference_name,
1960 "incoming_rate",
1961 )
1962
1963 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301964
1965
1966def is_internal_transfer(sle):
1967 data = frappe.get_cached_value(
1968 sle.voucher_type,
1969 sle.voucher_no,
1970 ["is_internal_supplier", "represents_company", "company"],
1971 as_dict=True,
1972 )
1973
1974 if data.is_internal_supplier and data.represents_company == data.company:
1975 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05301976
1977
1978def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
1979 table = frappe.qb.DocType("Stock Ledger Entry")
1980
1981 query = (
1982 frappe.qb.from_(table)
1983 .select(Sum(table.stock_value_difference).as_("value"))
1984 .where(
1985 (table.is_cancelled == 0)
1986 & (table.item_code == item_code)
1987 & (table.warehouse == warehouse)
1988 & (
1989 (table.posting_date < posting_date)
1990 | ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
1991 )
1992 )
1993 )
1994
1995 if voucher_no:
1996 query = query.where(table.voucher_no != voucher_no)
1997
1998 difference_amount = query.run()
1999 return flt(difference_amount[0][0]) if difference_amount else 0