blob: e88b1921fad431b2e9cbae1a9da5eb290bbe7206 [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
Ankush Menate1c16872022-04-21 20:01:48 +053012from frappe.query_builder.functions import CombineDatetime, Sum
Rohit Waghchaure64cb1152024-01-15 19:39:41 +053013from frappe.utils import (
14 cint,
15 cstr,
16 flt,
17 get_link_to_form,
18 getdate,
19 now,
20 nowdate,
21 nowtime,
22 parse_json,
23)
Achilles Rasquinha361366e2018-02-14 17:08:59 +053024
Chillar Anand915b3432021-09-02 16:44:59 +053025import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053026from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
mergify[bot]27a1e3b2023-10-16 19:15:18 +053027from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
s-aga-re1a87a82023-10-31 18:41:58 +053028from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
29 get_available_batches,
30)
31from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
32 get_sre_reserved_batch_nos_details,
s-aga-rd9e28432023-10-27 16:35:35 +053033 get_sre_reserved_serial_nos_details,
34)
Chillar Anand915b3432021-09-02 16:44:59 +053035from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053036 get_incoming_outgoing_rate_for_cancel,
mergify[bot]07175362023-12-21 14:40:52 +053037 get_incoming_rate,
Deepesh Garg6f107da2021-10-12 20:15:55 +053038 get_or_make_bin,
s-aga-r73b65ac2023-11-01 18:35:07 +053039 get_stock_balance,
Chillar Anand915b3432021-09-02 16:44:59 +053040 get_valuation_method,
41)
Ankush Menatb534fee2022-02-19 20:58:36 +053042from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053043
Nabin Hait97bce3a2021-07-12 13:24:43 +053044
Ankush Menat494bd9e2022-03-28 18:52:46 +053045class NegativeStockError(frappe.ValidationError):
46 pass
47
48
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053049class SerialNoExistsInFutureTransaction(frappe.ValidationError):
50 pass
Nabin Hait902e8602013-01-08 18:29:24 +053051
Anand Doshi5b004ff2013-09-25 19:55:41 +053052
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053053def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053054 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053055
Ankush Menat494bd9e2022-03-28 18:52:46 +053056 args:
57 - allow_negative_stock: disable negative stock valiations if true
58 - via_landed_cost_voucher: landed cost voucher cancels and reposts
59 entries of purchase document. This flag is used to identify if
60 cancellation and repost is happening via landed cost voucher, in
61 such cases certain validations need to be ignored (like negative
62 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053063 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053064 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053065
Nabin Haitca775742013-09-26 16:16:44 +053066 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053067 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053068 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053069 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053070 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053071
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053072 args = get_args_for_future_sle(sl_entries[0])
73 future_sle_exists(args, sl_entries)
74
Nabin Haitca775742013-09-26 16:16:44 +053075 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053076 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053077 validate_serial_no(sle)
78
Nabin Haita77b8c92020-12-21 14:45:50 +053079 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053080 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053081
Ankush Menat494bd9e2022-03-28 18:52:46 +053082 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
83 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
84 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
85 )
86 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053087
Ankush Menat494bd9e2022-03-28 18:52:46 +053088 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
89 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
90 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
91 )
92 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053093
Ankush Menat494bd9e2022-03-28 18:52:46 +053094 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053095 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053096
Nabin Haita77b8c92020-12-21 14:45:50 +053097 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053098
99 if sle.get("voucher_type") == "Stock Reconciliation":
100 # preserve previous_qty_after_transaction for qty reposting
101 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
102
Ankush Menat494bd9e2022-03-28 18:52:46 +0530103 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +0530104 if is_stock_item:
105 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
s-aga-r73b65ac2023-11-01 18:35:07 +0530106 args.reserved_stock = flt(frappe.db.get_value("Bin", bin_name, "reserved_stock"))
Ankush Menatcef84c22021-12-03 12:18:59 +0530107 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +0530108 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +0530109 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530110 frappe.msgprint(
111 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
112 )
113
Ankush Menatcef84c22021-12-03 12:18:59 +0530114
115def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
116 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
117 if not args.get("posting_date"):
118 args["posting_date"] = nowdate()
119
marination7a5fd712022-07-04 17:46:54 +0530120 if not (args.get("is_cancelled") and via_landed_cost_voucher):
121 # Reposts only current voucher SL Entries
122 # Updates valuation rate, stock value, stock queue for current transaction
123 update_entries_after(
124 {
125 "item_code": args.get("item_code"),
126 "warehouse": args.get("warehouse"),
127 "posting_date": args.get("posting_date"),
128 "posting_time": args.get("posting_time"),
129 "voucher_type": args.get("voucher_type"),
130 "voucher_no": args.get("voucher_no"),
131 "sle_id": args.get("name"),
132 "creation": args.get("creation"),
s-aga-r73b65ac2023-11-01 18:35:07 +0530133 "reserved_stock": args.get("reserved_stock"),
marination7a5fd712022-07-04 17:46:54 +0530134 },
135 allow_negative_stock=allow_negative_stock,
136 via_landed_cost_voucher=via_landed_cost_voucher,
137 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530138
139 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530140 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530141 update_qty_in_future_sle(args, allow_negative_stock)
142
Nabin Haitadeb9762014-10-06 11:53:52 +0530143
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530144def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530145 return frappe._dict(
146 {
147 "voucher_type": row.get("voucher_type"),
148 "voucher_no": row.get("voucher_no"),
149 "posting_date": row.get("posting_date"),
150 "posting_time": row.get("posting_time"),
151 }
152 )
153
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530154
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530155def validate_serial_no(sle):
156 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530157
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530158 for sn in get_serial_nos(sle.serial_no):
159 args = copy.deepcopy(sle)
160 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530161 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530162
163 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530164 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530165 voucher_type = frappe.bold(row.voucher_type)
166 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530167 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530168
169 if vouchers:
170 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530171 msg = (
172 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
173 The list of the transactions are as below."""
174 + "<br><br><ul><li>"
175 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530176
Ankush Menat494bd9e2022-03-28 18:52:46 +0530177 msg += "</li><li>".join(vouchers)
178 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530179
Ankush Menat494bd9e2022-03-28 18:52:46 +0530180 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530181 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
182
Ankush Menat494bd9e2022-03-28 18:52:46 +0530183
Nabin Hait186a0452021-02-18 14:14:21 +0530184def validate_cancellation(args):
185 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530186 repost_entry = frappe.db.get_value(
187 "Repost Item Valuation",
188 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
189 ["name", "status"],
190 as_dict=1,
191 )
Nabin Hait186a0452021-02-18 14:14:21 +0530192
193 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530194 if repost_entry.status == "In Progress":
195 frappe.throw(
196 _(
197 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
198 )
199 )
200 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530201 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530202 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530203 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530204 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530205
Ankush Menat494bd9e2022-03-28 18:52:46 +0530206
Nabin Hait9653f602013-08-20 15:37:33 +0530207def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530208 frappe.db.sql(
209 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530210 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530211 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530212 (now(), frappe.session.user, voucher_type, voucher_no),
213 )
214
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530215
Nabin Hait54c865e2015-03-27 15:38:31 +0530216def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530217 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530218 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530219 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530220 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530221 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530222 sle.submit()
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530223
224 # Added to handle the case when the stock ledger entry is created from the repostig
225 if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
226 sle.db_set("creation", args.get("creation_time"))
227
Nabin Haita77b8c92020-12-21 14:45:50 +0530228 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530229
Ankush Menat494bd9e2022-03-28 18:52:46 +0530230
231def repost_future_sle(
232 args=None,
233 voucher_type=None,
234 voucher_no=None,
235 allow_negative_stock=None,
236 via_landed_cost_voucher=False,
237 doc=None,
238):
Nabin Haite1fa7232022-07-20 15:19:09 +0530239 if not args:
240 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530241
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530242 reposting_data = {}
243 if doc and doc.reposting_data_file:
244 reposting_data = get_reposting_data(doc.reposting_data_file)
245
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530246 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530247 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530248 )
249 if items_to_be_repost:
250 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530251
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530252 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
253 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530254
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530255 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530256 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530257 validate_item_warehouse(args[i])
258
Ankush Menat494bd9e2022-03-28 18:52:46 +0530259 obj = update_entries_after(
260 {
261 "item_code": args[i].get("item_code"),
262 "warehouse": args[i].get("warehouse"),
263 "posting_date": args[i].get("posting_date"),
264 "posting_time": args[i].get("posting_time"),
265 "creation": args[i].get("creation"),
266 "distinct_item_warehouses": distinct_item_warehouses,
267 },
268 allow_negative_stock=allow_negative_stock,
269 via_landed_cost_voucher=via_landed_cost_voucher,
270 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530271 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530272
Ankush Menat494bd9e2022-03-28 18:52:46 +0530273 distinct_item_warehouses[
274 (args[i].get("item_code"), args[i].get("warehouse"))
275 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530276
Nabin Hait97bce3a2021-07-12 13:24:43 +0530277 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530278 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530279 if ("args_idx" not in data and not data.reposting_status) or (
280 data.sle_changed and data.reposting_status
281 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530282 data.args_idx = len(args)
283 args.append(data.sle)
284 elif data.sle_changed and not data.reposting_status:
285 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530286
Nabin Hait97bce3a2021-07-12 13:24:43 +0530287 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530288 i += 1
289
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530290 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530291 update_args_in_repost_item_valuation(
292 doc, i, args, distinct_item_warehouses, affected_transactions
293 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530294
Ankush Menat494bd9e2022-03-28 18:52:46 +0530295
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530296def get_reposting_data(file_path) -> dict:
297 file_name = frappe.db.get_value(
298 "File",
299 {
300 "file_url": file_path,
301 "attached_to_field": "reposting_data_file",
302 },
303 "name",
304 )
305
306 if not file_name:
307 return frappe._dict()
308
309 attached_file = frappe.get_doc("File", file_name)
310
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530311 data = gzip.decompress(attached_file.get_content())
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530312 if data := json.loads(data.decode("utf-8")):
313 data = data
314
315 return parse_json(data)
316
317
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530318def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530319 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530320 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530321 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530322 frappe.throw(_(validation_msg))
323
Ankush Menat494bd9e2022-03-28 18:52:46 +0530324
Ankush Menatecdb4932022-04-17 19:06:13 +0530325def update_args_in_repost_item_valuation(
326 doc, index, args, distinct_item_warehouses, affected_transactions
327):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530328 if not doc.items_to_be_repost:
329 file_name = ""
330 if doc.reposting_data_file:
331 file_name = get_reposting_file_name(doc.doctype, doc.name)
332 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
333
334 doc.reposting_data_file = create_json_gz_file(
335 {
336 "items_to_be_repost": args,
337 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
338 "affected_transactions": affected_transactions,
339 },
340 doc,
341 file_name,
342 )
343
344 doc.db_set(
345 {
346 "current_index": index,
347 "total_reposting_count": len(args),
348 "reposting_data_file": doc.reposting_data_file,
349 }
350 )
351
352 else:
353 doc.db_set(
354 {
355 "items_to_be_repost": json.dumps(args, default=str),
356 "distinct_item_and_warehouse": json.dumps(
357 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
358 ),
359 "current_index": index,
360 "affected_transactions": frappe.as_json(affected_transactions),
361 }
362 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530363
Ankush Menatecdb4932022-04-17 19:06:13 +0530364 if not frappe.flags.in_test:
365 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530366
Ankush Menat494bd9e2022-03-28 18:52:46 +0530367 frappe.publish_realtime(
368 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530369 {
370 "name": doc.name,
371 "items_to_be_repost": json.dumps(args, default=str),
372 "current_index": index,
373 "total_reposting_count": len(args),
374 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530375 doctype=doc.doctype,
376 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530377 )
378
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530379
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530380def get_reposting_file_name(dt, dn):
381 return frappe.db.get_value(
382 "File",
383 {
384 "attached_to_doctype": dt,
385 "attached_to_name": dn,
386 "attached_to_field": "reposting_data_file",
387 },
388 "name",
389 )
390
391
392def create_json_gz_file(data, doc, file_name=None) -> str:
393 encoded_content = frappe.safe_encode(frappe.as_json(data))
Akhil Narang21c3d9c2023-10-21 11:19:45 +0530394 compressed_content = gzip.compress(encoded_content)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530395
396 if not file_name:
397 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
398 _file = frappe.get_doc(
399 {
400 "doctype": "File",
401 "file_name": json_filename,
402 "attached_to_doctype": doc.doctype,
403 "attached_to_name": doc.name,
404 "attached_to_field": "reposting_data_file",
405 "content": compressed_content,
406 "is_private": 1,
407 }
408 )
409 _file.save(ignore_permissions=True)
410
411 return _file.file_url
412 else:
413 file_doc = frappe.get_doc("File", file_name)
414 path = file_doc.get_full_path()
415
416 with open(path, "wb") as f:
417 f.write(compressed_content)
418
419 return doc.reposting_data_file
420
421
422def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
423 if not reposting_data and doc and doc.reposting_data_file:
424 reposting_data = get_reposting_data(doc.reposting_data_file)
425
426 if reposting_data and reposting_data.items_to_be_repost:
427 return reposting_data.items_to_be_repost
428
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530429 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530430
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530431 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530432 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530433
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530434 if not items_to_be_repost and voucher_type and voucher_no:
435 items_to_be_repost = frappe.db.get_all(
436 "Stock Ledger Entry",
437 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
438 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
439 order_by="creation asc",
440 group_by="item_code, warehouse",
441 )
442
Nabin Haite1fa7232022-07-20 15:19:09 +0530443 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530444
Ankush Menat494bd9e2022-03-28 18:52:46 +0530445
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530446def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
447 if not reposting_data and doc and doc.reposting_data_file:
448 reposting_data = get_reposting_data(doc.reposting_data_file)
449
450 if reposting_data and reposting_data.distinct_item_and_warehouse:
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530451 return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse)
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530452
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530453 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530454
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530455 if doc and doc.distinct_item_and_warehouse:
456 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530457 distinct_item_warehouses = {
458 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
459 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530460 else:
461 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530462 distinct_item_warehouses.setdefault(
463 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
464 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530465
466 return distinct_item_warehouses
467
Ankush Menat494bd9e2022-03-28 18:52:46 +0530468
Rohit Waghchaureebc82302024-01-21 18:05:20 +0530469def parse_distinct_items_and_warehouses(distinct_items_and_warehouses):
470 new_dict = frappe._dict({})
471
472 # convert string keys to tuple
473 for k, v in distinct_items_and_warehouses.items():
474 new_dict[frappe.safe_eval(k)] = frappe._dict(v)
475
476 return new_dict
477
478
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530479def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
480 if not reposting_data and doc and doc.reposting_data_file:
481 reposting_data = get_reposting_data(doc.reposting_data_file)
482
483 if reposting_data and reposting_data.affected_transactions:
484 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
485
Ankush Menatecdb4932022-04-17 19:06:13 +0530486 if not doc.affected_transactions:
487 return set()
488
489 transactions = frappe.parse_json(doc.affected_transactions)
490 return {tuple(transaction) for transaction in transactions}
491
492
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530493def get_current_index(doc=None):
494 if doc and doc.current_index:
495 return doc.current_index
496
Ankush Menat494bd9e2022-03-28 18:52:46 +0530497
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530498class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530499 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530500 update valution rate and qty after transaction
501 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530502
Ankush Menat494bd9e2022-03-28 18:52:46 +0530503 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530504
Ankush Menat494bd9e2022-03-28 18:52:46 +0530505 args = {
506 "item_code": "ABC",
507 "warehouse": "XYZ",
508 "posting_date": "2012-12-12",
509 "posting_time": "12:00"
510 }
Nabin Hait902e8602013-01-08 18:29:24 +0530511 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530512
513 def __init__(
514 self,
515 args,
516 allow_zero_rate=False,
517 allow_negative_stock=None,
518 via_landed_cost_voucher=False,
519 verbose=1,
520 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530521 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530522 self.verbose = verbose
523 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530524 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530525 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530526 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
527 item_code=self.item_code
528 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530529
Nabin Haita77b8c92020-12-21 14:45:50 +0530530 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530531 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530532 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530533
Nabin Haita77b8c92020-12-21 14:45:50 +0530534 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530535 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530536 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530537
538 self.new_items_found = False
539 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530540 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-r73b65ac2023-11-01 18:35:07 +0530541 self.reserved_stock = flt(self.args.reserved_stock)
Nabin Haita77b8c92020-12-21 14:45:50 +0530542
543 self.data = frappe._dict()
544 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530545 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530546
Maricad6078aa2022-06-17 15:13:13 +0530547 def set_precision(self):
548 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
549 self.currency_precision = get_field_precision(
550 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530551 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530552
553 def initialize_previous_data(self, args):
554 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530555 Get previous sl entries for current item for each related warehouse
556 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530557
Ankush Menat494bd9e2022-03-28 18:52:46 +0530558 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530559
Ankush Menat494bd9e2022-03-28 18:52:46 +0530560 self.data = {
561 warehouse1: {
562 'previus_sle': {},
563 'qty_after_transaction': 10,
564 'valuation_rate': 100,
565 'stock_value': 1000,
566 'prev_stock_value': 1000,
567 'stock_queue': '[[10, 100]]',
568 'stock_value_difference': 1000
569 }
570 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530571
572 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530573 self.data.setdefault(args.warehouse, frappe._dict())
574 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530575 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530576 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530577
Ankush Menatc1d986a2021-08-31 19:43:42 +0530578 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
579 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
580
Ankush Menat494bd9e2022-03-28 18:52:46 +0530581 warehouse_dict.update(
582 {
583 "prev_stock_value": previous_sle.stock_value or 0.0,
584 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
585 "stock_value_difference": 0.0,
586 }
587 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530588
Nabin Haita77b8c92020-12-21 14:45:50 +0530589 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530590 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530591
Nabin Haita77b8c92020-12-21 14:45:50 +0530592 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530593 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530594 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530595 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530596 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530597 entries_to_fix = self.get_future_entries_to_fix()
598
599 i = 0
600 while i < len(entries_to_fix):
601 sle = entries_to_fix[i]
602 i += 1
603
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530604 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530605 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530606
Nabin Haita77b8c92020-12-21 14:45:50 +0530607 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530608 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530609
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530610 if self.exceptions:
611 self.raise_exceptions()
612
Nabin Hait186a0452021-02-18 14:14:21 +0530613 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530614 sl_entries = self.get_sle_against_current_voucher()
615 for sle in sl_entries:
616 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530617
Nabin Haita77b8c92020-12-21 14:45:50 +0530618 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530619 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530620
Ankush Menat494bd9e2022-03-28 18:52:46 +0530621 return frappe.db.sql(
622 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530623 select
624 *, timestamp(posting_date, posting_time) as "timestamp"
625 from
626 `tabStock Ledger Entry`
627 where
628 item_code = %(item_code)s
629 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530630 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530631 and (
632 posting_date = %(posting_date)s and
633 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
634 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530635 order by
636 creation ASC
637 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530638 """,
639 self.args,
640 as_dict=1,
641 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530642
Nabin Haita77b8c92020-12-21 14:45:50 +0530643 def get_future_entries_to_fix(self):
644 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530645 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
646 {"item_code": self.item_code, "warehouse": self.args.warehouse}
647 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530648
Nabin Haita77b8c92020-12-21 14:45:50 +0530649 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530650
Nabin Haita77b8c92020-12-21 14:45:50 +0530651 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530652 dependant_sle = get_sle_by_voucher_detail_no(
653 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
654 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530655
Nabin Haita77b8c92020-12-21 14:45:50 +0530656 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530657 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530658 elif (
659 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
660 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530661 return entries_to_fix
662 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530663 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530664 return entries_to_fix
665 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
666 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530667 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530668 self.initialize_previous_data(dependant_sle)
669 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530670 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530671
672 def update_distinct_item_warehouses(self, dependant_sle):
673 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530674 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530675
Nabin Hait97bce3a2021-07-12 13:24:43 +0530676 if key not in self.distinct_item_warehouses:
677 self.distinct_item_warehouses[key] = val
678 self.new_items_found = True
679 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530680 existing_sle_posting_date = (
681 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
682 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530683
684 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
685
Nabin Hait97bce3a2021-07-12 13:24:43 +0530686 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
687 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530688 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
689 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530690 self.distinct_item_warehouses[key] = val
691 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530692 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
693 # Future dependent voucher needs to be repost to get the correct stock value
694 # If dependent voucher has not reposted, then add it to the list
695 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530696 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530697 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
698 self.distinct_item_warehouses[key] = val
699
700 def get_dependent_voucher_detail_nos(self, key):
701 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
702 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
703
704 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530705
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530706 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530707 # previous sle data for this warehouse
708 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530709 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530710
Anand Doshi0dc79f42015-04-06 12:59:34 +0530711 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 +0530712 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530713 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530714 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530715 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530716 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530717
Nabin Haita77b8c92020-12-21 14:45:50 +0530718 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530719 if not self.args.get("sle_id"):
720 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530721
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530722 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530723 sle.voucher_type == "Stock Reconciliation"
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530724 and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530725 and sle.voucher_detail_no
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530726 and not self.args.get("sle_id")
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530727 and sle.is_cancelled == 0
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530728 ):
729 self.reset_actual_qty_for_stock_reco(sle)
730
731 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530732 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
733 and sle.voucher_detail_no
734 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530735 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530736 ):
737 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
738
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530739 dimensions = get_inventory_dimensions()
740 has_dimensions = False
741 if dimensions:
742 for dimension in dimensions:
743 if sle.get(dimension.get("fieldname")):
744 has_dimensions = True
745
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530746 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530747 self.calculate_valuation_for_serial_batch_bundle(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530748 elif sle.serial_no and not self.args.get("sle_id"):
749 # Only run in reposting
750 self.get_serialized_values(sle)
751 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
752 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
753 self.wh_data.qty_after_transaction = sle.qty_after_transaction
754
755 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
756 self.wh_data.valuation_rate
757 )
758 elif (
759 sle.batch_no
760 and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
761 and not self.args.get("sle_id")
762 ):
763 # Only run in reposting
764 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530765 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530766 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530767 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530768 self.wh_data.valuation_rate = sle.valuation_rate
769 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530770 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
771 self.wh_data.valuation_rate
772 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530773 if self.valuation_method != "Moving Average":
774 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530775 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530776 if self.valuation_method == "Moving Average":
777 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530778 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530779 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
780 self.wh_data.valuation_rate
781 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530782 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530783 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530784
Rushabh Mehta54047782013-12-26 11:07:46 +0530785 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530786 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530787 if not self.wh_data.qty_after_transaction:
788 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530789
Nabin Haita77b8c92020-12-21 14:45:50 +0530790 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
791 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530792
Nabin Hait902e8602013-01-08 18:29:24 +0530793 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530794 sle.qty_after_transaction = self.wh_data.qty_after_transaction
795 sle.valuation_rate = self.wh_data.valuation_rate
796 sle.stock_value = self.wh_data.stock_value
797 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530798
rohitwaghchaurea8216b92023-11-09 12:22:26 +0530799 if not sle.is_adjustment_entry or not self.args.get("sle_id"):
800 sle.stock_value_difference = stock_value_difference
801
802 sle.doctype = "Stock Ledger Entry"
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530803 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530804
rohitwaghchaure3e77c0b2023-11-14 19:27:41 +0530805 if not self.args.get("sle_id") or (
806 sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle
807 ):
Ankush Menat701878f2022-03-01 18:08:29 +0530808 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530809
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530810 def get_serialized_values(self, sle):
811 incoming_rate = flt(sle.incoming_rate)
812 actual_qty = flt(sle.actual_qty)
813 serial_nos = cstr(sle.serial_no).split("\n")
814
815 if incoming_rate < 0:
816 # wrong incoming rate
817 incoming_rate = self.wh_data.valuation_rate
818
819 stock_value_change = 0
820 if actual_qty > 0:
821 stock_value_change = actual_qty * incoming_rate
822 else:
823 # In case of delivery/stock issue, get average purchase rate
824 # of serial nos of current entry
825 if not sle.is_cancelled:
826 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
827 stock_value_change = -1 * outgoing_value
828 else:
829 stock_value_change = actual_qty * sle.outgoing_rate
830
831 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
832
833 if new_stock_qty > 0:
834 new_stock_value = (
835 self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
836 ) + stock_value_change
837 if new_stock_value >= 0:
838 # calculate new valuation rate only if stock value is positive
839 # else it remains the same as that of previous entry
840 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
841
842 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
843 allow_zero_rate = self.check_if_allow_zero_valuation_rate(
844 sle.voucher_type, sle.voucher_detail_no
845 )
846 if not allow_zero_rate:
847 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
848
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530849 def reset_actual_qty_for_stock_reco(self, sle):
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530850 doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
851 doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
852
853 if sle.actual_qty < 0:
854 sle.actual_qty = (
855 flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
856 * -1
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530857 )
858
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530859 if abs(sle.actual_qty) == 0.0:
860 sle.is_cancelled = 1
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530861
Rohit Waghchaure64cb1152024-01-15 19:39:41 +0530862 if sle.serial_and_batch_bundle and frappe.get_cached_value(
863 "Item", sle.item_code, "has_serial_no"
864 ):
865 self.update_serial_no_status(sle)
866
867 def update_serial_no_status(self, sle):
868 from erpnext.stock.serial_batch_bundle import get_serial_nos
869
870 serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
871 if not serial_nos:
872 return
873
874 warehouse = None
875 status = "Inactive"
876
877 if sle.actual_qty > 0:
878 warehouse = sle.warehouse
879 status = "Active"
880
881 sn_table = frappe.qb.DocType("Serial No")
882
883 query = (
884 frappe.qb.update(sn_table)
885 .set(sn_table.warehouse, warehouse)
886 .set(sn_table.status, status)
887 .where(sn_table.name.isin(serial_nos))
888 )
889
890 query.run()
891
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530892 def calculate_valuation_for_serial_batch_bundle(self, sle):
893 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
894
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +0530895 doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530896 doc.calculate_qty_and_amount(save=True)
897
898 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
899
s-aga-r1e15a3c2024-02-02 13:07:26 +0530900 precision = doc.precision("total_qty")
901 self.wh_data.qty_after_transaction += flt(doc.total_qty, precision)
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530902 if self.wh_data.qty_after_transaction:
s-aga-r1e15a3c2024-02-02 13:07:26 +0530903 self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt(
904 self.wh_data.qty_after_transaction, precision
905 )
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530906
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530907 def validate_negative_stock(self, sle):
908 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530909 validate negative stock for entries current datetime onwards
910 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530911 """
s-aga-rf0acb202023-04-12 14:13:54 +0530912 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530913 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530914
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530915 if diff < 0 and abs(diff) > 0.0001:
916 # negative stock!
917 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530918 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530919 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530920 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530921 return True
922
Nabin Haita77b8c92020-12-21 14:45:50 +0530923 def get_dynamic_incoming_outgoing_rate(self, sle):
924 # Get updated incoming/outgoing rate from transaction
925 if sle.recalculate_rate:
926 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
927
928 if flt(sle.actual_qty) >= 0:
929 sle.incoming_rate = rate
930 else:
931 sle.outgoing_rate = rate
932
933 def get_incoming_outgoing_rate_from_transaction(self, sle):
934 rate = 0
935 # Material Transfer, Repack, Manufacturing
936 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530937 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530938 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
939 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530940 elif sle.voucher_type in (
941 "Purchase Receipt",
942 "Purchase Invoice",
943 "Delivery Note",
944 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530945 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530946 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530947 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530948 from erpnext.controllers.sales_and_purchase_return import (
949 get_rate_for_return, # don't move this import to top
950 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530951
mergify[bot]07175362023-12-21 14:40:52 +0530952 if self.valuation_method == "Moving Average":
953 rate = get_incoming_rate(
954 {
955 "item_code": sle.item_code,
956 "warehouse": sle.warehouse,
957 "posting_date": sle.posting_date,
958 "posting_time": sle.posting_time,
959 "qty": sle.actual_qty,
960 "serial_no": sle.get("serial_no"),
961 "batch_no": sle.get("batch_no"),
962 "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
963 "company": sle.company,
964 "voucher_type": sle.voucher_type,
965 "voucher_no": sle.voucher_no,
966 "allow_zero_valuation": self.allow_zero_rate,
967 "sle": sle.name,
968 }
969 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530970
mergify[bot]07175362023-12-21 14:40:52 +0530971 else:
972 rate = get_rate_for_return(
973 sle.voucher_type,
974 sle.voucher_no,
975 sle.item_code,
976 voucher_detail_no=sle.voucher_detail_no,
977 sle=sle,
978 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530979 elif (
980 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530981 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530982 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530983 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530984 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530985 else:
986 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530987 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530988 elif sle.voucher_type == "Subcontracting Receipt":
989 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530990 else:
991 rate_field = "incoming_rate"
992
993 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530994 item_code, incoming_rate = frappe.db.get_value(
995 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
996 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530997
998 if item_code == sle.item_code:
999 rate = incoming_rate
1000 else:
1001 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1002 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +05301003 elif sle == "Subcontracting Receipt":
1004 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +05301005 else:
1006 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +05301007
Ankush Menat494bd9e2022-03-28 18:52:46 +05301008 rate = frappe.db.get_value(
1009 ref_doctype,
1010 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
1011 rate_field,
1012 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301013
1014 return rate
1015
1016 def update_outgoing_rate_on_transaction(self, sle):
1017 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301018 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
1019 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +05301020 """
1021 if sle.actual_qty and sle.voucher_detail_no:
1022 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
1023
1024 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
1025 self.update_rate_on_stock_entry(sle, outgoing_rate)
1026 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1027 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
1028 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
1029 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301030 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
1031 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +05301032 elif sle.voucher_type == "Stock Reconciliation":
1033 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +05301034
1035 def update_rate_on_stock_entry(self, sle, outgoing_rate):
1036 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
1037
Ankush Menat701878f2022-03-01 18:08:29 +05301038 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
1039 if not sle.dependant_sle_voucher_detail_no:
1040 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +05301041
1042 def recalculate_amounts_in_stock_entry(self, voucher_no):
1043 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +05301044 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
1045 stock_entry.db_update()
1046 for d in stock_entry.items:
1047 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +05301048
Nabin Haita77b8c92020-12-21 14:45:50 +05301049 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
1050 # Update item's incoming rate on transaction
1051 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
1052 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301053 frappe.db.set_value(
1054 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
1055 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301056 else:
1057 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +05301058 frappe.db.set_value(
1059 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +05301060 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301061 "incoming_rate",
1062 outgoing_rate,
1063 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301064
1065 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
1066 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301067 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
1068 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
1069 ):
1070 frappe.db.set_value(
1071 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
1072 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301073 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301074 frappe.db.set_value(
1075 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
1076 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301077
1078 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +05301079 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +05301080 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +05301081 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301082 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +05301083 d.db_update()
1084
Sagar Sharma323bdf82022-05-17 15:14:07 +05301085 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +05301086 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
1087 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301088 else:
1089 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301090 "Subcontracting Receipt Supplied Item",
1091 sle.voucher_detail_no,
1092 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +05301093 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301094
s-aga-ra6cb6c62023-05-03 09:51:58 +05301095 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301096 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +05301097 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301098 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +05301099 d.db_update()
1100
s-aga-r88a3f652023-05-30 16:54:28 +05301101 def update_rate_on_stock_reconciliation(self, sle):
1102 if not sle.serial_no and not sle.batch_no:
1103 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
1104
1105 for item in sr.items:
1106 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +05301107 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +05301108 continue
1109
1110 previous_sle = get_previous_sle(
1111 {
1112 "item_code": item.item_code,
1113 "warehouse": item.warehouse,
1114 "posting_date": sr.posting_date,
1115 "posting_time": sr.posting_time,
1116 "sle": sle.name,
1117 }
1118 )
1119
1120 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
1121 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
1122 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
1123
1124 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +05301125 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +05301126 item.amount_difference = item.amount - item.current_amount
1127 else:
1128 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1129 sr.db_update()
1130
1131 for item in sr.items:
1132 item.db_update()
1133
Nabin Hait328c4f92020-01-02 19:00:32 +05301134 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1135 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301136 all_serial_nos = frappe.get_all(
1137 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1138 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301139
Ankush Menat494bd9e2022-03-28 18:52:46 +05301140 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 +05301141
1142 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301143 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301144 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301145 incoming_rate = frappe.db.sql(
1146 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301147 select incoming_rate
1148 from `tabStock Ledger Entry`
1149 where
1150 company = %s
1151 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301152 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301153 and (serial_no = %s
1154 or serial_no like %s
1155 or serial_no like %s
1156 or serial_no like %s
1157 )
1158 order by posting_date desc
1159 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301160 """,
1161 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1162 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301163
1164 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1165
1166 return incoming_values
1167
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301168 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301169 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301170 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301171 if new_stock_qty >= 0:
1172 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301173 if flt(self.wh_data.qty_after_transaction) <= 0:
1174 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301175 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301176 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1177 actual_qty * sle.incoming_rate
1178 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301179
Nabin Haita77b8c92020-12-21 14:45:50 +05301180 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301181
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301182 elif sle.outgoing_rate:
1183 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301184 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1185 actual_qty * sle.outgoing_rate
1186 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301187
Nabin Haita77b8c92020-12-21 14:45:50 +05301188 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301189 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301190 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301191 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301192 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1193 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301194
Nabin Haita77b8c92020-12-21 14:45:50 +05301195 if not self.wh_data.valuation_rate and actual_qty > 0:
1196 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301197
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301198 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001199 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301200 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301201 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1202 sle.voucher_type, sle.voucher_detail_no
1203 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001204 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301205 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301206
Ankush Menatf089d392022-02-02 12:51:21 +05301207 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301208 incoming_rate = flt(sle.incoming_rate)
1209 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301210 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301211
Ankush Menat494bd9e2022-03-28 18:52:46 +05301212 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1213 self.wh_data.qty_after_transaction + actual_qty
1214 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301215
Ankush Menat97e18a12022-01-15 17:42:25 +05301216 if self.valuation_method == "LIFO":
1217 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1218 else:
1219 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1220
Ankush Menatb534fee2022-02-19 20:58:36 +05301221 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1222
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301223 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301224 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301225 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301226
Ankush Menat4b29fb62021-12-18 18:40:22 +05301227 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301228 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1229 sle.voucher_type, sle.voucher_detail_no
1230 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301231 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301232 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301233 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301234 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301235
Ankush Menat494bd9e2022-03-28 18:52:46 +05301236 stock_queue.remove_stock(
1237 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1238 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301239
Ankush Menatb534fee2022-02-19 20:58:36 +05301240 _qty, stock_value = stock_queue.get_total_stock_and_value()
1241
1242 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301243
Ankush Menat97e18a12022-01-15 17:42:25 +05301244 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301245 self.wh_data.stock_value = round_off_if_near_zero(
1246 self.wh_data.stock_value + stock_value_difference
1247 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301248
Nabin Haita77b8c92020-12-21 14:45:50 +05301249 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301250 self.wh_data.stock_queue.append(
1251 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1252 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301253
Ankush Menatb534fee2022-02-19 20:58:36 +05301254 if self.wh_data.qty_after_transaction:
1255 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1256
Ankush Menatce0514c2022-02-15 11:41:41 +05301257 def update_batched_values(self, sle):
1258 incoming_rate = flt(sle.incoming_rate)
1259 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301260
Ankush Menat494bd9e2022-03-28 18:52:46 +05301261 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1262 self.wh_data.qty_after_transaction + actual_qty
1263 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301264
1265 if actual_qty > 0:
1266 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301267 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301268 outgoing_rate = get_batch_incoming_rate(
1269 item_code=sle.item_code,
1270 warehouse=sle.warehouse,
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301271 batch_no=sle.batch_no,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301272 posting_date=sle.posting_date,
1273 posting_time=sle.posting_time,
1274 creation=sle.creation,
1275 )
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301276
Ankush Menataba7a7c2022-02-19 19:36:28 +05301277 if outgoing_rate is None:
1278 # This can *only* happen if qty available for the batch is zero.
1279 # in such case fall back various other rates.
1280 # future entries will correct the overall accounting as each
1281 # batch individually uses moving average rates.
1282 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301283 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301284
Ankush Menat494bd9e2022-03-28 18:52:46 +05301285 self.wh_data.stock_value = round_off_if_near_zero(
1286 self.wh_data.stock_value + stock_value_difference
1287 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301288 if self.wh_data.qty_after_transaction:
1289 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301290
Javier Wong9b11d9b2017-04-14 18:24:04 +08001291 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301292 ref_item_dt = ""
1293
1294 if voucher_type == "Stock Entry":
1295 ref_item_dt = voucher_type + " Detail"
1296 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1297 ref_item_dt = voucher_type + " Item"
1298
1299 if ref_item_dt:
1300 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1301 else:
1302 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301303
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301304 def get_fallback_rate(self, sle) -> float:
1305 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301306 This should only get used for negative stock."""
1307 return get_valuation_rate(
1308 sle.item_code,
1309 sle.warehouse,
1310 sle.voucher_type,
1311 sle.voucher_no,
1312 self.allow_zero_rate,
1313 currency=erpnext.get_company_currency(sle.company),
1314 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301315 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301316
Nabin Haita77b8c92020-12-21 14:45:50 +05301317 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301318 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301319 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1320 sle = sle[0] if sle else frappe._dict()
1321 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301322
Nabin Haita77b8c92020-12-21 14:45:50 +05301323 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301324 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301325 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301326
1327 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301328 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301329 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301330 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301331
Ankush Menat494bd9e2022-03-28 18:52:46 +05301332 if (
1333 exceptions[0]["voucher_type"],
1334 exceptions[0]["voucher_no"],
1335 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301336
Nabin Haita77b8c92020-12-21 14:45:50 +05301337 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301338 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301339 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1340 frappe.get_desk_link("Warehouse", warehouse),
1341 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301342 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301343 msg = _(
1344 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1345 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301346 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301347 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1348 frappe.get_desk_link("Warehouse", warehouse),
1349 exceptions[0]["posting_date"],
1350 exceptions[0]["posting_time"],
1351 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1352 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301353
Nabin Haita77b8c92020-12-21 14:45:50 +05301354 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301355 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301356 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301357
1358 if allowed_qty > 0:
1359 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1360 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1361 )
1362 else:
1363 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1364 msg,
1365 )
s-aga-rf0acb202023-04-12 14:13:54 +05301366
Nabin Haita77b8c92020-12-21 14:45:50 +05301367 msg_list.append(msg)
1368
1369 if msg_list:
1370 message = "\n\n".join(msg_list)
1371 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301372 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301373 else:
1374 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301375
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301376 def update_bin_data(self, sle):
1377 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301378 values_to_update = {
1379 "actual_qty": sle.qty_after_transaction,
1380 "stock_value": sle.stock_value,
1381 }
1382
1383 if sle.valuation_rate is not None:
1384 values_to_update["valuation_rate"] = sle.valuation_rate
1385
1386 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301387
Nabin Haita77b8c92020-12-21 14:45:50 +05301388 def update_bin(self):
1389 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301390 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301391 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301392
Ankush Menat494bd9e2022-03-28 18:52:46 +05301393 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301394 if data.valuation_rate is not None:
1395 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301396 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301397
marination8418c4b2021-06-22 21:35:25 +05301398
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301399def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301400 """get stock ledger entries filtered by specific posting datetime conditions"""
1401
Ankush Menat494bd9e2022-03-28 18:52:46 +05301402 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301403 if not args.get("posting_date"):
1404 args["posting_date"] = "1900-01-01"
1405 if not args.get("posting_time"):
1406 args["posting_time"] = "00:00"
1407
1408 voucher_condition = ""
1409 if exclude_current_voucher:
1410 voucher_no = args.get("voucher_no")
1411 voucher_condition = f"and voucher_no != '{voucher_no}'"
1412
Ankush Menat494bd9e2022-03-28 18:52:46 +05301413 sle = frappe.db.sql(
1414 """
marination8418c4b2021-06-22 21:35:25 +05301415 select *, timestamp(posting_date, posting_time) as "timestamp"
1416 from `tabStock Ledger Entry`
1417 where item_code = %(item_code)s
1418 and warehouse = %(warehouse)s
1419 and is_cancelled = 0
1420 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301421 and (
1422 posting_date < %(posting_date)s or
1423 (
1424 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301425 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301426 )
1427 )
marination8418c4b2021-06-22 21:35:25 +05301428 order by timestamp(posting_date, posting_time) desc, creation desc
1429 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301430 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301431 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301432 ),
1433 args,
1434 as_dict=1,
1435 )
marination8418c4b2021-06-22 21:35:25 +05301436
1437 return sle[0] if sle else frappe._dict()
1438
Ankush Menat494bd9e2022-03-28 18:52:46 +05301439
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301440def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301441 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301442 get the last sle on or before the current time-bucket,
1443 to get actual qty before transaction, this function
1444 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301445
Ankush Menat494bd9e2022-03-28 18:52:46 +05301446 args = {
1447 "item_code": "ABC",
1448 "warehouse": "XYZ",
1449 "posting_date": "2012-12-12",
1450 "posting_time": "12:00",
1451 "sle": "name of reference Stock Ledger Entry"
1452 }
Anand Doshi1b531862013-01-10 19:29:51 +05301453 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301454 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301455 sle = get_stock_ledger_entries(
1456 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1457 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301458 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301459
Ankush Menat494bd9e2022-03-28 18:52:46 +05301460
1461def get_stock_ledger_entries(
1462 previous_sle,
1463 operator=None,
1464 order="desc",
1465 limit=None,
1466 for_update=False,
1467 debug=False,
1468 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301469 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301470):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301471 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301472 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1473 operator
1474 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301475 if previous_sle.get("warehouse"):
1476 conditions += " and warehouse = %(warehouse)s"
1477 elif previous_sle.get("warehouse_condition"):
1478 conditions += " and " + previous_sle.get("warehouse_condition")
1479
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301480 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301481 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1482 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301483 conditions += (
1484 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301485 (
1486 serial_no = {0}
1487 or serial_no like {1}
1488 or serial_no like {2}
1489 or serial_no like {3}
1490 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301491 """
1492 ).format(
1493 frappe.db.escape(serial_no),
1494 frappe.db.escape("{}\n%".format(serial_no)),
1495 frappe.db.escape("%\n{}".format(serial_no)),
1496 frappe.db.escape("%\n{}\n%".format(serial_no)),
1497 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301498
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301499 if not previous_sle.get("posting_date"):
1500 previous_sle["posting_date"] = "1900-01-01"
1501 if not previous_sle.get("posting_time"):
1502 previous_sle["posting_time"] = "00:00"
1503
1504 if operator in (">", "<=") and previous_sle.get("name"):
1505 conditions += " and name!=%(name)s"
1506
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301507 if extra_cond:
1508 conditions += f"{extra_cond}"
1509
Ankush Menat494bd9e2022-03-28 18:52:46 +05301510 return frappe.db.sql(
1511 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301512 select *, timestamp(posting_date, posting_time) as "timestamp"
1513 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301514 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301515 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301516 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301517 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301518 %(limit)s %(for_update)s"""
1519 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301520 "conditions": conditions,
1521 "limit": limit or "",
1522 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301523 "order": order,
1524 },
1525 previous_sle,
1526 as_dict=1,
1527 debug=debug,
1528 )
1529
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301530
Nabin Haita77b8c92020-12-21 14:45:50 +05301531def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301532 return frappe.db.get_value(
1533 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301534 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301535 [
1536 "item_code",
1537 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301538 "actual_qty",
1539 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301540 "posting_date",
1541 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301542 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301543 "timestamp(posting_date, posting_time) as timestamp",
1544 ],
1545 as_dict=1,
1546 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301547
Ankush Menatce0514c2022-02-15 11:41:41 +05301548
Ankush Menat494bd9e2022-03-28 18:52:46 +05301549def get_batch_incoming_rate(
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301550 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301551):
1552
Ankush Menat102fff22022-02-19 15:51:04 +05301553 sle = frappe.qb.DocType("Stock Ledger Entry")
1554
Ankush Menate1c16872022-04-21 20:01:48 +05301555 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301556 posting_date, posting_time
1557 )
Ankush Menat102fff22022-02-19 15:51:04 +05301558 if creation:
1559 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301560 CombineDatetime(sle.posting_date, sle.posting_time)
1561 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301562 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301563
1564 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301565 frappe.qb.from_(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301566 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301567 .where(
1568 (sle.item_code == item_code)
1569 & (sle.warehouse == warehouse)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301570 & (sle.batch_no == batch_no)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301571 & (sle.is_cancelled == 0)
1572 )
1573 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301574 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301575
1576 if batch_details and batch_details[0].batch_qty:
1577 return batch_details[0].batch_value / batch_details[0].batch_qty
1578
1579
Ankush Menat494bd9e2022-03-28 18:52:46 +05301580def get_valuation_rate(
1581 item_code,
1582 warehouse,
1583 voucher_type,
1584 voucher_no,
1585 allow_zero_rate=False,
1586 currency=None,
1587 company=None,
1588 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301589 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301590 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301591):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301592
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301593 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1594
Ankush Menatf7ffe042021-11-01 13:21:14 +05301595 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301596 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301597
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301598 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1599 table = frappe.qb.DocType("Stock Ledger Entry")
1600 query = (
1601 frappe.qb.from_(table)
1602 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1603 .where(
1604 (table.item_code == item_code)
1605 & (table.warehouse == warehouse)
1606 & (table.batch_no == batch_no)
1607 & (table.is_cancelled == 0)
1608 & (table.voucher_no != voucher_no)
1609 & (table.voucher_type != voucher_type)
1610 )
1611 )
1612
1613 last_valuation_rate = query.run()
1614 if last_valuation_rate:
1615 return flt(last_valuation_rate[0][0])
1616
Ankush Menat342d09a2022-02-19 14:28:51 +05301617 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301618 if warehouse and serial_and_batch_bundle:
1619 batch_obj = BatchNoValuation(
1620 sle=frappe._dict(
1621 {
1622 "item_code": item_code,
1623 "warehouse": warehouse,
1624 "actual_qty": -1,
1625 "serial_and_batch_bundle": serial_and_batch_bundle,
1626 }
1627 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301628 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301629
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301630 return batch_obj.get_incoming_rate()
1631
Ankush Menatf7ffe042021-11-01 13:21:14 +05301632 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301633 if last_valuation_rate := frappe.db.sql(
1634 """select valuation_rate
1635 from `tabStock Ledger Entry` force index (item_warehouse)
1636 where
1637 item_code = %s
1638 AND warehouse = %s
1639 AND valuation_rate >= 0
1640 AND is_cancelled = 0
1641 AND NOT (voucher_no = %s AND voucher_type = %s)
1642 order by posting_date desc, posting_time desc, name desc limit 1""",
1643 (item_code, warehouse, voucher_no, voucher_type),
1644 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301645 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301646
1647 # If negative stock allowed, and item delivered without any incoming entry,
1648 # system does not found any SLE, then take valuation rate from Item
1649 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301650
1651 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301652 # try Item Standard rate
1653 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301654
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301655 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301656 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301657 valuation_rate = frappe.db.get_value(
1658 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1659 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301660
Ankush Menat494bd9e2022-03-28 18:52:46 +05301661 if (
1662 not allow_zero_rate
1663 and not valuation_rate
1664 and raise_error_if_no_rate
1665 and cint(erpnext.is_perpetual_inventory_enabled(company))
1666 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301667 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301668
Ankush Menat494bd9e2022-03-28 18:52:46 +05301669 message = _(
1670 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1671 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301672 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301673 solutions = (
1674 "<li>"
1675 + _(
1676 "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."
1677 ).format(voucher_type)
1678 + "</li>"
1679 )
1680 solutions += (
1681 "<li>"
1682 + _("If not, you can Cancel / Submit this entry")
1683 + " {0} ".format(frappe.bold("after"))
1684 + _("performing either one below:")
1685 + "</li>"
1686 )
Marica97715f22020-05-11 20:45:37 +05301687 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1688 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1689 msg = message + solutions + sub_solutions + "</li>"
1690
1691 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301692
1693 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301694
Ankush Menat494bd9e2022-03-28 18:52:46 +05301695
Ankush Menate7109c12021-08-26 16:40:45 +05301696def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301697 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301698 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301699 qty_shift = args.actual_qty
1700
Ankush Menat7c839c42022-05-06 12:09:08 +05301701 args["time_format"] = "%H:%i:%s"
1702
marination8418c4b2021-06-22 21:35:25 +05301703 # find difference/shift in qty caused by stock reconciliation
1704 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301705 qty_shift = get_stock_reco_qty_shift(args)
1706
1707 # find the next nearest stock reco so that we only recalculate SLEs till that point
1708 next_stock_reco_detail = get_next_stock_reco(args)
1709 if next_stock_reco_detail:
1710 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301711
marination40389772021-07-02 17:13:45 +05301712 # add condition to update SLEs before this date & time
1713 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301714
Ankush Menat494bd9e2022-03-28 18:52:46 +05301715 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301716 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301717 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301718 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301719 where
1720 item_code = %(item_code)s
1721 and warehouse = %(warehouse)s
1722 and voucher_no != %(voucher_no)s
1723 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301724 and (
1725 posting_date > %(posting_date)s or
1726 (
1727 posting_date = %(posting_date)s and
1728 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1729 )
1730 )
marination40389772021-07-02 17:13:45 +05301731 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301732 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301733 args,
1734 )
Nabin Hait186a0452021-02-18 14:14:21 +05301735
1736 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1737
Ankush Menat494bd9e2022-03-28 18:52:46 +05301738
marination40389772021-07-02 17:13:45 +05301739def get_stock_reco_qty_shift(args):
1740 stock_reco_qty_shift = 0
1741 if args.get("is_cancelled"):
1742 if args.get("previous_qty_after_transaction"):
1743 # get qty (balance) that was set at submission
1744 last_balance = args.get("previous_qty_after_transaction")
1745 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1746 else:
1747 stock_reco_qty_shift = flt(args.actual_qty)
1748 else:
1749 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301750 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301751 "qty_after_transaction"
1752 )
marination40389772021-07-02 17:13:45 +05301753
1754 if last_balance is not None:
1755 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1756 else:
1757 stock_reco_qty_shift = args.qty_after_transaction
1758
1759 return stock_reco_qty_shift
1760
Ankush Menat494bd9e2022-03-28 18:52:46 +05301761
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301762def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301763 """Returns next nearest stock reconciliaton's details."""
1764
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301765 sle = frappe.qb.DocType("Stock Ledger Entry")
1766
1767 query = (
1768 frappe.qb.from_(sle)
1769 .select(
1770 sle.name,
1771 sle.posting_date,
1772 sle.posting_time,
1773 sle.creation,
1774 sle.voucher_no,
1775 sle.item_code,
1776 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301777 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301778 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301779 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301780 )
1781 .where(
1782 (sle.item_code == kwargs.get("item_code"))
1783 & (sle.warehouse == kwargs.get("warehouse"))
1784 & (sle.voucher_type == "Stock Reconciliation")
1785 & (sle.voucher_no != kwargs.get("voucher_no"))
1786 & (sle.is_cancelled == 0)
1787 & (
1788 (
1789 CombineDatetime(sle.posting_date, sle.posting_time)
1790 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301791 )
1792 | (
1793 (
1794 CombineDatetime(sle.posting_date, sle.posting_time)
1795 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301796 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301797 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301798 )
1799 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301800 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301801 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1802 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301803 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301804 )
1805
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301806 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301807 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301808
1809 return query.run(as_dict=True)
1810
marination40389772021-07-02 17:13:45 +05301811
1812def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301813 return f"""
1814 and
1815 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1816 or (
1817 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1818 and creation < '{detail.creation}'
1819 )
1820 )"""
1821
Ankush Menat494bd9e2022-03-28 18:52:46 +05301822
Ankush Menate7109c12021-08-26 16:40:45 +05301823def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301824 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301825 return
barredterraeb9ee3f2023-12-05 11:22:55 +01001826 if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
Ankush Menat5eba5752021-12-07 23:03:52 +05301827 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301828
Ankush Menat5eba5752021-12-07 23:03:52 +05301829 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301830
1831 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301832 message = _(
1833 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1834 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301835 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301836 frappe.get_desk_link("Item", args.item_code),
1837 frappe.get_desk_link("Warehouse", args.warehouse),
1838 neg_sle[0]["posting_date"],
1839 neg_sle[0]["posting_time"],
1840 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1841 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301842
Ankush Menat494bd9e2022-03-28 18:52:46 +05301843 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301844
s-aga-rd9e28432023-10-27 16:35:35 +05301845 if args.batch_no:
1846 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1847 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1848 message = _(
1849 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1850 ).format(
1851 abs(neg_batch_sle[0]["cumulative_total"]),
1852 frappe.get_desk_link("Batch", args.batch_no),
1853 frappe.get_desk_link("Warehouse", args.warehouse),
1854 neg_batch_sle[0]["posting_date"],
1855 neg_batch_sle[0]["posting_time"],
1856 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1857 )
1858 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301859
s-aga-r73b65ac2023-11-01 18:35:07 +05301860 if args.reserved_stock:
1861 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301862
Nabin Haita77b8c92020-12-21 14:45:50 +05301863
Maricad6078aa2022-06-17 15:13:13 +05301864def is_negative_with_precision(neg_sle, is_batch=False):
1865 """
1866 Returns whether system precision rounded qty is insufficient.
1867 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1868 """
1869
1870 if not neg_sle:
1871 return False
1872
1873 field = "cumulative_total" if is_batch else "qty_after_transaction"
1874 precision = cint(frappe.db.get_default("float_precision")) or 2
1875 qty_deficit = flt(neg_sle[0][field], precision)
1876
1877 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1878
1879
Nabin Haita77b8c92020-12-21 14:45:50 +05301880def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301881 return frappe.db.sql(
1882 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301883 select
1884 qty_after_transaction, posting_date, posting_time,
1885 voucher_type, voucher_no
1886 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301887 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301888 item_code = %(item_code)s
1889 and warehouse = %(warehouse)s
1890 and voucher_no != %(voucher_no)s
1891 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1892 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301893 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301894 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301895 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301896 """,
1897 args,
1898 as_dict=1,
1899 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301900
Ankush Menat5eba5752021-12-07 23:03:52 +05301901
1902def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301903 return frappe.db.sql(
1904 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301905 with batch_ledger as (
1906 select
1907 posting_date, posting_time, voucher_type, voucher_no,
1908 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1909 from `tabStock Ledger Entry`
1910 where
1911 item_code = %(item_code)s
1912 and warehouse = %(warehouse)s
1913 and batch_no=%(batch_no)s
1914 and is_cancelled = 0
1915 order by posting_date, posting_time, creation
1916 )
1917 select * from batch_ledger
1918 where
1919 cumulative_total < 0.0
1920 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1921 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301922 """,
1923 args,
1924 as_dict=1,
1925 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301926
1927
s-aga-rd9e28432023-10-27 16:35:35 +05301928def validate_reserved_stock(kwargs):
1929 if kwargs.serial_no:
1930 serial_nos = kwargs.serial_no.split("\n")
1931 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1932
s-aga-re1a87a82023-10-31 18:41:58 +05301933 elif kwargs.batch_no:
1934 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1935
s-aga-rd9e28432023-10-27 16:35:35 +05301936 elif kwargs.serial_and_batch_bundle:
1937 sbb_entries = frappe.db.get_all(
1938 "Serial and Batch Entry",
1939 {
1940 "parenttype": "Serial and Batch Bundle",
1941 "parent": kwargs.serial_and_batch_bundle,
1942 "docstatus": 1,
1943 },
s-aga-re1a87a82023-10-31 18:41:58 +05301944 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301945 )
s-aga-rd9e28432023-10-27 16:35:35 +05301946
s-aga-re1a87a82023-10-31 18:41:58 +05301947 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301948 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301949 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1950 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301951
s-aga-r92317062023-11-02 10:36:00 +05301952 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1953 precision = cint(frappe.db.get_default("float_precision")) or 2
1954 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301955
s-aga-r92317062023-11-02 10:36:00 +05301956 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1957 if diff < 0 and abs(diff) > 0.0001:
1958 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1959 abs(diff),
1960 frappe.get_desk_link("Item", kwargs.item_code),
1961 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1962 nowdate(),
1963 nowtime(),
1964 )
1965 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301966
s-aga-rd9e28432023-10-27 16:35:35 +05301967
1968def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1969 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1970 item_code, warehouse, serial_nos
1971 ):
1972 if common_serial_nos := list(
1973 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1974 ):
1975 msg = _(
1976 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1977 )
1978 msg += "<br />"
1979 msg += _("Example: Serial No {0} reserved in {1}.").format(
1980 frappe.bold(common_serial_nos[0]),
1981 frappe.get_desk_link(
1982 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1983 ),
1984 )
1985 frappe.throw(msg, title=_("Reserved Serial No."))
1986
1987
s-aga-re1a87a82023-10-31 18:41:58 +05301988def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1989 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1990 available_batches = get_available_batches(
1991 frappe._dict(
1992 {
1993 "item_code": item_code,
1994 "warehouse": warehouse,
1995 "posting_date": nowdate(),
1996 "posting_time": nowtime(),
1997 }
1998 )
1999 )
2000 available_batches_map = {row.batch_no: row.qty for row in available_batches}
2001 precision = cint(frappe.db.get_default("float_precision")) or 2
2002
2003 for batch_no in batch_nos:
2004 diff = flt(
2005 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
2006 )
2007 if diff < 0 and abs(diff) > 0.0001:
2008 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
2009 abs(diff),
2010 frappe.get_desk_link("Batch", batch_no),
2011 frappe.get_desk_link("Warehouse", warehouse),
2012 nowdate(),
2013 nowtime(),
2014 )
2015 frappe.throw(msg, title=_("Reserved Stock for Batch"))
2016
2017
Ankush Menateb8b4242022-02-12 13:08:28 +05302018def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
2019 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
2020 return True
2021 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
2022 return True
2023 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05302024
2025
2026def get_incoming_rate_for_inter_company_transfer(sle) -> float:
2027 """
2028 For inter company transfer, incoming rate is the average of the outgoing rate
2029 """
2030 rate = 0.0
2031
2032 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
2033
2034 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
2035
2036 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
2037
2038 if reference_name:
2039 rate = frappe.get_cached_value(
2040 doctype,
2041 reference_name,
2042 "incoming_rate",
2043 )
2044
2045 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05302046
2047
2048def is_internal_transfer(sle):
2049 data = frappe.get_cached_value(
2050 sle.voucher_type,
2051 sle.voucher_no,
2052 ["is_internal_supplier", "represents_company", "company"],
2053 as_dict=True,
2054 )
2055
2056 if data.is_internal_supplier and data.represents_company == data.company:
2057 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302058
2059
2060def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
2061 table = frappe.qb.DocType("Stock Ledger Entry")
2062
2063 query = (
2064 frappe.qb.from_(table)
2065 .select(Sum(table.stock_value_difference).as_("value"))
2066 .where(
2067 (table.is_cancelled == 0)
2068 & (table.item_code == item_code)
2069 & (table.warehouse == warehouse)
2070 & (
2071 (table.posting_date < posting_date)
2072 | ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
2073 )
2074 )
2075 )
2076
2077 if voucher_no:
2078 query = query.where(table.voucher_no != voucher_no)
2079
2080 difference_amount = query.run()
2081 return flt(difference_amount[0][0]) if difference_amount else 0