blob: 48119b8d1f18b23670929cd3aac8620d5179ea58 [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
Nabin Hait26d46552013-01-09 15:23:05 +05305import json
Ankush Menatecdb4932022-04-17 19:06:13 +05306from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +05309from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053010from frappe.model.meta import get_field_precision
Rohit Waghchaurebb954512023-06-02 00:11:43 +053011from frappe.query_builder import Case
Ankush Menate1c16872022-04-21 20:01:48 +053012from frappe.query_builder.functions import CombineDatetime, Sum
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053013from frappe.utils import (
14 cint,
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053015 flt,
16 get_link_to_form,
17 getdate,
18 gzip_compress,
19 gzip_decompress,
20 now,
21 nowdate,
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-rf0acb202023-04-12 14:13:54 +053028from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
29 get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
30)
Chillar Anand915b3432021-09-02 16:44:59 +053031from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053032 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053033 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053034 get_valuation_method,
35)
Ankush Menatb534fee2022-02-19 20:58:36 +053036from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053037
Nabin Hait97bce3a2021-07-12 13:24:43 +053038
Ankush Menat494bd9e2022-03-28 18:52:46 +053039class NegativeStockError(frappe.ValidationError):
40 pass
41
42
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053043class SerialNoExistsInFutureTransaction(frappe.ValidationError):
44 pass
Nabin Hait902e8602013-01-08 18:29:24 +053045
Anand Doshi5b004ff2013-09-25 19:55:41 +053046
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053047def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053048 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053049
Ankush Menat494bd9e2022-03-28 18:52:46 +053050 args:
51 - allow_negative_stock: disable negative stock valiations if true
52 - via_landed_cost_voucher: landed cost voucher cancels and reposts
53 entries of purchase document. This flag is used to identify if
54 cancellation and repost is happening via landed cost voucher, in
55 such cases certain validations need to be ignored (like negative
56 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053057 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053058 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053059
Nabin Haitca775742013-09-26 16:16:44 +053060 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053061 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053062 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053063 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053064 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053065
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053066 args = get_args_for_future_sle(sl_entries[0])
67 future_sle_exists(args, sl_entries)
68
Nabin Haitca775742013-09-26 16:16:44 +053069 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053070 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053071 validate_serial_no(sle)
72
Nabin Haita77b8c92020-12-21 14:45:50 +053073 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053074 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053075
Ankush Menat494bd9e2022-03-28 18:52:46 +053076 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
77 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
78 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
79 )
80 sle["incoming_rate"] = 0.0
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("incoming_rate"):
83 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
84 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
85 )
86 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053087
Ankush Menat494bd9e2022-03-28 18:52:46 +053088 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053089 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053090
Nabin Haita77b8c92020-12-21 14:45:50 +053091 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053092
93 if sle.get("voucher_type") == "Stock Reconciliation":
94 # preserve previous_qty_after_transaction for qty reposting
95 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
96
Ankush Menat494bd9e2022-03-28 18:52:46 +053097 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053098 if is_stock_item:
99 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +0530100 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +0530101 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +0530102 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530103 frappe.msgprint(
104 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
105 )
106
Ankush Menatcef84c22021-12-03 12:18:59 +0530107
108def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
109 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
110 if not args.get("posting_date"):
111 args["posting_date"] = nowdate()
112
marination7a5fd712022-07-04 17:46:54 +0530113 if not (args.get("is_cancelled") and via_landed_cost_voucher):
114 # Reposts only current voucher SL Entries
115 # Updates valuation rate, stock value, stock queue for current transaction
116 update_entries_after(
117 {
118 "item_code": args.get("item_code"),
119 "warehouse": args.get("warehouse"),
120 "posting_date": args.get("posting_date"),
121 "posting_time": args.get("posting_time"),
122 "voucher_type": args.get("voucher_type"),
123 "voucher_no": args.get("voucher_no"),
124 "sle_id": args.get("name"),
125 "creation": args.get("creation"),
126 },
127 allow_negative_stock=allow_negative_stock,
128 via_landed_cost_voucher=via_landed_cost_voucher,
129 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530130
131 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530132 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530133 update_qty_in_future_sle(args, allow_negative_stock)
134
Nabin Haitadeb9762014-10-06 11:53:52 +0530135
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530136def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530137 return frappe._dict(
138 {
139 "voucher_type": row.get("voucher_type"),
140 "voucher_no": row.get("voucher_no"),
141 "posting_date": row.get("posting_date"),
142 "posting_time": row.get("posting_time"),
143 }
144 )
145
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530146
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530147def validate_serial_no(sle):
148 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530149
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530150 for sn in get_serial_nos(sle.serial_no):
151 args = copy.deepcopy(sle)
152 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530153 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530154
155 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530156 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530157 voucher_type = frappe.bold(row.voucher_type)
158 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530159 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530160
161 if vouchers:
162 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163 msg = (
164 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
165 The list of the transactions are as below."""
166 + "<br><br><ul><li>"
167 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530168
Ankush Menat494bd9e2022-03-28 18:52:46 +0530169 msg += "</li><li>".join(vouchers)
170 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530171
Ankush Menat494bd9e2022-03-28 18:52:46 +0530172 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530173 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
174
Ankush Menat494bd9e2022-03-28 18:52:46 +0530175
Nabin Hait186a0452021-02-18 14:14:21 +0530176def validate_cancellation(args):
177 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530178 repost_entry = frappe.db.get_value(
179 "Repost Item Valuation",
180 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
181 ["name", "status"],
182 as_dict=1,
183 )
Nabin Hait186a0452021-02-18 14:14:21 +0530184
185 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530186 if repost_entry.status == "In Progress":
187 frappe.throw(
188 _(
189 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
190 )
191 )
192 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530193 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530194 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530195 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530196 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530197
Ankush Menat494bd9e2022-03-28 18:52:46 +0530198
Nabin Hait9653f602013-08-20 15:37:33 +0530199def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530200 frappe.db.sql(
201 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530202 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530203 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530204 (now(), frappe.session.user, voucher_type, voucher_no),
205 )
206
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530207
Nabin Hait54c865e2015-03-27 15:38:31 +0530208def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530209 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530210 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530211 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530212 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530213 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530214 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530215 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530216
Ankush Menat494bd9e2022-03-28 18:52:46 +0530217
218def repost_future_sle(
219 args=None,
220 voucher_type=None,
221 voucher_no=None,
222 allow_negative_stock=None,
223 via_landed_cost_voucher=False,
224 doc=None,
225):
Nabin Haite1fa7232022-07-20 15:19:09 +0530226 if not args:
227 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530228
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530229 reposting_data = {}
230 if doc and doc.reposting_data_file:
231 reposting_data = get_reposting_data(doc.reposting_data_file)
232
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530233 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530234 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530235 )
236 if items_to_be_repost:
237 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530238
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530239 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
240 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530241
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530242 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530243 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530244 validate_item_warehouse(args[i])
245
Ankush Menat494bd9e2022-03-28 18:52:46 +0530246 obj = update_entries_after(
247 {
248 "item_code": args[i].get("item_code"),
249 "warehouse": args[i].get("warehouse"),
250 "posting_date": args[i].get("posting_date"),
251 "posting_time": args[i].get("posting_time"),
252 "creation": args[i].get("creation"),
253 "distinct_item_warehouses": distinct_item_warehouses,
254 },
255 allow_negative_stock=allow_negative_stock,
256 via_landed_cost_voucher=via_landed_cost_voucher,
257 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530258 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530259
Ankush Menat494bd9e2022-03-28 18:52:46 +0530260 distinct_item_warehouses[
261 (args[i].get("item_code"), args[i].get("warehouse"))
262 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530263
Nabin Hait97bce3a2021-07-12 13:24:43 +0530264 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530265 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530266 if ("args_idx" not in data and not data.reposting_status) or (
267 data.sle_changed and data.reposting_status
268 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530269 data.args_idx = len(args)
270 args.append(data.sle)
271 elif data.sle_changed and not data.reposting_status:
272 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530273
Nabin Hait97bce3a2021-07-12 13:24:43 +0530274 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530275 i += 1
276
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530277 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530278 update_args_in_repost_item_valuation(
279 doc, i, args, distinct_item_warehouses, affected_transactions
280 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530281
Ankush Menat494bd9e2022-03-28 18:52:46 +0530282
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530283def get_reposting_data(file_path) -> dict:
284 file_name = frappe.db.get_value(
285 "File",
286 {
287 "file_url": file_path,
288 "attached_to_field": "reposting_data_file",
289 },
290 "name",
291 )
292
293 if not file_name:
294 return frappe._dict()
295
296 attached_file = frappe.get_doc("File", file_name)
297
298 data = gzip_decompress(attached_file.get_content())
299 if data := json.loads(data.decode("utf-8")):
300 data = data
301
302 return parse_json(data)
303
304
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530305def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530306 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530307 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530308 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530309 frappe.throw(_(validation_msg))
310
Ankush Menat494bd9e2022-03-28 18:52:46 +0530311
Ankush Menatecdb4932022-04-17 19:06:13 +0530312def update_args_in_repost_item_valuation(
313 doc, index, args, distinct_item_warehouses, affected_transactions
314):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530315 if not doc.items_to_be_repost:
316 file_name = ""
317 if doc.reposting_data_file:
318 file_name = get_reposting_file_name(doc.doctype, doc.name)
319 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
320
321 doc.reposting_data_file = create_json_gz_file(
322 {
323 "items_to_be_repost": args,
324 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
325 "affected_transactions": affected_transactions,
326 },
327 doc,
328 file_name,
329 )
330
331 doc.db_set(
332 {
333 "current_index": index,
334 "total_reposting_count": len(args),
335 "reposting_data_file": doc.reposting_data_file,
336 }
337 )
338
339 else:
340 doc.db_set(
341 {
342 "items_to_be_repost": json.dumps(args, default=str),
343 "distinct_item_and_warehouse": json.dumps(
344 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
345 ),
346 "current_index": index,
347 "affected_transactions": frappe.as_json(affected_transactions),
348 }
349 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530350
Ankush Menatecdb4932022-04-17 19:06:13 +0530351 if not frappe.flags.in_test:
352 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530353
Ankush Menat494bd9e2022-03-28 18:52:46 +0530354 frappe.publish_realtime(
355 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530356 {
357 "name": doc.name,
358 "items_to_be_repost": json.dumps(args, default=str),
359 "current_index": index,
360 "total_reposting_count": len(args),
361 },
Ankush Menatc0642cf2023-07-29 15:02:11 +0530362 doctype=doc.doctype,
363 docname=doc.name,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530364 )
365
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530366
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530367def get_reposting_file_name(dt, dn):
368 return frappe.db.get_value(
369 "File",
370 {
371 "attached_to_doctype": dt,
372 "attached_to_name": dn,
373 "attached_to_field": "reposting_data_file",
374 },
375 "name",
376 )
377
378
379def create_json_gz_file(data, doc, file_name=None) -> str:
380 encoded_content = frappe.safe_encode(frappe.as_json(data))
381 compressed_content = gzip_compress(encoded_content)
382
383 if not file_name:
384 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
385 _file = frappe.get_doc(
386 {
387 "doctype": "File",
388 "file_name": json_filename,
389 "attached_to_doctype": doc.doctype,
390 "attached_to_name": doc.name,
391 "attached_to_field": "reposting_data_file",
392 "content": compressed_content,
393 "is_private": 1,
394 }
395 )
396 _file.save(ignore_permissions=True)
397
398 return _file.file_url
399 else:
400 file_doc = frappe.get_doc("File", file_name)
401 path = file_doc.get_full_path()
402
403 with open(path, "wb") as f:
404 f.write(compressed_content)
405
406 return doc.reposting_data_file
407
408
409def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
410 if not reposting_data and doc and doc.reposting_data_file:
411 reposting_data = get_reposting_data(doc.reposting_data_file)
412
413 if reposting_data and reposting_data.items_to_be_repost:
414 return reposting_data.items_to_be_repost
415
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530416 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530417
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530418 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530419 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530420
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530421 if not items_to_be_repost and voucher_type and voucher_no:
422 items_to_be_repost = frappe.db.get_all(
423 "Stock Ledger Entry",
424 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
425 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
426 order_by="creation asc",
427 group_by="item_code, warehouse",
428 )
429
Nabin Haite1fa7232022-07-20 15:19:09 +0530430 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530431
Ankush Menat494bd9e2022-03-28 18:52:46 +0530432
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530433def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
434 if not reposting_data and doc and doc.reposting_data_file:
435 reposting_data = get_reposting_data(doc.reposting_data_file)
436
437 if reposting_data and reposting_data.distinct_item_and_warehouse:
438 return reposting_data.distinct_item_and_warehouse
439
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530440 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530441
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530442 if doc and doc.distinct_item_and_warehouse:
443 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530444 distinct_item_warehouses = {
445 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
446 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530447 else:
448 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530449 distinct_item_warehouses.setdefault(
450 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
451 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530452
453 return distinct_item_warehouses
454
Ankush Menat494bd9e2022-03-28 18:52:46 +0530455
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530456def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
457 if not reposting_data and doc and doc.reposting_data_file:
458 reposting_data = get_reposting_data(doc.reposting_data_file)
459
460 if reposting_data and reposting_data.affected_transactions:
461 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
462
Ankush Menatecdb4932022-04-17 19:06:13 +0530463 if not doc.affected_transactions:
464 return set()
465
466 transactions = frappe.parse_json(doc.affected_transactions)
467 return {tuple(transaction) for transaction in transactions}
468
469
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530470def get_current_index(doc=None):
471 if doc and doc.current_index:
472 return doc.current_index
473
Ankush Menat494bd9e2022-03-28 18:52:46 +0530474
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530475class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530476 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530477 update valution rate and qty after transaction
478 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530479
Ankush Menat494bd9e2022-03-28 18:52:46 +0530480 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530481
Ankush Menat494bd9e2022-03-28 18:52:46 +0530482 args = {
483 "item_code": "ABC",
484 "warehouse": "XYZ",
485 "posting_date": "2012-12-12",
486 "posting_time": "12:00"
487 }
Nabin Hait902e8602013-01-08 18:29:24 +0530488 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530489
490 def __init__(
491 self,
492 args,
493 allow_zero_rate=False,
494 allow_negative_stock=None,
495 via_landed_cost_voucher=False,
496 verbose=1,
497 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530498 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530499 self.verbose = verbose
500 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530501 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530502 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530503 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
504 item_code=self.item_code
505 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530506
Nabin Haita77b8c92020-12-21 14:45:50 +0530507 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530508 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530509 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530510
Nabin Haita77b8c92020-12-21 14:45:50 +0530511 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530512 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530513 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530514
515 self.new_items_found = False
516 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530517 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-rf0acb202023-04-12 14:13:54 +0530518 self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
Nabin Haita77b8c92020-12-21 14:45:50 +0530519
520 self.data = frappe._dict()
521 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530522 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530523
Maricad6078aa2022-06-17 15:13:13 +0530524 def set_precision(self):
525 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
526 self.currency_precision = get_field_precision(
527 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530528 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530529
530 def initialize_previous_data(self, args):
531 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530532 Get previous sl entries for current item for each related warehouse
533 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530534
Ankush Menat494bd9e2022-03-28 18:52:46 +0530535 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530536
Ankush Menat494bd9e2022-03-28 18:52:46 +0530537 self.data = {
538 warehouse1: {
539 'previus_sle': {},
540 'qty_after_transaction': 10,
541 'valuation_rate': 100,
542 'stock_value': 1000,
543 'prev_stock_value': 1000,
544 'stock_queue': '[[10, 100]]',
545 'stock_value_difference': 1000
546 }
547 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530548
549 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530550 self.data.setdefault(args.warehouse, frappe._dict())
551 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530552 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530553 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530554
Ankush Menatc1d986a2021-08-31 19:43:42 +0530555 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
556 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
557
Ankush Menat494bd9e2022-03-28 18:52:46 +0530558 warehouse_dict.update(
559 {
560 "prev_stock_value": previous_sle.stock_value or 0.0,
561 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
562 "stock_value_difference": 0.0,
563 }
564 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530565
Nabin Haita77b8c92020-12-21 14:45:50 +0530566 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530567 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530568
Nabin Haita77b8c92020-12-21 14:45:50 +0530569 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530570 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530571 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530572 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530573 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530574 entries_to_fix = self.get_future_entries_to_fix()
575
576 i = 0
577 while i < len(entries_to_fix):
578 sle = entries_to_fix[i]
579 i += 1
580
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530581 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530582 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530583
Nabin Haita77b8c92020-12-21 14:45:50 +0530584 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530585 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530586
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530587 if self.exceptions:
588 self.raise_exceptions()
589
Nabin Hait186a0452021-02-18 14:14:21 +0530590 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530591 sl_entries = self.get_sle_against_current_voucher()
592 for sle in sl_entries:
593 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530594
Nabin Haita77b8c92020-12-21 14:45:50 +0530595 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530596 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530597
Ankush Menat494bd9e2022-03-28 18:52:46 +0530598 return frappe.db.sql(
599 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530600 select
601 *, timestamp(posting_date, posting_time) as "timestamp"
602 from
603 `tabStock Ledger Entry`
604 where
605 item_code = %(item_code)s
606 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530607 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530608 and (
609 posting_date = %(posting_date)s and
610 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
611 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530612 order by
613 creation ASC
614 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530615 """,
616 self.args,
617 as_dict=1,
618 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530619
Nabin Haita77b8c92020-12-21 14:45:50 +0530620 def get_future_entries_to_fix(self):
621 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530622 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
623 {"item_code": self.item_code, "warehouse": self.args.warehouse}
624 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530625
Nabin Haita77b8c92020-12-21 14:45:50 +0530626 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530627
Nabin Haita77b8c92020-12-21 14:45:50 +0530628 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530629 dependant_sle = get_sle_by_voucher_detail_no(
630 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
631 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530632
Nabin Haita77b8c92020-12-21 14:45:50 +0530633 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530634 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530635 elif (
636 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
637 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530638 return entries_to_fix
639 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530640 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530641 return entries_to_fix
642 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
643 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530644 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530645 self.initialize_previous_data(dependant_sle)
646 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530647 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530648
649 def update_distinct_item_warehouses(self, dependant_sle):
650 key = (dependant_sle.item_code, dependant_sle.warehouse)
mergify[bot]87d02512023-08-19 15:37:33 +0530651 val = frappe._dict({"sle": dependant_sle})
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530652
Nabin Hait97bce3a2021-07-12 13:24:43 +0530653 if key not in self.distinct_item_warehouses:
654 self.distinct_item_warehouses[key] = val
655 self.new_items_found = True
656 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530657 existing_sle_posting_date = (
658 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
659 )
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530660
661 dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
662
Nabin Hait97bce3a2021-07-12 13:24:43 +0530663 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
664 val.sle_changed = True
mergify[bot]87d02512023-08-19 15:37:33 +0530665 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
666 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530667 self.distinct_item_warehouses[key] = val
668 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530669 elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
670 # Future dependent voucher needs to be repost to get the correct stock value
671 # If dependent voucher has not reposted, then add it to the list
672 dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
Rohit Waghchaureb77a8082023-07-01 11:30:46 +0530673 self.new_items_found = True
Rohit Waghchaurec16a5812023-07-11 17:51:27 +0530674 val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
675 self.distinct_item_warehouses[key] = val
676
677 def get_dependent_voucher_detail_nos(self, key):
678 if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
679 self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
680
681 return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
Nabin Hait97bce3a2021-07-12 13:24:43 +0530682
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530683 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530684 # previous sle data for this warehouse
685 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530686 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530687
Anand Doshi0dc79f42015-04-06 12:59:34 +0530688 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 +0530689 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530690 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530691 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530692 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530693 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530694
Nabin Haita77b8c92020-12-21 14:45:50 +0530695 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530696 if not self.args.get("sle_id"):
697 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530698
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530699 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530700 sle.voucher_type == "Stock Reconciliation"
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530701 and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle))
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530702 and sle.voucher_detail_no
703 and sle.actual_qty < 0
704 ):
705 self.reset_actual_qty_for_stock_reco(sle)
706
707 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530708 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
709 and sle.voucher_detail_no
710 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530711 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530712 ):
713 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
714
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530715 dimensions = get_inventory_dimensions()
716 has_dimensions = False
717 if dimensions:
718 for dimension in dimensions:
719 if sle.get(dimension.get("fieldname")):
720 has_dimensions = True
721
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530722 if sle.serial_and_batch_bundle:
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530723 self.calculate_valuation_for_serial_batch_bundle(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530724 else:
mergify[bot]27a1e3b2023-10-16 19:15:18 +0530725 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530726 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530727 self.wh_data.valuation_rate = sle.valuation_rate
728 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530729 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
730 self.wh_data.valuation_rate
731 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530732 if self.valuation_method != "Moving Average":
733 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530734 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530735 if self.valuation_method == "Moving Average":
736 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530737 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530738 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
739 self.wh_data.valuation_rate
740 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530741 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530742 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530743
Rushabh Mehta54047782013-12-26 11:07:46 +0530744 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530745 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530746 if not self.wh_data.qty_after_transaction:
747 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530748
Nabin Haita77b8c92020-12-21 14:45:50 +0530749 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
750 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530751
Nabin Hait902e8602013-01-08 18:29:24 +0530752 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530753 sle.qty_after_transaction = self.wh_data.qty_after_transaction
754 sle.valuation_rate = self.wh_data.valuation_rate
755 sle.stock_value = self.wh_data.stock_value
756 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530757 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530758 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530759
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530760 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530761
Ankush Menat701878f2022-03-01 18:08:29 +0530762 if not self.args.get("sle_id"):
763 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530764
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530765 def reset_actual_qty_for_stock_reco(self, sle):
Rohit Waghchaure42b22942023-05-27 19:18:03 +0530766 if sle.serial_and_batch_bundle:
767 current_qty = frappe.get_cached_value(
768 "Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty"
769 )
770
771 if current_qty is not None:
772 current_qty = abs(current_qty)
773 else:
774 current_qty = frappe.get_cached_value(
775 "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
776 )
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530777
778 if current_qty:
779 sle.actual_qty = current_qty * -1
780 elif current_qty == 0:
781 sle.is_cancelled = 1
782
Rohit Waghchaured3ceb072023-03-31 09:03:54 +0530783 def calculate_valuation_for_serial_batch_bundle(self, sle):
784 doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
785
786 doc.set_incoming_rate(save=True)
787 doc.calculate_qty_and_amount(save=True)
788
789 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
790
791 self.wh_data.qty_after_transaction += doc.total_qty
792 if self.wh_data.qty_after_transaction:
793 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
794
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530795 def validate_negative_stock(self, sle):
796 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530797 validate negative stock for entries current datetime onwards
798 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530799 """
s-aga-rf0acb202023-04-12 14:13:54 +0530800 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530801 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530802
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530803 if diff < 0 and abs(diff) > 0.0001:
804 # negative stock!
805 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530806 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530807 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530808 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530809 return True
810
Nabin Haita77b8c92020-12-21 14:45:50 +0530811 def get_dynamic_incoming_outgoing_rate(self, sle):
812 # Get updated incoming/outgoing rate from transaction
813 if sle.recalculate_rate:
814 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
815
816 if flt(sle.actual_qty) >= 0:
817 sle.incoming_rate = rate
818 else:
819 sle.outgoing_rate = rate
820
821 def get_incoming_outgoing_rate_from_transaction(self, sle):
822 rate = 0
823 # Material Transfer, Repack, Manufacturing
824 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530825 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530826 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
827 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530828 elif sle.voucher_type in (
829 "Purchase Receipt",
830 "Purchase Invoice",
831 "Delivery Note",
832 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530833 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530834 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530835 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530836 from erpnext.controllers.sales_and_purchase_return import (
837 get_rate_for_return, # don't move this import to top
838 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530839
840 rate = get_rate_for_return(
841 sle.voucher_type,
842 sle.voucher_no,
843 sle.item_code,
844 voucher_detail_no=sle.voucher_detail_no,
845 sle=sle,
846 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530847
848 elif (
849 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530850 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530851 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530852 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530853 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530854 else:
855 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530856 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530857 elif sle.voucher_type == "Subcontracting Receipt":
858 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530859 else:
860 rate_field = "incoming_rate"
861
862 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530863 item_code, incoming_rate = frappe.db.get_value(
864 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
865 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530866
867 if item_code == sle.item_code:
868 rate = incoming_rate
869 else:
870 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
871 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530872 elif sle == "Subcontracting Receipt":
873 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530874 else:
875 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530876
Ankush Menat494bd9e2022-03-28 18:52:46 +0530877 rate = frappe.db.get_value(
878 ref_doctype,
879 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
880 rate_field,
881 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530882
883 return rate
884
885 def update_outgoing_rate_on_transaction(self, sle):
886 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530887 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
888 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530889 """
890 if sle.actual_qty and sle.voucher_detail_no:
891 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
892
893 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
894 self.update_rate_on_stock_entry(sle, outgoing_rate)
895 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
896 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
897 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
898 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530899 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
900 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530901 elif sle.voucher_type == "Stock Reconciliation":
902 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530903
904 def update_rate_on_stock_entry(self, sle, outgoing_rate):
905 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
906
Ankush Menat701878f2022-03-01 18:08:29 +0530907 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
908 if not sle.dependant_sle_voucher_detail_no:
909 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530910
911 def recalculate_amounts_in_stock_entry(self, voucher_no):
912 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530913 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
914 stock_entry.db_update()
915 for d in stock_entry.items:
916 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530917
Nabin Haita77b8c92020-12-21 14:45:50 +0530918 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
919 # Update item's incoming rate on transaction
920 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
921 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530922 frappe.db.set_value(
923 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
924 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530925 else:
926 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530927 frappe.db.set_value(
928 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530929 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530930 "incoming_rate",
931 outgoing_rate,
932 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530933
934 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
935 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530936 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
937 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
938 ):
939 frappe.db.set_value(
940 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
941 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530942 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530943 frappe.db.set_value(
944 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
945 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530946
947 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530948 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530949 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530950 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530951 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530952 d.db_update()
953
Sagar Sharma323bdf82022-05-17 15:14:07 +0530954 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530955 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
956 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530957 else:
958 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530959 "Subcontracting Receipt Supplied Item",
960 sle.voucher_detail_no,
961 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530962 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530963
s-aga-ra6cb6c62023-05-03 09:51:58 +0530964 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530965 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530966 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530967 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530968 d.db_update()
969
s-aga-r88a3f652023-05-30 16:54:28 +0530970 def update_rate_on_stock_reconciliation(self, sle):
971 if not sle.serial_no and not sle.batch_no:
972 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
973
974 for item in sr.items:
975 # Skip for Serial and Batch Items
s-aga-rdb159dd2023-06-12 18:28:16 +0530976 if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
s-aga-r88a3f652023-05-30 16:54:28 +0530977 continue
978
979 previous_sle = get_previous_sle(
980 {
981 "item_code": item.item_code,
982 "warehouse": item.warehouse,
983 "posting_date": sr.posting_date,
984 "posting_time": sr.posting_time,
985 "sle": sle.name,
986 }
987 )
988
989 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
990 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
991 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
992
993 item.amount = flt(item.qty) * flt(item.valuation_rate)
s-aga-r6a1b0a22023-06-15 11:39:22 +0530994 item.quantity_difference = item.qty - item.current_qty
s-aga-r88a3f652023-05-30 16:54:28 +0530995 item.amount_difference = item.amount - item.current_amount
996 else:
997 sr.difference_amount = sum([item.amount_difference for item in sr.items])
998 sr.db_update()
999
1000 for item in sr.items:
1001 item.db_update()
1002
Nabin Hait328c4f92020-01-02 19:00:32 +05301003 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
1004 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301005 all_serial_nos = frappe.get_all(
1006 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
1007 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301008
Ankush Menat494bd9e2022-03-28 18:52:46 +05301009 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 +05301010
1011 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +05301012 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +05301013 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301014 incoming_rate = frappe.db.sql(
1015 """
Nabin Hait328c4f92020-01-02 19:00:32 +05301016 select incoming_rate
1017 from `tabStock Ledger Entry`
1018 where
1019 company = %s
1020 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301021 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +05301022 and (serial_no = %s
1023 or serial_no like %s
1024 or serial_no like %s
1025 or serial_no like %s
1026 )
1027 order by posting_date desc
1028 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301029 """,
1030 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
1031 )
Nabin Hait328c4f92020-01-02 19:00:32 +05301032
1033 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1034
1035 return incoming_values
1036
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301037 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301038 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301039 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301040 if new_stock_qty >= 0:
1041 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301042 if flt(self.wh_data.qty_after_transaction) <= 0:
1043 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301044 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301045 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1046 actual_qty * sle.incoming_rate
1047 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301048
Nabin Haita77b8c92020-12-21 14:45:50 +05301049 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301050
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301051 elif sle.outgoing_rate:
1052 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301053 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1054 actual_qty * sle.outgoing_rate
1055 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301056
Nabin Haita77b8c92020-12-21 14:45:50 +05301057 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301058 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301059 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301060 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301061 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1062 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301063
Nabin Haita77b8c92020-12-21 14:45:50 +05301064 if not self.wh_data.valuation_rate and actual_qty > 0:
1065 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301066
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301067 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001068 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301069 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301070 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1071 sle.voucher_type, sle.voucher_detail_no
1072 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001073 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301074 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301075
Ankush Menatf089d392022-02-02 12:51:21 +05301076 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301077 incoming_rate = flt(sle.incoming_rate)
1078 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301079 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301080
Ankush Menat494bd9e2022-03-28 18:52:46 +05301081 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1082 self.wh_data.qty_after_transaction + actual_qty
1083 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301084
Ankush Menat97e18a12022-01-15 17:42:25 +05301085 if self.valuation_method == "LIFO":
1086 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1087 else:
1088 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1089
Ankush Menatb534fee2022-02-19 20:58:36 +05301090 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1091
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301092 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301093 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301094 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301095
Ankush Menat4b29fb62021-12-18 18:40:22 +05301096 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301097 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1098 sle.voucher_type, sle.voucher_detail_no
1099 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301100 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301101 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301102 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301103 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301104
Ankush Menat494bd9e2022-03-28 18:52:46 +05301105 stock_queue.remove_stock(
1106 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1107 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301108
Ankush Menatb534fee2022-02-19 20:58:36 +05301109 _qty, stock_value = stock_queue.get_total_stock_and_value()
1110
1111 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301112
Ankush Menat97e18a12022-01-15 17:42:25 +05301113 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301114 self.wh_data.stock_value = round_off_if_near_zero(
1115 self.wh_data.stock_value + stock_value_difference
1116 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301117
Nabin Haita77b8c92020-12-21 14:45:50 +05301118 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301119 self.wh_data.stock_queue.append(
1120 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1121 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301122
Ankush Menatb534fee2022-02-19 20:58:36 +05301123 if self.wh_data.qty_after_transaction:
1124 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1125
Ankush Menatce0514c2022-02-15 11:41:41 +05301126 def update_batched_values(self, sle):
1127 incoming_rate = flt(sle.incoming_rate)
1128 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301129
Ankush Menat494bd9e2022-03-28 18:52:46 +05301130 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1131 self.wh_data.qty_after_transaction + actual_qty
1132 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301133
1134 if actual_qty > 0:
1135 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301136 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301137 outgoing_rate = get_batch_incoming_rate(
1138 item_code=sle.item_code,
1139 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301140 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301141 posting_date=sle.posting_date,
1142 posting_time=sle.posting_time,
1143 creation=sle.creation,
1144 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301145 if outgoing_rate is None:
1146 # This can *only* happen if qty available for the batch is zero.
1147 # in such case fall back various other rates.
1148 # future entries will correct the overall accounting as each
1149 # batch individually uses moving average rates.
1150 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301151 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301152
Ankush Menat494bd9e2022-03-28 18:52:46 +05301153 self.wh_data.stock_value = round_off_if_near_zero(
1154 self.wh_data.stock_value + stock_value_difference
1155 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301156 if self.wh_data.qty_after_transaction:
1157 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301158
Javier Wong9b11d9b2017-04-14 18:24:04 +08001159 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301160 ref_item_dt = ""
1161
1162 if voucher_type == "Stock Entry":
1163 ref_item_dt = voucher_type + " Detail"
1164 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1165 ref_item_dt = voucher_type + " Item"
1166
1167 if ref_item_dt:
1168 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1169 else:
1170 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301171
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301172 def get_fallback_rate(self, sle) -> float:
1173 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301174 This should only get used for negative stock."""
1175 return get_valuation_rate(
1176 sle.item_code,
1177 sle.warehouse,
1178 sle.voucher_type,
1179 sle.voucher_no,
1180 self.allow_zero_rate,
1181 currency=erpnext.get_company_currency(sle.company),
1182 company=sle.company,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301183 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301184
Nabin Haita77b8c92020-12-21 14:45:50 +05301185 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301186 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301187 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1188 sle = sle[0] if sle else frappe._dict()
1189 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301190
Nabin Haita77b8c92020-12-21 14:45:50 +05301191 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301192 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301193 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301194
1195 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301196 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301197 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301198 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301199
Ankush Menat494bd9e2022-03-28 18:52:46 +05301200 if (
1201 exceptions[0]["voucher_type"],
1202 exceptions[0]["voucher_no"],
1203 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301204
Nabin Haita77b8c92020-12-21 14:45:50 +05301205 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301206 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301207 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1208 frappe.get_desk_link("Warehouse", warehouse),
1209 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301210 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301211 msg = _(
1212 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1213 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301214 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301215 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1216 frappe.get_desk_link("Warehouse", warehouse),
1217 exceptions[0]["posting_date"],
1218 exceptions[0]["posting_time"],
1219 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1220 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301221
Nabin Haita77b8c92020-12-21 14:45:50 +05301222 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301223 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301224 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
s-aga-r2d8363a2023-09-02 11:02:24 +05301225
1226 if allowed_qty > 0:
1227 msg = "{0} As {1} units are reserved for other sales orders, you are allowed to consume only {2} units.".format(
1228 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1229 )
1230 else:
1231 msg = "{0} As the full stock is reserved for other sales orders, you're not allowed to consume the stock.".format(
1232 msg,
1233 )
s-aga-rf0acb202023-04-12 14:13:54 +05301234
Nabin Haita77b8c92020-12-21 14:45:50 +05301235 msg_list.append(msg)
1236
1237 if msg_list:
1238 message = "\n\n".join(msg_list)
1239 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301240 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301241 else:
1242 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301243
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301244 def update_bin_data(self, sle):
1245 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301246 values_to_update = {
1247 "actual_qty": sle.qty_after_transaction,
1248 "stock_value": sle.stock_value,
1249 }
1250
1251 if sle.valuation_rate is not None:
1252 values_to_update["valuation_rate"] = sle.valuation_rate
1253
1254 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301255
Nabin Haita77b8c92020-12-21 14:45:50 +05301256 def update_bin(self):
1257 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301258 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301259 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301260
Ankush Menat494bd9e2022-03-28 18:52:46 +05301261 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301262 if data.valuation_rate is not None:
1263 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301264 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301265
marination8418c4b2021-06-22 21:35:25 +05301266
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301267def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301268 """get stock ledger entries filtered by specific posting datetime conditions"""
1269
Ankush Menat494bd9e2022-03-28 18:52:46 +05301270 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301271 if not args.get("posting_date"):
1272 args["posting_date"] = "1900-01-01"
1273 if not args.get("posting_time"):
1274 args["posting_time"] = "00:00"
1275
1276 voucher_condition = ""
1277 if exclude_current_voucher:
1278 voucher_no = args.get("voucher_no")
1279 voucher_condition = f"and voucher_no != '{voucher_no}'"
1280
Ankush Menat494bd9e2022-03-28 18:52:46 +05301281 sle = frappe.db.sql(
1282 """
marination8418c4b2021-06-22 21:35:25 +05301283 select *, timestamp(posting_date, posting_time) as "timestamp"
1284 from `tabStock Ledger Entry`
1285 where item_code = %(item_code)s
1286 and warehouse = %(warehouse)s
1287 and is_cancelled = 0
1288 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301289 and (
1290 posting_date < %(posting_date)s or
1291 (
1292 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301293 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301294 )
1295 )
marination8418c4b2021-06-22 21:35:25 +05301296 order by timestamp(posting_date, posting_time) desc, creation desc
1297 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301298 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301299 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301300 ),
1301 args,
1302 as_dict=1,
1303 )
marination8418c4b2021-06-22 21:35:25 +05301304
1305 return sle[0] if sle else frappe._dict()
1306
Ankush Menat494bd9e2022-03-28 18:52:46 +05301307
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301308def get_previous_sle(args, for_update=False, extra_cond=None):
Anand Doshi1b531862013-01-10 19:29:51 +05301309 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301310 get the last sle on or before the current time-bucket,
1311 to get actual qty before transaction, this function
1312 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301313
Ankush Menat494bd9e2022-03-28 18:52:46 +05301314 args = {
1315 "item_code": "ABC",
1316 "warehouse": "XYZ",
1317 "posting_date": "2012-12-12",
1318 "posting_time": "12:00",
1319 "sle": "name of reference Stock Ledger Entry"
1320 }
Anand Doshi1b531862013-01-10 19:29:51 +05301321 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301322 args["name"] = args.get("sle", None) or ""
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301323 sle = get_stock_ledger_entries(
1324 args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond
1325 )
Pratik Vyas16371b72013-09-18 18:31:03 +05301326 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301327
Ankush Menat494bd9e2022-03-28 18:52:46 +05301328
1329def get_stock_ledger_entries(
1330 previous_sle,
1331 operator=None,
1332 order="desc",
1333 limit=None,
1334 for_update=False,
1335 debug=False,
1336 check_serial_no=True,
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301337 extra_cond=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301338):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301339 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301340 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1341 operator
1342 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301343 if previous_sle.get("warehouse"):
1344 conditions += " and warehouse = %(warehouse)s"
1345 elif previous_sle.get("warehouse_condition"):
1346 conditions += " and " + previous_sle.get("warehouse_condition")
1347
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301348 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301349 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1350 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301351 conditions += (
1352 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301353 (
1354 serial_no = {0}
1355 or serial_no like {1}
1356 or serial_no like {2}
1357 or serial_no like {3}
1358 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301359 """
1360 ).format(
1361 frappe.db.escape(serial_no),
1362 frappe.db.escape("{}\n%".format(serial_no)),
1363 frappe.db.escape("%\n{}".format(serial_no)),
1364 frappe.db.escape("%\n{}\n%".format(serial_no)),
1365 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301366
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301367 if not previous_sle.get("posting_date"):
1368 previous_sle["posting_date"] = "1900-01-01"
1369 if not previous_sle.get("posting_time"):
1370 previous_sle["posting_time"] = "00:00"
1371
1372 if operator in (">", "<=") and previous_sle.get("name"):
1373 conditions += " and name!=%(name)s"
1374
mergify[bot]27a1e3b2023-10-16 19:15:18 +05301375 if extra_cond:
1376 conditions += f"{extra_cond}"
1377
Ankush Menat494bd9e2022-03-28 18:52:46 +05301378 return frappe.db.sql(
1379 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301380 select *, timestamp(posting_date, posting_time) as "timestamp"
1381 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301382 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301383 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301384 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301385 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301386 %(limit)s %(for_update)s"""
1387 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301388 "conditions": conditions,
1389 "limit": limit or "",
1390 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301391 "order": order,
1392 },
1393 previous_sle,
1394 as_dict=1,
1395 debug=debug,
1396 )
1397
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301398
Nabin Haita77b8c92020-12-21 14:45:50 +05301399def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301400 return frappe.db.get_value(
1401 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301402 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301403 [
1404 "item_code",
1405 "warehouse",
Rohit Waghchaureb77a8082023-07-01 11:30:46 +05301406 "actual_qty",
1407 "qty_after_transaction",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301408 "posting_date",
1409 "posting_time",
Rohit Waghchaurec16a5812023-07-11 17:51:27 +05301410 "voucher_detail_no",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301411 "timestamp(posting_date, posting_time) as timestamp",
1412 ],
1413 as_dict=1,
1414 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301415
Ankush Menatce0514c2022-02-15 11:41:41 +05301416
Ankush Menat494bd9e2022-03-28 18:52:46 +05301417def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301418 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301419):
1420
Ankush Menat102fff22022-02-19 15:51:04 +05301421 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301422 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301423
Ankush Menate1c16872022-04-21 20:01:48 +05301424 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301425 posting_date, posting_time
1426 )
Ankush Menat102fff22022-02-19 15:51:04 +05301427 if creation:
1428 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301429 CombineDatetime(sle.posting_date, sle.posting_time)
1430 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301431 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301432
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301433 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301434 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301435 )
1436
Ankush Menat102fff22022-02-19 15:51:04 +05301437 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301438 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301439 .inner_join(batch_ledger)
1440 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1441 .select(
1442 Sum(
1443 Case()
1444 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1445 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1446 ).as_("batch_value"),
1447 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1448 "batch_qty"
1449 ),
1450 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301451 .where(
1452 (sle.item_code == item_code)
1453 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301454 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301455 & (sle.is_cancelled == 0)
1456 )
1457 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301458 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301459
1460 if batch_details and batch_details[0].batch_qty:
1461 return batch_details[0].batch_value / batch_details[0].batch_qty
1462
1463
Ankush Menat494bd9e2022-03-28 18:52:46 +05301464def get_valuation_rate(
1465 item_code,
1466 warehouse,
1467 voucher_type,
1468 voucher_no,
1469 allow_zero_rate=False,
1470 currency=None,
1471 company=None,
1472 raise_error_if_no_rate=True,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301473 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301474):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301475
Rohit Waghchaured3ceb072023-03-31 09:03:54 +05301476 from erpnext.stock.serial_batch_bundle import BatchNoValuation
1477
Ankush Menatf7ffe042021-11-01 13:21:14 +05301478 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301479 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301480
Ankush Menat342d09a2022-02-19 14:28:51 +05301481 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301482 if warehouse and serial_and_batch_bundle:
1483 batch_obj = BatchNoValuation(
1484 sle=frappe._dict(
1485 {
1486 "item_code": item_code,
1487 "warehouse": warehouse,
1488 "actual_qty": -1,
1489 "serial_and_batch_bundle": serial_and_batch_bundle,
1490 }
1491 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301492 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301493
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301494 return batch_obj.get_incoming_rate()
1495
Ankush Menatf7ffe042021-11-01 13:21:14 +05301496 # Get valuation rate from last sle for the same item and warehouse
Akhil Narangdd911aa2023-09-26 13:45:39 +05301497 if last_valuation_rate := frappe.db.sql(
1498 """select valuation_rate
1499 from `tabStock Ledger Entry` force index (item_warehouse)
1500 where
1501 item_code = %s
1502 AND warehouse = %s
1503 AND valuation_rate >= 0
1504 AND is_cancelled = 0
1505 AND NOT (voucher_no = %s AND voucher_type = %s)
1506 order by posting_date desc, posting_time desc, name desc limit 1""",
1507 (item_code, warehouse, voucher_no, voucher_type),
1508 ):
Nabin Haita77b8c92020-12-21 14:45:50 +05301509 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301510
1511 # If negative stock allowed, and item delivered without any incoming entry,
1512 # system does not found any SLE, then take valuation rate from Item
1513 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301514
1515 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301516 # try Item Standard rate
1517 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301518
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301519 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301520 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301521 valuation_rate = frappe.db.get_value(
1522 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1523 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301524
Ankush Menat494bd9e2022-03-28 18:52:46 +05301525 if (
1526 not allow_zero_rate
1527 and not valuation_rate
1528 and raise_error_if_no_rate
1529 and cint(erpnext.is_perpetual_inventory_enabled(company))
1530 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301531 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301532
Ankush Menat494bd9e2022-03-28 18:52:46 +05301533 message = _(
1534 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1535 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301536 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301537 solutions = (
1538 "<li>"
1539 + _(
1540 "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."
1541 ).format(voucher_type)
1542 + "</li>"
1543 )
1544 solutions += (
1545 "<li>"
1546 + _("If not, you can Cancel / Submit this entry")
1547 + " {0} ".format(frappe.bold("after"))
1548 + _("performing either one below:")
1549 + "</li>"
1550 )
Marica97715f22020-05-11 20:45:37 +05301551 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1552 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1553 msg = message + solutions + sub_solutions + "</li>"
1554
1555 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301556
1557 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301558
Ankush Menat494bd9e2022-03-28 18:52:46 +05301559
Ankush Menate7109c12021-08-26 16:40:45 +05301560def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301561 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301562 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301563 qty_shift = args.actual_qty
1564
Ankush Menat7c839c42022-05-06 12:09:08 +05301565 args["time_format"] = "%H:%i:%s"
1566
marination8418c4b2021-06-22 21:35:25 +05301567 # find difference/shift in qty caused by stock reconciliation
1568 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301569 qty_shift = get_stock_reco_qty_shift(args)
1570
1571 # find the next nearest stock reco so that we only recalculate SLEs till that point
1572 next_stock_reco_detail = get_next_stock_reco(args)
1573 if next_stock_reco_detail:
1574 detail = next_stock_reco_detail[0]
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301575 if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no):
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301576 regenerate_sle_for_batch_stock_reco(detail)
1577
marination40389772021-07-02 17:13:45 +05301578 # add condition to update SLEs before this date & time
1579 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301580
Ankush Menat494bd9e2022-03-28 18:52:46 +05301581 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301582 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301583 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301584 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301585 where
1586 item_code = %(item_code)s
1587 and warehouse = %(warehouse)s
1588 and voucher_no != %(voucher_no)s
1589 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301590 and (
1591 posting_date > %(posting_date)s or
1592 (
1593 posting_date = %(posting_date)s and
1594 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1595 )
1596 )
marination40389772021-07-02 17:13:45 +05301597 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301598 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301599 args,
1600 )
Nabin Hait186a0452021-02-18 14:14:21 +05301601
1602 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1603
Ankush Menat494bd9e2022-03-28 18:52:46 +05301604
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301605def regenerate_sle_for_batch_stock_reco(detail):
1606 doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301607 doc.recalculate_current_qty(detail.item_code, detail.batch_no)
Rohit Waghchauref2253dd2023-04-22 11:16:12 +05301608
1609 if not frappe.db.exists(
1610 "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
1611 ):
Rohit Waghchaure2d5ccc02023-05-01 21:17:18 +05301612 doc.repost_future_sle_and_gle(force=True)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301613
1614
marination40389772021-07-02 17:13:45 +05301615def get_stock_reco_qty_shift(args):
1616 stock_reco_qty_shift = 0
1617 if args.get("is_cancelled"):
1618 if args.get("previous_qty_after_transaction"):
1619 # get qty (balance) that was set at submission
1620 last_balance = args.get("previous_qty_after_transaction")
1621 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1622 else:
1623 stock_reco_qty_shift = flt(args.actual_qty)
1624 else:
1625 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301626 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301627 "qty_after_transaction"
1628 )
marination40389772021-07-02 17:13:45 +05301629
1630 if last_balance is not None:
1631 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1632 else:
1633 stock_reco_qty_shift = args.qty_after_transaction
1634
1635 return stock_reco_qty_shift
1636
Ankush Menat494bd9e2022-03-28 18:52:46 +05301637
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301638def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301639 """Returns next nearest stock reconciliaton's details."""
1640
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301641 sle = frappe.qb.DocType("Stock Ledger Entry")
1642
1643 query = (
1644 frappe.qb.from_(sle)
1645 .select(
1646 sle.name,
1647 sle.posting_date,
1648 sle.posting_time,
1649 sle.creation,
1650 sle.voucher_no,
1651 sle.item_code,
1652 sle.batch_no,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301653 sle.serial_and_batch_bundle,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301654 sle.actual_qty,
Rohit Waghchaure42b22942023-05-27 19:18:03 +05301655 sle.has_batch_no,
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301656 )
1657 .where(
1658 (sle.item_code == kwargs.get("item_code"))
1659 & (sle.warehouse == kwargs.get("warehouse"))
1660 & (sle.voucher_type == "Stock Reconciliation")
1661 & (sle.voucher_no != kwargs.get("voucher_no"))
1662 & (sle.is_cancelled == 0)
1663 & (
1664 (
1665 CombineDatetime(sle.posting_date, sle.posting_time)
1666 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301667 )
1668 | (
1669 (
1670 CombineDatetime(sle.posting_date, sle.posting_time)
1671 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301672 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301673 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301674 )
1675 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301676 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301677 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1678 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301679 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301680 )
1681
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301682 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301683 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301684
1685 return query.run(as_dict=True)
1686
marination40389772021-07-02 17:13:45 +05301687
1688def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301689 return f"""
1690 and
1691 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1692 or (
1693 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1694 and creation < '{detail.creation}'
1695 )
1696 )"""
1697
Ankush Menat494bd9e2022-03-28 18:52:46 +05301698
Ankush Menate7109c12021-08-26 16:40:45 +05301699def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301700 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301701 return
1702 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1703 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301704
Ankush Menat5eba5752021-12-07 23:03:52 +05301705 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301706
1707 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301708 message = _(
1709 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1710 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301711 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301712 frappe.get_desk_link("Item", args.item_code),
1713 frappe.get_desk_link("Warehouse", args.warehouse),
1714 neg_sle[0]["posting_date"],
1715 neg_sle[0]["posting_time"],
1716 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1717 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301718
Ankush Menat494bd9e2022-03-28 18:52:46 +05301719 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301720
1721 if not args.batch_no:
1722 return
1723
1724 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301725 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301726 message = _(
1727 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1728 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301729 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301730 frappe.get_desk_link("Batch", args.batch_no),
1731 frappe.get_desk_link("Warehouse", args.warehouse),
1732 neg_batch_sle[0]["posting_date"],
1733 neg_batch_sle[0]["posting_time"],
1734 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1735 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301736 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301737
Nabin Haita77b8c92020-12-21 14:45:50 +05301738
Maricad6078aa2022-06-17 15:13:13 +05301739def is_negative_with_precision(neg_sle, is_batch=False):
1740 """
1741 Returns whether system precision rounded qty is insufficient.
1742 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1743 """
1744
1745 if not neg_sle:
1746 return False
1747
1748 field = "cumulative_total" if is_batch else "qty_after_transaction"
1749 precision = cint(frappe.db.get_default("float_precision")) or 2
1750 qty_deficit = flt(neg_sle[0][field], precision)
1751
1752 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1753
1754
Nabin Haita77b8c92020-12-21 14:45:50 +05301755def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301756 return frappe.db.sql(
1757 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301758 select
1759 qty_after_transaction, posting_date, posting_time,
1760 voucher_type, voucher_no
1761 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301762 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301763 item_code = %(item_code)s
1764 and warehouse = %(warehouse)s
1765 and voucher_no != %(voucher_no)s
1766 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1767 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301768 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301769 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301770 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301771 """,
1772 args,
1773 as_dict=1,
1774 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301775
Ankush Menat5eba5752021-12-07 23:03:52 +05301776
1777def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301778 return frappe.db.sql(
1779 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301780 with batch_ledger as (
1781 select
1782 posting_date, posting_time, voucher_type, voucher_no,
1783 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1784 from `tabStock Ledger Entry`
1785 where
1786 item_code = %(item_code)s
1787 and warehouse = %(warehouse)s
1788 and batch_no=%(batch_no)s
1789 and is_cancelled = 0
1790 order by posting_date, posting_time, creation
1791 )
1792 select * from batch_ledger
1793 where
1794 cumulative_total < 0.0
1795 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1796 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301797 """,
1798 args,
1799 as_dict=1,
1800 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301801
1802
1803def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1804 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1805 return True
1806 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1807 return True
1808 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301809
1810
1811def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1812 """
1813 For inter company transfer, incoming rate is the average of the outgoing rate
1814 """
1815 rate = 0.0
1816
1817 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1818
1819 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1820
1821 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1822
1823 if reference_name:
1824 rate = frappe.get_cached_value(
1825 doctype,
1826 reference_name,
1827 "incoming_rate",
1828 )
1829
1830 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301831
1832
1833def is_internal_transfer(sle):
1834 data = frappe.get_cached_value(
1835 sle.voucher_type,
1836 sle.voucher_no,
1837 ["is_internal_supplier", "represents_company", "company"],
1838 as_dict=True,
1839 )
1840
1841 if data.is_internal_supplier and data.represents_company == data.company:
1842 return True