blob: 45764f3ec0a5db122c7963d78b8b5e98ee4377da [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
900 self.wh_data.qty_after_transaction += doc.total_qty
901 if self.wh_data.qty_after_transaction:
902 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
903
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530904 def validate_negative_stock(self, sle):
905 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530906 validate negative stock for entries current datetime onwards
907 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530908 """
s-aga-rf0acb202023-04-12 14:13:54 +0530909 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530910 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530911
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530912 if diff < 0 and abs(diff) > 0.0001:
913 # negative stock!
914 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530915 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530916 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530917 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530918 return True
919
Nabin Haita77b8c92020-12-21 14:45:50 +0530920 def get_dynamic_incoming_outgoing_rate(self, sle):
921 # Get updated incoming/outgoing rate from transaction
922 if sle.recalculate_rate:
923 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
924
925 if flt(sle.actual_qty) >= 0:
926 sle.incoming_rate = rate
927 else:
928 sle.outgoing_rate = rate
929
930 def get_incoming_outgoing_rate_from_transaction(self, sle):
931 rate = 0
932 # Material Transfer, Repack, Manufacturing
933 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530934 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530935 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
936 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530937 elif sle.voucher_type in (
938 "Purchase Receipt",
939 "Purchase Invoice",
940 "Delivery Note",
941 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530942 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530943 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530944 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530945 from erpnext.controllers.sales_and_purchase_return import (
946 get_rate_for_return, # don't move this import to top
947 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530948
mergify[bot]07175362023-12-21 14:40:52 +0530949 if self.valuation_method == "Moving Average":
950 rate = get_incoming_rate(
951 {
952 "item_code": sle.item_code,
953 "warehouse": sle.warehouse,
954 "posting_date": sle.posting_date,
955 "posting_time": sle.posting_time,
956 "qty": sle.actual_qty,
957 "serial_no": sle.get("serial_no"),
958 "batch_no": sle.get("batch_no"),
959 "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"),
960 "company": sle.company,
961 "voucher_type": sle.voucher_type,
962 "voucher_no": sle.voucher_no,
963 "allow_zero_valuation": self.allow_zero_rate,
964 "sle": sle.name,
965 }
966 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530967
mergify[bot]07175362023-12-21 14:40:52 +0530968 else:
969 rate = get_rate_for_return(
970 sle.voucher_type,
971 sle.voucher_no,
972 sle.item_code,
973 voucher_detail_no=sle.voucher_detail_no,
974 sle=sle,
975 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530976 elif (
977 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530978 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530979 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530980 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530981 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530982 else:
983 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530984 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530985 elif sle.voucher_type == "Subcontracting Receipt":
986 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530987 else:
988 rate_field = "incoming_rate"
989
990 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530991 item_code, incoming_rate = frappe.db.get_value(
992 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
993 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530994
995 if item_code == sle.item_code:
996 rate = incoming_rate
997 else:
998 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
999 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +05301000 elif sle == "Subcontracting Receipt":
1001 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +05301002 else:
1003 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +05301004
Ankush Menat494bd9e2022-03-28 18:52:46 +05301005 rate = frappe.db.get_value(
1006 ref_doctype,
1007 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
1008 rate_field,
1009 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301010
1011 return rate
1012
1013 def update_outgoing_rate_on_transaction(self, sle):
1014 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301015 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
1016 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +05301017 """
1018 if sle.actual_qty and sle.voucher_detail_no:
1019 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
1020
1021 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
1022 self.update_rate_on_stock_entry(sle, outgoing_rate)
1023 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
1024 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
1025 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
1026 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301027 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
1028 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +05301029 elif sle.voucher_type == "Stock Reconciliation":
1030 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +05301031
1032 def update_rate_on_stock_entry(self, sle, outgoing_rate):
1033 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
1034
Ankush Menat701878f2022-03-01 18:08:29 +05301035 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
1036 if not sle.dependant_sle_voucher_detail_no:
1037 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +05301038
1039 def recalculate_amounts_in_stock_entry(self, voucher_no):
1040 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +05301041 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
1042 stock_entry.db_update()
1043 for d in stock_entry.items:
1044 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +05301045
Nabin Haita77b8c92020-12-21 14:45:50 +05301046 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
1047 # Update item's incoming rate on transaction
1048 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
1049 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301050 frappe.db.set_value(
1051 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
1052 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301053 else:
1054 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +05301055 frappe.db.set_value(
1056 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +05301057 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301058 "incoming_rate",
1059 outgoing_rate,
1060 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301061
1062 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
1063 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301064 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
1065 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
1066 ):
1067 frappe.db.set_value(
1068 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
1069 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301070 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301071 frappe.db.set_value(
1072 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
1073 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301074
1075 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +05301076 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +05301077 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +05301078 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301079 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +05301080 d.db_update()
1081
Sagar Sharma323bdf82022-05-17 15:14:07 +05301082 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +05301083 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
1084 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +05301085 else:
1086 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301087 "Subcontracting Receipt Supplied Item",
1088 sle.voucher_detail_no,
1089 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +05301090 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301091
s-aga-ra6cb6c62023-05-03 09:51:58 +05301092 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301093 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +05301094 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +05301095 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +05301096 d.db_update()
1097
s-aga-r88a3f652023-05-30 16:54:28 +05301098 def update_rate_on_stock_reconciliation(self, sle):
1099 if not sle.serial_no and not sle.batch_no:
1100 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
1101
1102 for item in sr.items:
1103 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +05301104 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +05301105 continue
1106
1107 previous_sle = get_previous_sle(
1108 {
1109 "item_code": item.item_code,
1110 "warehouse": item.warehouse,
1111 "posting_date": sr.posting_date,
1112 "posting_time": sr.posting_time,
1113 "sle": sle.name,
1114 }
1115 )
1116
1117 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
1118 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
1119 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
1120
1121 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +05301122 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +05301123 item.amount_difference = item.amount - item.current_amount
1124 else:
1125 sr.difference_amount = sum([item.amount_difference for item in sr.items])
1126 sr.db_update()
1127
1128 for item in sr.items:
1129 item.db_update()
1130
Nabin Hait328c4f92020-01-02 19:00:32 +05301131 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1132 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301133 all_serial_nos = frappe.get_all(
1134 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1135 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301136
Ankush Menat494bd9e2022-03-28 18:52:46 +05301137 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 +05301138
1139 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301140 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301141 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301142 incoming_rate = frappe.db.sql(
1143 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301144 select incoming_rate
1145 from `tabStock Ledger Entry`
1146 where
1147 company = %s
1148 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301149 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301150 and (serial_no = %s
1151 or serial_no like %s
1152 or serial_no like %s
1153 or serial_no like %s
1154 )
1155 order by posting_date desc
1156 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301157 """,
1158 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1159 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301160
1161 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1162
1163 return incoming_values
1164
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301165 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301166 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301167 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301168 if new_stock_qty >= 0:
1169 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301170 if flt(self.wh_data.qty_after_transaction) <= 0:
1171 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301172 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301173 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1174 actual_qty * sle.incoming_rate
1175 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301176
Nabin Haita77b8c92020-12-21 14:45:50 +05301177 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301178
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301179 elif sle.outgoing_rate:
1180 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301181 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1182 actual_qty * sle.outgoing_rate
1183 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301184
Nabin Haita77b8c92020-12-21 14:45:50 +05301185 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301186 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301187 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301188 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301189 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1190 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301191
Nabin Haita77b8c92020-12-21 14:45:50 +05301192 if not self.wh_data.valuation_rate and actual_qty > 0:
1193 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301194
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301195 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001196 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301197 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301198 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1199 sle.voucher_type, sle.voucher_detail_no
1200 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001201 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301202 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301203
Ankush Menatf089d392022-02-02 12:51:21 +05301204 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301205 incoming_rate = flt(sle.incoming_rate)
1206 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301207 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301208
Ankush Menat494bd9e2022-03-28 18:52:46 +05301209 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1210 self.wh_data.qty_after_transaction + actual_qty
1211 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301212
Ankush Menat97e18a12022-01-15 17:42:25 +05301213 if self.valuation_method == "LIFO":
1214 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1215 else:
1216 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1217
Ankush Menatb534fee2022-02-19 20:58:36 +05301218 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1219
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301220 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301221 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301222 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301223
Ankush Menat4b29fb62021-12-18 18:40:22 +05301224 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301225 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1226 sle.voucher_type, sle.voucher_detail_no
1227 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301228 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301229 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301230 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301231 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301232
Ankush Menat494bd9e2022-03-28 18:52:46 +05301233 stock_queue.remove_stock(
1234 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1235 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301236
Ankush Menatb534fee2022-02-19 20:58:36 +05301237 _qty, stock_value = stock_queue.get_total_stock_and_value()
1238
1239 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301240
Ankush Menat97e18a12022-01-15 17:42:25 +05301241 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301242 self.wh_data.stock_value = round_off_if_near_zero(
1243 self.wh_data.stock_value + stock_value_difference
1244 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301245
Nabin Haita77b8c92020-12-21 14:45:50 +05301246 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301247 self.wh_data.stock_queue.append(
1248 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1249 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301250
Ankush Menatb534fee2022-02-19 20:58:36 +05301251 if self.wh_data.qty_after_transaction:
1252 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1253
Ankush Menatce0514c2022-02-15 11:41:41 +05301254 def update_batched_values(self, sle):
1255 incoming_rate = flt(sle.incoming_rate)
1256 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301257
Ankush Menat494bd9e2022-03-28 18:52:46 +05301258 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1259 self.wh_data.qty_after_transaction + actual_qty
1260 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301261
1262 if actual_qty > 0:
1263 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301264 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301265 outgoing_rate = get_batch_incoming_rate(
1266 item_code=sle.item_code,
1267 warehouse=sle.warehouse,
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301268 batch_no=sle.batch_no,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301269 posting_date=sle.posting_date,
1270 posting_time=sle.posting_time,
1271 creation=sle.creation,
1272 )
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301273
Ankush Menataba7a7c2022-02-19 19:36:28 +05301274 if outgoing_rate is None:
1275 # This can *only* happen if qty available for the batch is zero.
1276 # in such case fall back various other rates.
1277 # future entries will correct the overall accounting as each
1278 # batch individually uses moving average rates.
1279 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301280 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301281
Ankush Menat494bd9e2022-03-28 18:52:46 +05301282 self.wh_data.stock_value = round_off_if_near_zero(
1283 self.wh_data.stock_value + stock_value_difference
1284 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301285 if self.wh_data.qty_after_transaction:
1286 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301287
Javier Wong9b11d9b2017-04-14 18:24:04 +08001288 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301289 ref_item_dt = ""
1290
1291 if voucher_type == "Stock Entry":
1292 ref_item_dt = voucher_type + " Detail"
1293 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1294 ref_item_dt = voucher_type + " Item"
1295
1296 if ref_item_dt:
1297 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1298 else:
1299 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301300
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301301 def get_fallback_rate(self, sle) -> float:
1302 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301303 This should only get used for negative stock."""
1304 return get_valuation_rate(
1305 sle.item_code,
1306 sle.warehouse,
1307 sle.voucher_type,
1308 sle.voucher_no,
1309 self.allow_zero_rate,
1310 currency=erpnext.get_company_currency(sle.company),
1311 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301312 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301313
Nabin Haita77b8c92020-12-21 14:45:50 +05301314 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301315 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301316 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1317 sle = sle[0] if sle else frappe._dict()
1318 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301319
Nabin Haita77b8c92020-12-21 14:45:50 +05301320 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301321 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301322 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301323
1324 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301325 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301326 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301327 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301328
Ankush Menat494bd9e2022-03-28 18:52:46 +05301329 if (
1330 exceptions[0]["voucher_type"],
1331 exceptions[0]["voucher_no"],
1332 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301333
Nabin Haita77b8c92020-12-21 14:45:50 +05301334 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301335 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301336 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1337 frappe.get_desk_link("Warehouse", warehouse),
1338 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301339 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301340 msg = _(
1341 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1342 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301343 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301344 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1345 frappe.get_desk_link("Warehouse", warehouse),
1346 exceptions[0]["posting_date"],
1347 exceptions[0]["posting_time"],
1348 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1349 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301350
Nabin Haita77b8c92020-12-21 14:45:50 +05301351 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301352 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301353 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301354
1355 if allowed_qty > 0:
1356 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1357 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1358 )
1359 else:
1360 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1361 msg,
1362 )
s-aga-rf0acb202023-04-12 14:13:54 +05301363
Nabin Haita77b8c92020-12-21 14:45:50 +05301364 msg_list.append(msg)
1365
1366 if msg_list:
1367 message = "\n\n".join(msg_list)
1368 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301369 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301370 else:
1371 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301372
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301373 def update_bin_data(self, sle):
1374 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301375 values_to_update = {
1376 "actual_qty": sle.qty_after_transaction,
1377 "stock_value": sle.stock_value,
1378 }
1379
1380 if sle.valuation_rate is not None:
1381 values_to_update["valuation_rate"] = sle.valuation_rate
1382
1383 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301384
Nabin Haita77b8c92020-12-21 14:45:50 +05301385 def update_bin(self):
1386 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301387 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301388 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301389
Ankush Menat494bd9e2022-03-28 18:52:46 +05301390 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301391 if data.valuation_rate is not None:
1392 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301393 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301394
marination8418c4b2021-06-22 21:35:25 +05301395
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301396def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301397 """get stock ledger entries filtered by specific posting datetime conditions"""
1398
Ankush Menat494bd9e2022-03-28 18:52:46 +05301399 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301400 if not args.get("posting_date"):
1401 args["posting_date"] = "1900-01-01"
1402 if not args.get("posting_time"):
1403 args["posting_time"] = "00:00"
1404
1405 voucher_condition = ""
1406 if exclude_current_voucher:
1407 voucher_no = args.get("voucher_no")
1408 voucher_condition = f"and voucher_no != '{voucher_no}'"
1409
Ankush Menat494bd9e2022-03-28 18:52:46 +05301410 sle = frappe.db.sql(
1411 """
marination8418c4b2021-06-22 21:35:25 +05301412 select *, timestamp(posting_date, posting_time) as "timestamp"
1413 from `tabStock Ledger Entry`
1414 where item_code = %(item_code)s
1415 and warehouse = %(warehouse)s
1416 and is_cancelled = 0
1417 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301418 and (
1419 posting_date < %(posting_date)s or
1420 (
1421 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301422 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301423 )
1424 )
marination8418c4b2021-06-22 21:35:25 +05301425 order by timestamp(posting_date, posting_time) desc, creation desc
1426 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301427 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301428 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301429 ),
1430 args,
1431 as_dict=1,
1432 )
marination8418c4b2021-06-22 21:35:25 +05301433
1434 return sle[0] if sle else frappe._dict()
1435
Ankush Menat494bd9e2022-03-28 18:52:46 +05301436
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301437def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301438 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301439 get the last sle on or before the current time-bucket,
1440 to get actual qty before transaction, this function
1441 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301442
Ankush Menat494bd9e2022-03-28 18:52:46 +05301443 args = {
1444 "item_code": "ABC",
1445 "warehouse": "XYZ",
1446 "posting_date": "2012-12-12",
1447 "posting_time": "12:00",
1448 "sle": "name of reference Stock Ledger Entry"
1449 }
Anand Doshi1b531862013-01-10 19:29:51 +05301450 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301451 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301452 sle = get_stock_ledger_entries(
1453 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1454 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301455 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301456
Ankush Menat494bd9e2022-03-28 18:52:46 +05301457
1458def get_stock_ledger_entries(
1459 previous_sle,
1460 operator=None,
1461 order="desc",
1462 limit=None,
1463 for_update=False,
1464 debug=False,
1465 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301466 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301467):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301468 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301469 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1470 operator
1471 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301472 if previous_sle.get("warehouse"):
1473 conditions += " and warehouse = %(warehouse)s"
1474 elif previous_sle.get("warehouse_condition"):
1475 conditions += " and " + previous_sle.get("warehouse_condition")
1476
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301477 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301478 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1479 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301480 conditions += (
1481 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301482 (
1483 serial_no = {0}
1484 or serial_no like {1}
1485 or serial_no like {2}
1486 or serial_no like {3}
1487 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301488 """
1489 ).format(
1490 frappe.db.escape(serial_no),
1491 frappe.db.escape("{}\n%".format(serial_no)),
1492 frappe.db.escape("%\n{}".format(serial_no)),
1493 frappe.db.escape("%\n{}\n%".format(serial_no)),
1494 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301495
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301496 if not previous_sle.get("posting_date"):
1497 previous_sle["posting_date"] = "1900-01-01"
1498 if not previous_sle.get("posting_time"):
1499 previous_sle["posting_time"] = "00:00"
1500
1501 if operator in (">", "<=") and previous_sle.get("name"):
1502 conditions += " and name!=%(name)s"
1503
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301504 if extra_cond:
1505 conditions += f"{extra_cond}"
1506
Ankush Menat494bd9e2022-03-28 18:52:46 +05301507 return frappe.db.sql(
1508 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301509 select *, timestamp(posting_date, posting_time) as "timestamp"
1510 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301511 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301512 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301513 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301514 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301515 %(limit)s %(for_update)s"""
1516 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301517 "conditions": conditions,
1518 "limit": limit or "",
1519 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301520 "order": order,
1521 },
1522 previous_sle,
1523 as_dict=1,
1524 debug=debug,
1525 )
1526
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301527
Nabin Haita77b8c92020-12-21 14:45:50 +05301528def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301529 return frappe.db.get_value(
1530 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301531 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301532 [
1533 "item_code",
1534 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301535 "actual_qty",
1536 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301537 "posting_date",
1538 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301539 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301540 "timestamp(posting_date, posting_time) as timestamp",
1541 ],
1542 as_dict=1,
1543 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301544
Ankush Menatce0514c2022-02-15 11:41:41 +05301545
Ankush Menat494bd9e2022-03-28 18:52:46 +05301546def get_batch_incoming_rate(
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301547 item_code, warehouse, batch_no, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301548):
1549
Ankush Menat102fff22022-02-19 15:51:04 +05301550 sle = frappe.qb.DocType("Stock Ledger Entry")
1551
Ankush Menate1c16872022-04-21 20:01:48 +05301552 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301553 posting_date, posting_time
1554 )
Ankush Menat102fff22022-02-19 15:51:04 +05301555 if creation:
1556 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301557 CombineDatetime(sle.posting_date, sle.posting_time)
1558 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301559 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301560
1561 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301562 frappe.qb.from_(sle)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301563 .select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301564 .where(
1565 (sle.item_code == item_code)
1566 & (sle.warehouse == warehouse)
Rohit Waghchaure64cb1152024-01-15 19:39:41 +05301567 & (sle.batch_no == batch_no)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301568 & (sle.is_cancelled == 0)
1569 )
1570 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301571 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301572
1573 if batch_details and batch_details[0].batch_qty:
1574 return batch_details[0].batch_value / batch_details[0].batch_qty
1575
1576
Ankush Menat494bd9e2022-03-28 18:52:46 +05301577def get_valuation_rate(
1578 item_code,
1579 warehouse,
1580 voucher_type,
1581 voucher_no,
1582 allow_zero_rate=False,
1583 currency=None,
1584 company=None,
1585 raise_error_if_no_rate=True,
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301586 batch_no=None,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301587 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301588):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301589
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301590 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1591
Ankush Menatf7ffe042021-11-01 13:21:14 +05301592 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301593 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301594
rohitwaghchaured4c0dbf2023-11-03 17:19:06 +05301595 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
1596 table = frappe.qb.DocType("Stock Ledger Entry")
1597 query = (
1598 frappe.qb.from_(table)
1599 .select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
1600 .where(
1601 (table.item_code == item_code)
1602 & (table.warehouse == warehouse)
1603 & (table.batch_no == batch_no)
1604 & (table.is_cancelled == 0)
1605 & (table.voucher_no != voucher_no)
1606 & (table.voucher_type != voucher_type)
1607 )
1608 )
1609
1610 last_valuation_rate = query.run()
1611 if last_valuation_rate:
1612 return flt(last_valuation_rate[0][0])
1613
Ankush Menat342d09a2022-02-19 14:28:51 +05301614 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301615 if warehouse and serial_and_batch_bundle:
1616 batch_obj = BatchNoValuation(
1617 sle=frappe._dict(
1618 {
1619 "item_code": item_code,
1620 "warehouse": warehouse,
1621 "actual_qty": -1,
1622 "serial_and_batch_bundle": serial_and_batch_bundle,
1623 }
1624 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301625 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301626
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301627 return batch_obj.get_incoming_rate()
1628
Ankush Menatf7ffe042021-11-01 13:21:14 +05301629 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301630 if last_valuation_rate := frappe.db.sql(
1631 """select valuation_rate
1632 from `tabStock Ledger Entry` force index (item_warehouse)
1633 where
1634 item_code = %s
1635 AND warehouse = %s
1636 AND valuation_rate >= 0
1637 AND is_cancelled = 0
1638 AND NOT (voucher_no = %s AND voucher_type = %s)
1639 order by posting_date desc, posting_time desc, name desc limit 1""",
1640 (item_code, warehouse, voucher_no, voucher_type),
1641 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301642 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301643
1644 # If negative stock allowed, and item delivered without any incoming entry,
1645 # system does not found any SLE, then take valuation rate from Item
1646 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301647
1648 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301649 # try Item Standard rate
1650 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301651
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301652 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301653 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301654 valuation_rate = frappe.db.get_value(
1655 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1656 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301657
Ankush Menat494bd9e2022-03-28 18:52:46 +05301658 if (
1659 not allow_zero_rate
1660 and not valuation_rate
1661 and raise_error_if_no_rate
1662 and cint(erpnext.is_perpetual_inventory_enabled(company))
1663 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301664 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301665
Ankush Menat494bd9e2022-03-28 18:52:46 +05301666 message = _(
1667 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1668 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301669 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301670 solutions = (
1671 "<li>"
1672 + _(
1673 "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."
1674 ).format(voucher_type)
1675 + "</li>"
1676 )
1677 solutions += (
1678 "<li>"
1679 + _("If not, you can Cancel / Submit this entry")
1680 + " {0} ".format(frappe.bold("after"))
1681 + _("performing either one below:")
1682 + "</li>"
1683 )
Marica97715f22020-05-11 20:45:37 +05301684 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1685 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1686 msg = message + solutions + sub_solutions + "</li>"
1687
1688 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301689
1690 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301691
Ankush Menat494bd9e2022-03-28 18:52:46 +05301692
Ankush Menate7109c12021-08-26 16:40:45 +05301693def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301694 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301695 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301696 qty_shift = args.actual_qty
1697
Ankush Menat7c839c42022-05-06 12:09:08 +05301698 args["time_format"] = "%H:%i:%s"
1699
marination8418c4b2021-06-22 21:35:25 +05301700 # find difference/shift in qty caused by stock reconciliation
1701 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301702 qty_shift = get_stock_reco_qty_shift(args)
1703
1704 # find the next nearest stock reco so that we only recalculate SLEs till that point
1705 next_stock_reco_detail = get_next_stock_reco(args)
1706 if next_stock_reco_detail:
1707 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301708
marination40389772021-07-02 17:13:45 +05301709 # add condition to update SLEs before this date & time
1710 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301711
Ankush Menat494bd9e2022-03-28 18:52:46 +05301712 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301713 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301714 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301715 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301716 where
1717 item_code = %(item_code)s
1718 and warehouse = %(warehouse)s
1719 and voucher_no != %(voucher_no)s
1720 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301721 and (
1722 posting_date > %(posting_date)s or
1723 (
1724 posting_date = %(posting_date)s and
1725 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1726 )
1727 )
marination40389772021-07-02 17:13:45 +05301728 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301729 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301730 args,
1731 )
Nabin Hait186a0452021-02-18 14:14:21 +05301732
1733 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1734
Ankush Menat494bd9e2022-03-28 18:52:46 +05301735
marination40389772021-07-02 17:13:45 +05301736def get_stock_reco_qty_shift(args):
1737 stock_reco_qty_shift = 0
1738 if args.get("is_cancelled"):
1739 if args.get("previous_qty_after_transaction"):
1740 # get qty (balance) that was set at submission
1741 last_balance = args.get("previous_qty_after_transaction")
1742 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1743 else:
1744 stock_reco_qty_shift = flt(args.actual_qty)
1745 else:
1746 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301747 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301748 "qty_after_transaction"
1749 )
marination40389772021-07-02 17:13:45 +05301750
1751 if last_balance is not None:
1752 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1753 else:
1754 stock_reco_qty_shift = args.qty_after_transaction
1755
1756 return stock_reco_qty_shift
1757
Ankush Menat494bd9e2022-03-28 18:52:46 +05301758
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301759def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301760 """Returns next nearest stock reconciliaton's details."""
1761
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301762 sle = frappe.qb.DocType("Stock Ledger Entry")
1763
1764 query = (
1765 frappe.qb.from_(sle)
1766 .select(
1767 sle.name,
1768 sle.posting_date,
1769 sle.posting_time,
1770 sle.creation,
1771 sle.voucher_no,
1772 sle.item_code,
1773 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301774 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301775 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301776 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301777 )
1778 .where(
1779 (sle.item_code == kwargs.get("item_code"))
1780 & (sle.warehouse == kwargs.get("warehouse"))
1781 & (sle.voucher_type == "Stock Reconciliation")
1782 & (sle.voucher_no != kwargs.get("voucher_no"))
1783 & (sle.is_cancelled == 0)
1784 & (
1785 (
1786 CombineDatetime(sle.posting_date, sle.posting_time)
1787 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301788 )
1789 | (
1790 (
1791 CombineDatetime(sle.posting_date, sle.posting_time)
1792 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301793 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301794 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301795 )
1796 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301797 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301798 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1799 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301800 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301801 )
1802
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301803 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301804 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301805
1806 return query.run(as_dict=True)
1807
marination40389772021-07-02 17:13:45 +05301808
1809def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301810 return f"""
1811 and
1812 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1813 or (
1814 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1815 and creation < '{detail.creation}'
1816 )
1817 )"""
1818
Ankush Menat494bd9e2022-03-28 18:52:46 +05301819
Ankush Menate7109c12021-08-26 16:40:45 +05301820def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301821 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301822 return
barredterraeb9ee3f2023-12-05 11:22:55 +01001823 if args.actual_qty >= 0 and args.voucher_type != "Stock Reconciliation":
Ankush Menat5eba5752021-12-07 23:03:52 +05301824 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301825
Ankush Menat5eba5752021-12-07 23:03:52 +05301826 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301827
1828 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301829 message = _(
1830 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1831 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301832 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301833 frappe.get_desk_link("Item", args.item_code),
1834 frappe.get_desk_link("Warehouse", args.warehouse),
1835 neg_sle[0]["posting_date"],
1836 neg_sle[0]["posting_time"],
1837 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1838 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301839
Ankush Menat494bd9e2022-03-28 18:52:46 +05301840 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301841
s-aga-rd9e28432023-10-27 16:35:35 +05301842 if args.batch_no:
1843 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1844 if is_negative_with_precision(neg_batch_sle, is_batch=True):
1845 message = _(
1846 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1847 ).format(
1848 abs(neg_batch_sle[0]["cumulative_total"]),
1849 frappe.get_desk_link("Batch", args.batch_no),
1850 frappe.get_desk_link("Warehouse", args.warehouse),
1851 neg_batch_sle[0]["posting_date"],
1852 neg_batch_sle[0]["posting_time"],
1853 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1854 )
1855 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301856
s-aga-r73b65ac2023-11-01 18:35:07 +05301857 if args.reserved_stock:
1858 validate_reserved_stock(args)
Ankush Menat5eba5752021-12-07 23:03:52 +05301859
Nabin Haita77b8c92020-12-21 14:45:50 +05301860
Maricad6078aa2022-06-17 15:13:13 +05301861def is_negative_with_precision(neg_sle, is_batch=False):
1862 """
1863 Returns whether system precision rounded qty is insufficient.
1864 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1865 """
1866
1867 if not neg_sle:
1868 return False
1869
1870 field = "cumulative_total" if is_batch else "qty_after_transaction"
1871 precision = cint(frappe.db.get_default("float_precision")) or 2
1872 qty_deficit = flt(neg_sle[0][field], precision)
1873
1874 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1875
1876
Nabin Haita77b8c92020-12-21 14:45:50 +05301877def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301878 return frappe.db.sql(
1879 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301880 select
1881 qty_after_transaction, posting_date, posting_time,
1882 voucher_type, voucher_no
1883 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301884 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301885 item_code = %(item_code)s
1886 and warehouse = %(warehouse)s
1887 and voucher_no != %(voucher_no)s
1888 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1889 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301890 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301891 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301892 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301893 """,
1894 args,
1895 as_dict=1,
1896 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301897
Ankush Menat5eba5752021-12-07 23:03:52 +05301898
1899def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301900 return frappe.db.sql(
1901 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301902 with batch_ledger as (
1903 select
1904 posting_date, posting_time, voucher_type, voucher_no,
1905 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1906 from `tabStock Ledger Entry`
1907 where
1908 item_code = %(item_code)s
1909 and warehouse = %(warehouse)s
1910 and batch_no=%(batch_no)s
1911 and is_cancelled = 0
1912 order by posting_date, posting_time, creation
1913 )
1914 select * from batch_ledger
1915 where
1916 cumulative_total < 0.0
1917 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1918 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301919 """,
1920 args,
1921 as_dict=1,
1922 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301923
1924
s-aga-rd9e28432023-10-27 16:35:35 +05301925def validate_reserved_stock(kwargs):
1926 if kwargs.serial_no:
1927 serial_nos = kwargs.serial_no.split("\n")
1928 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
1929
s-aga-re1a87a82023-10-31 18:41:58 +05301930 elif kwargs.batch_no:
1931 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])
1932
s-aga-rd9e28432023-10-27 16:35:35 +05301933 elif kwargs.serial_and_batch_bundle:
1934 sbb_entries = frappe.db.get_all(
1935 "Serial and Batch Entry",
1936 {
1937 "parenttype": "Serial and Batch Bundle",
1938 "parent": kwargs.serial_and_batch_bundle,
1939 "docstatus": 1,
1940 },
s-aga-re1a87a82023-10-31 18:41:58 +05301941 ["batch_no", "serial_no"],
s-aga-rd9e28432023-10-27 16:35:35 +05301942 )
s-aga-rd9e28432023-10-27 16:35:35 +05301943
s-aga-re1a87a82023-10-31 18:41:58 +05301944 if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
s-aga-rd9e28432023-10-27 16:35:35 +05301945 validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
s-aga-re1a87a82023-10-31 18:41:58 +05301946 elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
1947 validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)
s-aga-rd9e28432023-10-27 16:35:35 +05301948
s-aga-r92317062023-11-02 10:36:00 +05301949 # Qty based validation for non-serial-batch items OR SRE with Reservation Based On Qty.
1950 precision = cint(frappe.db.get_default("float_precision")) or 2
1951 balance_qty = get_stock_balance(kwargs.item_code, kwargs.warehouse)
s-aga-r73b65ac2023-11-01 18:35:07 +05301952
s-aga-r92317062023-11-02 10:36:00 +05301953 diff = flt(balance_qty - kwargs.get("reserved_stock", 0), precision)
1954 if diff < 0 and abs(diff) > 0.0001:
1955 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
1956 abs(diff),
1957 frappe.get_desk_link("Item", kwargs.item_code),
1958 frappe.get_desk_link("Warehouse", kwargs.warehouse),
1959 nowdate(),
1960 nowtime(),
1961 )
1962 frappe.throw(msg, title=_("Reserved Stock"))
s-aga-r73b65ac2023-11-01 18:35:07 +05301963
s-aga-rd9e28432023-10-27 16:35:35 +05301964
1965def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
1966 if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
1967 item_code, warehouse, serial_nos
1968 ):
1969 if common_serial_nos := list(
1970 set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
1971 ):
1972 msg = _(
1973 "Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
1974 )
1975 msg += "<br />"
1976 msg += _("Example: Serial No {0} reserved in {1}.").format(
1977 frappe.bold(common_serial_nos[0]),
1978 frappe.get_desk_link(
1979 "Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
1980 ),
1981 )
1982 frappe.throw(msg, title=_("Reserved Serial No."))
1983
1984
s-aga-re1a87a82023-10-31 18:41:58 +05301985def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
1986 if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
1987 available_batches = get_available_batches(
1988 frappe._dict(
1989 {
1990 "item_code": item_code,
1991 "warehouse": warehouse,
1992 "posting_date": nowdate(),
1993 "posting_time": nowtime(),
1994 }
1995 )
1996 )
1997 available_batches_map = {row.batch_no: row.qty for row in available_batches}
1998 precision = cint(frappe.db.get_default("float_precision")) or 2
1999
2000 for batch_no in batch_nos:
2001 diff = flt(
2002 available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
2003 )
2004 if diff < 0 and abs(diff) > 0.0001:
2005 msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
2006 abs(diff),
2007 frappe.get_desk_link("Batch", batch_no),
2008 frappe.get_desk_link("Warehouse", warehouse),
2009 nowdate(),
2010 nowtime(),
2011 )
2012 frappe.throw(msg, title=_("Reserved Stock for Batch"))
2013
2014
Ankush Menateb8b4242022-02-12 13:08:28 +05302015def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
2016 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
2017 return True
2018 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
2019 return True
2020 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05302021
2022
2023def get_incoming_rate_for_inter_company_transfer(sle) -> float:
2024 """
2025 For inter company transfer, incoming rate is the average of the outgoing rate
2026 """
2027 rate = 0.0
2028
2029 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
2030
2031 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
2032
2033 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
2034
2035 if reference_name:
2036 rate = frappe.get_cached_value(
2037 doctype,
2038 reference_name,
2039 "incoming_rate",
2040 )
2041
2042 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05302043
2044
2045def is_internal_transfer(sle):
2046 data = frappe.get_cached_value(
2047 sle.voucher_type,
2048 sle.voucher_no,
2049 ["is_internal_supplier", "represents_company", "company"],
2050 as_dict=True,
2051 )
2052
2053 if data.is_internal_supplier and data.represents_company == data.company:
2054 return True
rohitwaghchaurea8216b92023-11-09 12:22:26 +05302055
2056
2057def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None):
2058 table = frappe.qb.DocType("Stock Ledger Entry")
2059
2060 query = (
2061 frappe.qb.from_(table)
2062 .select(Sum(table.stock_value_difference).as_("value"))
2063 .where(
2064 (table.is_cancelled == 0)
2065 & (table.item_code == item_code)
2066 & (table.warehouse == warehouse)
2067 & (
2068 (table.posting_date < posting_date)
2069 | ((table.posting_date == posting_date) & (table.posting_time <= posting_time))
2070 )
2071 )
2072 )
2073
2074 if voucher_no:
2075 query = query.where(table.voucher_no != voucher_no)
2076
2077 difference_amount = query.run()
2078 return flt(difference_amount[0][0]) if difference_amount else 0