blob: ba1081f4dcec481778da2fb6f6de730d5ab0b34c [file] [log] [blame]
Anand Doshi885e0742015-03-03 14:55:30 +05301# Copyright (c) 2015, 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 Menateb8b4242022-02-12 13:08:28 +05306from typing import Optional
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
9from frappe import _
10from frappe.model.meta import get_field_precision
Ankush Menat102fff22022-02-19 15:51:04 +053011from frappe.query_builder.functions import Sum
Ankush Menatcef84c22021-12-03 12:18:59 +053012from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
Ankush Menat102fff22022-02-19 15:51:04 +053013from pypika import CustomFunction
Achilles Rasquinha361366e2018-02-14 17:08:59 +053014
Chillar Anand915b3432021-09-02 16:44:59 +053015import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053016from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
Chillar Anand915b3432021-09-02 16:44:59 +053017from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053018 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053019 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053020 get_valuation_method,
21)
Ankush Menatb534fee2022-02-19 20:58:36 +053022from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053023
Nabin Hait97bce3a2021-07-12 13:24:43 +053024
Rushabh Mehta793ba6b2014-02-14 15:47:51 +053025class NegativeStockError(frappe.ValidationError): pass
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053026class SerialNoExistsInFutureTransaction(frappe.ValidationError):
27 pass
Nabin Hait902e8602013-01-08 18:29:24 +053028
Anand Doshi5b004ff2013-09-25 19:55:41 +053029
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053030def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menateb8495a2022-03-02 12:01:51 +053031 """ Create SL entries from SL entry dicts
32
33 args:
34 - allow_negative_stock: disable negative stock valiations if true
35 - via_landed_cost_voucher: landed cost voucher cancels and reposts
36 entries of purchase document. This flag is used to identify if
37 cancellation and repost is happening via landed cost voucher, in
38 such cases certain validations need to be ignored (like negative
39 stock)
40 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053041 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Haitca775742013-09-26 16:16:44 +053042 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053043 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053044 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053045 validate_cancellation(sl_entries)
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053046 set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053047
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053048 args = get_args_for_future_sle(sl_entries[0])
49 future_sle_exists(args, sl_entries)
50
Nabin Haitca775742013-09-26 16:16:44 +053051 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053052 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053053 validate_serial_no(sle)
54
Nabin Haita77b8c92020-12-21 14:45:50 +053055 if cancel:
56 sle['actual_qty'] = -flt(sle.get('actual_qty'))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053057
Nabin Haita77b8c92020-12-21 14:45:50 +053058 if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
59 sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
60 sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
61 sle['incoming_rate'] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053062
Nabin Haita77b8c92020-12-21 14:45:50 +053063 if sle['actual_qty'] > 0 and not sle.get('incoming_rate'):
64 sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
65 sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
66 sle['outgoing_rate'] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053067
Nabin Hait5288bde2014-11-03 15:08:21 +053068 if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053069 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053070
Nabin Haita77b8c92020-12-21 14:45:50 +053071 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053072
73 if sle.get("voucher_type") == "Stock Reconciliation":
74 # preserve previous_qty_after_transaction for qty reposting
75 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
76
Ankush Menatcef84c22021-12-03 12:18:59 +053077 is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
78 if is_stock_item:
79 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053080 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053081 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053082 else:
83 frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
84
85def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
86 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
87 if not args.get("posting_date"):
88 args["posting_date"] = nowdate()
89
90 if args.get("is_cancelled") and via_landed_cost_voucher:
91 return
92
93 # Reposts only current voucher SL Entries
94 # Updates valuation rate, stock value, stock queue for current transaction
95 update_entries_after({
96 "item_code": args.get('item_code'),
97 "warehouse": args.get('warehouse'),
98 "posting_date": args.get("posting_date"),
99 "posting_time": args.get("posting_time"),
100 "voucher_type": args.get("voucher_type"),
101 "voucher_no": args.get("voucher_no"),
102 "sle_id": args.get('name'),
103 "creation": args.get('creation')
104 }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
105
106 # update qty in future sle and Validate negative qty
107 update_qty_in_future_sle(args, allow_negative_stock)
108
Nabin Haitadeb9762014-10-06 11:53:52 +0530109
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530110def get_args_for_future_sle(row):
111 return frappe._dict({
112 'voucher_type': row.get('voucher_type'),
113 'voucher_no': row.get('voucher_no'),
114 'posting_date': row.get('posting_date'),
115 'posting_time': row.get('posting_time')
116 })
117
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530118def validate_serial_no(sle):
119 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530120
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530121 for sn in get_serial_nos(sle.serial_no):
122 args = copy.deepcopy(sle)
123 args.serial_no = sn
124 args.warehouse = ''
125
126 vouchers = []
127 for row in get_stock_ledger_entries(args, '>'):
128 voucher_type = frappe.bold(row.voucher_type)
129 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
130 vouchers.append(f'{voucher_type} {voucher_no}')
131
132 if vouchers:
133 serial_no = frappe.bold(sn)
134 msg = (f'''The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
135 The list of the transactions are as below.''' + '<br><br><ul><li>')
136
137 msg += '</li><li>'.join(vouchers)
138 msg += '</li></ul>'
139
140 title = 'Cannot Submit' if not sle.get('is_cancelled') else 'Cannot Cancel'
141 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
142
Nabin Hait186a0452021-02-18 14:14:21 +0530143def validate_cancellation(args):
144 if args[0].get("is_cancelled"):
145 repost_entry = frappe.db.get_value("Repost Item Valuation", {
146 'voucher_type': args[0].voucher_type,
147 'voucher_no': args[0].voucher_no,
148 'docstatus': 1
149 }, ['name', 'status'], as_dict=1)
150
151 if repost_entry:
152 if repost_entry.status == 'In Progress':
153 frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
154 if repost_entry.status == 'Queued':
Nabin Haitd46b2362021-02-23 16:38:52 +0530155 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menataa024fc2021-11-18 12:51:26 +0530156 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530157 doc.cancel()
158 doc.delete()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530159
Nabin Hait9653f602013-08-20 15:37:33 +0530160def set_as_cancel(voucher_type, voucher_no):
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530161 frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530162 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530163 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Rushabh Mehta793ba6b2014-02-14 15:47:51 +0530164 (now(), frappe.session.user, voucher_type, voucher_no))
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530165
Nabin Hait54c865e2015-03-27 15:38:31 +0530166def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530167 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530168 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530169 sle.flags.ignore_permissions = 1
Nabin Hait4ccd8d32015-01-23 12:18:01 +0530170 sle.allow_negative_stock=allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530171 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530172 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530173 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530174
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530175def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False, doc=None):
Nabin Haita77b8c92020-12-21 14:45:50 +0530176 if not args and voucher_type and voucher_no:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530177 args = get_items_to_be_repost(voucher_type, voucher_no, doc)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530178
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530179 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530180
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530181 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530182 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530183 validate_item_warehouse(args[i])
184
Nabin Haita77b8c92020-12-21 14:45:50 +0530185 obj = update_entries_after({
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530186 'item_code': args[i].get('item_code'),
187 'warehouse': args[i].get('warehouse'),
188 'posting_date': args[i].get('posting_date'),
189 'posting_time': args[i].get('posting_time'),
190 'creation': args[i].get('creation'),
191 'distinct_item_warehouses': distinct_item_warehouses
Nabin Haita77b8c92020-12-21 14:45:50 +0530192 }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
193
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530194 distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530195
Nabin Hait97bce3a2021-07-12 13:24:43 +0530196 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530197 for item_wh, data in distinct_item_warehouses.items():
Nabin Hait97bce3a2021-07-12 13:24:43 +0530198 if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status):
199 data.args_idx = len(args)
200 args.append(data.sle)
201 elif data.sle_changed and not data.reposting_status:
202 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530203
Nabin Hait97bce3a2021-07-12 13:24:43 +0530204 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530205 i += 1
206
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530207 if doc and i % 2 == 0:
208 update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
209
210 if doc and args:
211 update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
212
213def validate_item_warehouse(args):
214 for field in ['item_code', 'warehouse', 'posting_date', 'posting_time']:
215 if not args.get(field):
216 validation_msg = f'The field {frappe.unscrub(args.get(field))} is required for the reposting'
217 frappe.throw(_(validation_msg))
218
219def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses):
220 frappe.db.set_value(doc.doctype, doc.name, {
221 'items_to_be_repost': json.dumps(args, default=str),
222 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str),
223 'current_index': index
224 })
225
226 frappe.db.commit()
227
228 frappe.publish_realtime('item_reposting_progress', {
229 'name': doc.name,
230 'items_to_be_repost': json.dumps(args, default=str),
231 'current_index': index
232 })
233
234def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
235 if doc and doc.items_to_be_repost:
236 return json.loads(doc.items_to_be_repost) or []
237
Nabin Haita77b8c92020-12-21 14:45:50 +0530238 return frappe.db.get_all("Stock Ledger Entry",
239 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
Nabin Hait186a0452021-02-18 14:14:21 +0530240 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
Nabin Haita77b8c92020-12-21 14:45:50 +0530241 order_by="creation asc",
242 group_by="item_code, warehouse"
243 )
Nabin Hait74c281c2013-08-19 16:17:18 +0530244
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530245def get_distinct_item_warehouse(args=None, doc=None):
246 distinct_item_warehouses = {}
247 if doc and doc.distinct_item_and_warehouse:
248 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
249 distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()}
250 else:
251 for i, d in enumerate(args):
252 distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({
253 "reposting_status": False,
254 "sle": d,
255 "args_idx": i
256 }))
257
258 return distinct_item_warehouses
259
260def get_current_index(doc=None):
261 if doc and doc.current_index:
262 return doc.current_index
263
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530264class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530265 """
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530266 update valution rate and qty after transaction
Nabin Hait902e8602013-01-08 18:29:24 +0530267 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530268
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530269 :param args: args as dict
270
271 args = {
272 "item_code": "ABC",
273 "warehouse": "XYZ",
274 "posting_date": "2012-12-12",
275 "posting_time": "12:00"
276 }
Nabin Hait902e8602013-01-08 18:29:24 +0530277 """
Anand Doshi0dc79f42015-04-06 12:59:34 +0530278 def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1):
Nabin Haita77b8c92020-12-21 14:45:50 +0530279 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530280 self.verbose = verbose
281 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530282 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530283 self.item_code = args.get("item_code")
284 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(item_code=self.item_code)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530285
Nabin Haita77b8c92020-12-21 14:45:50 +0530286 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530287 if self.args.sle_id:
288 self.args['name'] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530289
Nabin Haita77b8c92020-12-21 14:45:50 +0530290 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
291 self.get_precision()
292 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530293
294 self.new_items_found = False
295 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Nabin Haita77b8c92020-12-21 14:45:50 +0530296
297 self.data = frappe._dict()
298 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530299 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530300
Nabin Haita77b8c92020-12-21 14:45:50 +0530301 def get_precision(self):
302 company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency")
303 self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
304 currency=company_base_currency)
305
306 def initialize_previous_data(self, args):
307 """
308 Get previous sl entries for current item for each related warehouse
309 and assigns into self.data dict
310
311 :Data Structure:
312
313 self.data = {
314 warehouse1: {
315 'previus_sle': {},
316 'qty_after_transaction': 10,
317 'valuation_rate': 100,
318 'stock_value': 1000,
319 'prev_stock_value': 1000,
320 'stock_queue': '[[10, 100]]',
321 'stock_value_difference': 1000
322 }
323 }
324
325 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530326 self.data.setdefault(args.warehouse, frappe._dict())
327 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530328 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530329 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530330
Ankush Menatc1d986a2021-08-31 19:43:42 +0530331 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
332 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
333
334 warehouse_dict.update({
Nabin Haita77b8c92020-12-21 14:45:50 +0530335 "prev_stock_value": previous_sle.stock_value or 0.0,
336 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
337 "stock_value_difference": 0.0
338 })
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530339
Nabin Haita77b8c92020-12-21 14:45:50 +0530340 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530341 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530342
Nabin Haita77b8c92020-12-21 14:45:50 +0530343 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530344 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530345 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530346 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530347 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530348 entries_to_fix = self.get_future_entries_to_fix()
349
350 i = 0
351 while i < len(entries_to_fix):
352 sle = entries_to_fix[i]
353 i += 1
354
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530355 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530356
Nabin Haita77b8c92020-12-21 14:45:50 +0530357 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530358 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530359
Nabin Hait186a0452021-02-18 14:14:21 +0530360 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530361
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530362 if self.exceptions:
363 self.raise_exceptions()
364
Nabin Hait186a0452021-02-18 14:14:21 +0530365 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530366 sl_entries = self.get_sle_against_current_voucher()
367 for sle in sl_entries:
368 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530369
Nabin Haita77b8c92020-12-21 14:45:50 +0530370 def get_sle_against_current_voucher(self):
Nabin Haitf2be0802021-02-15 19:27:49 +0530371 self.args['time_format'] = '%H:%i:%s'
372
Nabin Haita77b8c92020-12-21 14:45:50 +0530373 return frappe.db.sql("""
374 select
375 *, timestamp(posting_date, posting_time) as "timestamp"
376 from
377 `tabStock Ledger Entry`
378 where
379 item_code = %(item_code)s
380 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530381 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530382 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
383
Nabin Haita77b8c92020-12-21 14:45:50 +0530384 order by
385 creation ASC
386 for update
387 """, self.args, as_dict=1)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530388
Nabin Haita77b8c92020-12-21 14:45:50 +0530389 def get_future_entries_to_fix(self):
390 # includes current entry!
391 args = self.data[self.args.warehouse].previous_sle \
392 or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse})
Deepesh Gargb4be2922021-01-28 13:09:56 +0530393
Nabin Haita77b8c92020-12-21 14:45:50 +0530394 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530395
Nabin Haita77b8c92020-12-21 14:45:50 +0530396 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
397 dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no,
398 excluded_sle=sle.name)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530399
Nabin Haita77b8c92020-12-21 14:45:50 +0530400 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530401 return entries_to_fix
Nabin Haita77b8c92020-12-21 14:45:50 +0530402 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
Nabin Hait243d59b2021-02-02 16:55:13 +0530403 return entries_to_fix
404 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530405 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530406 return entries_to_fix
407 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
408 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530409 else:
410 return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
411
412 def update_distinct_item_warehouses(self, dependant_sle):
413 key = (dependant_sle.item_code, dependant_sle.warehouse)
414 val = frappe._dict({
415 "sle": dependant_sle
416 })
417 if key not in self.distinct_item_warehouses:
418 self.distinct_item_warehouses[key] = val
419 self.new_items_found = True
420 else:
421 existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
422 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
423 val.sle_changed = True
424 self.distinct_item_warehouses[key] = val
425 self.new_items_found = True
426
427 def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
Nabin Haita77b8c92020-12-21 14:45:50 +0530428 self.initialize_previous_data(dependant_sle)
429
430 args = self.data[dependant_sle.warehouse].previous_sle \
431 or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse})
432 future_sle_for_dependant = list(self.get_sle_after_datetime(args))
433
434 entries_to_fix.extend(future_sle_for_dependant)
Nabin Hait243d59b2021-02-02 16:55:13 +0530435 return sorted(entries_to_fix, key=lambda k: k['timestamp'])
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530436
437 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530438 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
439
Nabin Haita77b8c92020-12-21 14:45:50 +0530440 # previous sle data for this warehouse
441 self.wh_data = self.data[sle.warehouse]
442
Anand Doshi0dc79f42015-04-06 12:59:34 +0530443 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 +0530444 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530445 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530446 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530447 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530448 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530449
Nabin Haita77b8c92020-12-21 14:45:50 +0530450 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530451 if not self.args.get("sle_id"):
452 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530453
Ankush Menat66bf21f2022-01-16 20:45:59 +0530454 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530455 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530456 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530457 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530458 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530459
Nabin Haita77b8c92020-12-21 14:45:50 +0530460 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
Ankush Menatce0514c2022-02-15 11:41:41 +0530461 elif sle.batch_no and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True):
462 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530463 else:
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +0530464 if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530465 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530466 self.wh_data.valuation_rate = sle.valuation_rate
467 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Nabin Haita77b8c92020-12-21 14:45:50 +0530468 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
Ankush Menatb0cf6192022-01-16 13:02:23 +0530469 if self.valuation_method != "Moving Average":
470 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530471 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530472 if self.valuation_method == "Moving Average":
473 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530474 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
475 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
Nabin Haitb96c0142014-10-07 11:25:04 +0530476 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530477 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530478
Rushabh Mehta54047782013-12-26 11:07:46 +0530479 # rounding as per precision
Nabin Haita77b8c92020-12-21 14:45:50 +0530480 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530481 if not self.wh_data.qty_after_transaction:
482 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530483 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
484 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530485
Nabin Hait902e8602013-01-08 18:29:24 +0530486 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530487 sle.qty_after_transaction = self.wh_data.qty_after_transaction
488 sle.valuation_rate = self.wh_data.valuation_rate
489 sle.stock_value = self.wh_data.stock_value
490 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530491 sle.stock_value_difference = stock_value_difference
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530492 sle.doctype="Stock Ledger Entry"
493 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530494
Ankush Menat701878f2022-03-01 18:08:29 +0530495 if not self.args.get("sle_id"):
496 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530497
Ankush Menatce0514c2022-02-15 11:41:41 +0530498
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530499 def validate_negative_stock(self, sle):
500 """
501 validate negative stock for entries current datetime onwards
502 will not consider cancelled entries
503 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530504 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530505
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530506 if diff < 0 and abs(diff) > 0.0001:
507 # negative stock!
508 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530509 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530510 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530511 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530512 return True
513
Nabin Haita77b8c92020-12-21 14:45:50 +0530514 def get_dynamic_incoming_outgoing_rate(self, sle):
515 # Get updated incoming/outgoing rate from transaction
516 if sle.recalculate_rate:
517 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
518
519 if flt(sle.actual_qty) >= 0:
520 sle.incoming_rate = rate
521 else:
522 sle.outgoing_rate = rate
523
524 def get_incoming_outgoing_rate_from_transaction(self, sle):
525 rate = 0
526 # Material Transfer, Repack, Manufacturing
527 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530528 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530529 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
530 # Sales and Purchase Return
531 elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"):
532 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530533 from erpnext.controllers.sales_and_purchase_return import (
534 get_rate_for_return, # don't move this import to top
535 )
rohitwaghchaurece6c3b52021-04-13 20:55:52 +0530536 rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code,
537 voucher_detail_no=sle.voucher_detail_no, sle = sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530538 else:
539 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530540 rate_field = "valuation_rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530541 else:
542 rate_field = "incoming_rate"
543
544 # check in item table
545 item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item",
546 sle.voucher_detail_no, ["item_code", rate_field])
547
548 if item_code == sle.item_code:
549 rate = incoming_rate
550 else:
551 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
552 ref_doctype = "Packed Item"
553 else:
554 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530555
Nabin Haita77b8c92020-12-21 14:45:50 +0530556 rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no,
557 "item_code": sle.item_code}, rate_field)
558
559 return rate
560
561 def update_outgoing_rate_on_transaction(self, sle):
562 """
563 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
564 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
565 """
566 if sle.actual_qty and sle.voucher_detail_no:
567 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
568
569 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
570 self.update_rate_on_stock_entry(sle, outgoing_rate)
571 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
572 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
573 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
574 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
575
576 def update_rate_on_stock_entry(self, sle, outgoing_rate):
577 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
578
Ankush Menat701878f2022-03-01 18:08:29 +0530579 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
580 if not sle.dependant_sle_voucher_detail_no:
581 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530582
583 def recalculate_amounts_in_stock_entry(self, voucher_no):
584 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530585 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
586 stock_entry.db_update()
587 for d in stock_entry.items:
588 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530589
Nabin Haita77b8c92020-12-21 14:45:50 +0530590 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
591 # Update item's incoming rate on transaction
592 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
593 if item_code == sle.item_code:
594 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate)
595 else:
596 # packed item
597 frappe.db.set_value("Packed Item",
598 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
599 "incoming_rate", outgoing_rate)
600
601 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
602 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
603 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate)
604 else:
605 frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
606
607 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530608 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes':
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530609 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530610 doc.update_valuation_rate(reset_outgoing_rate=False)
611 for d in (doc.items + doc.supplied_items):
612 d.db_update()
613
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530614 def get_serialized_values(self, sle):
615 incoming_rate = flt(sle.incoming_rate)
616 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530617 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530618
619 if incoming_rate < 0:
620 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530621 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530622
Nabin Hait2620bf42016-02-29 11:30:27 +0530623 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530624 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530625 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530626 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530627 # In case of delivery/stock issue, get average purchase rate
628 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530629 if not sle.is_cancelled:
630 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
631 stock_value_change = -1 * outgoing_value
632 else:
633 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530634
Nabin Haita77b8c92020-12-21 14:45:50 +0530635 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530636
Nabin Hait2620bf42016-02-29 11:30:27 +0530637 if new_stock_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530638 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530639 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530640 # calculate new valuation rate only if stock value is positive
641 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530642 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530643
Nabin Haita77b8c92020-12-21 14:45:50 +0530644 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530645 allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
646 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530647 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530648
Nabin Hait328c4f92020-01-02 19:00:32 +0530649 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
650 # get rate from serial nos within same company
651 all_serial_nos = frappe.get_all("Serial No",
652 fields=["purchase_rate", "name", "company"],
653 filters = {'name': ('in', serial_nos)})
654
Ankush Menat98917802021-06-11 18:40:22 +0530655 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 +0530656
657 # Get rate for serial nos which has been transferred to other company
658 invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company]
659 for serial_no in invalid_serial_nos:
660 incoming_rate = frappe.db.sql("""
661 select incoming_rate
662 from `tabStock Ledger Entry`
663 where
664 company = %s
665 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530666 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530667 and (serial_no = %s
668 or serial_no like %s
669 or serial_no like %s
670 or serial_no like %s
671 )
672 order by posting_date desc
673 limit 1
674 """, (sle.company, serial_no, serial_no+'\n%', '%\n'+serial_no, '%\n'+serial_no+'\n%'))
675
676 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
677
678 return incoming_values
679
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530680 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530681 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530682 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530683 if new_stock_qty >= 0:
684 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530685 if flt(self.wh_data.qty_after_transaction) <= 0:
686 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530687 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530688 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530689 (actual_qty * sle.incoming_rate)
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530690
Nabin Haita77b8c92020-12-21 14:45:50 +0530691 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530692
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530693 elif sle.outgoing_rate:
694 if new_stock_qty:
Nabin Haita77b8c92020-12-21 14:45:50 +0530695 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530696 (actual_qty * sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530697
Nabin Haita77b8c92020-12-21 14:45:50 +0530698 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530699 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530700 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530701 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530702 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
703 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530704
Nabin Haita77b8c92020-12-21 14:45:50 +0530705 if not self.wh_data.valuation_rate and actual_qty > 0:
706 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530707
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530708 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800709 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530710 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Javier Wong9b11d9b2017-04-14 18:24:04 +0800711 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
712 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530713 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530714
Ankush Menatf089d392022-02-02 12:51:21 +0530715 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530716 incoming_rate = flt(sle.incoming_rate)
717 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530718 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530719
Ankush Menatb534fee2022-02-19 20:58:36 +0530720 self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
721
Ankush Menat97e18a12022-01-15 17:42:25 +0530722 if self.valuation_method == "LIFO":
723 stock_queue = LIFOValuation(self.wh_data.stock_queue)
724 else:
725 stock_queue = FIFOValuation(self.wh_data.stock_queue)
726
Ankush Menatb534fee2022-02-19 20:58:36 +0530727 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
728
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530729 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530730 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530731 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530732 def rate_generator() -> float:
733 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
734 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530735 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530736 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530737 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530738
Ankush Menat97e18a12022-01-15 17:42:25 +0530739 stock_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530740
Ankush Menatb534fee2022-02-19 20:58:36 +0530741 _qty, stock_value = stock_queue.get_total_stock_and_value()
742
743 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530744
Ankush Menat97e18a12022-01-15 17:42:25 +0530745 self.wh_data.stock_queue = stock_queue.state
Ankush Menatb534fee2022-02-19 20:58:36 +0530746 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530747
Nabin Haita77b8c92020-12-21 14:45:50 +0530748 if not self.wh_data.stock_queue:
749 self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530750
Ankush Menatb534fee2022-02-19 20:58:36 +0530751 if self.wh_data.qty_after_transaction:
752 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
753
Ankush Menatce0514c2022-02-15 11:41:41 +0530754 def update_batched_values(self, sle):
755 incoming_rate = flt(sle.incoming_rate)
756 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530757
Ankush Menat35483242022-02-19 22:22:27 +0530758 self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
Ankush Menatce0514c2022-02-15 11:41:41 +0530759
760 if actual_qty > 0:
761 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530762 else:
Ankush Menat35483242022-02-19 22:22:27 +0530763 outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code,
764 warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date,
765 posting_time=sle.posting_time, creation=sle.creation)
Ankush Menataba7a7c2022-02-19 19:36:28 +0530766 if outgoing_rate is None:
767 # This can *only* happen if qty available for the batch is zero.
768 # in such case fall back various other rates.
769 # future entries will correct the overall accounting as each
770 # batch individually uses moving average rates.
771 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530772 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530773
Ankush Menat35483242022-02-19 22:22:27 +0530774 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
Ankush Menatce0514c2022-02-15 11:41:41 +0530775 if self.wh_data.qty_after_transaction:
776 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530777
Javier Wong9b11d9b2017-04-14 18:24:04 +0800778 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530779 ref_item_dt = ""
780
781 if voucher_type == "Stock Entry":
782 ref_item_dt = voucher_type + " Detail"
783 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
784 ref_item_dt = voucher_type + " Item"
785
786 if ref_item_dt:
787 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
788 else:
789 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530790
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530791 def get_fallback_rate(self, sle) -> float:
792 """When exact incoming rate isn't available use any of other "average" rates as fallback.
793 This should only get used for negative stock."""
794 return get_valuation_rate(sle.item_code, sle.warehouse,
795 sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
796 currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
797
Nabin Haita77b8c92020-12-21 14:45:50 +0530798 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530799 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530800 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
801 sle = sle[0] if sle else frappe._dict()
802 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530803
Nabin Haita77b8c92020-12-21 14:45:50 +0530804 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530805 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530806 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530807
808 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530809 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530810 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530811 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530812
Nabin Haita77b8c92020-12-21 14:45:50 +0530813 if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in
814 frappe.local.flags.currently_saving):
Nabin Hait3edefb12016-07-20 16:13:18 +0530815
Nabin Haita77b8c92020-12-21 14:45:50 +0530816 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Nabin Hait243d59b2021-02-02 16:55:13 +0530817 abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
Nabin Haita77b8c92020-12-21 14:45:50 +0530818 frappe.get_desk_link('Warehouse', warehouse))
819 else:
820 msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
Nabin Hait243d59b2021-02-02 16:55:13 +0530821 abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
Nabin Haita77b8c92020-12-21 14:45:50 +0530822 frappe.get_desk_link('Warehouse', warehouse),
823 exceptions[0]["posting_date"], exceptions[0]["posting_time"],
824 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530825
Nabin Haita77b8c92020-12-21 14:45:50 +0530826 if msg:
827 msg_list.append(msg)
828
829 if msg_list:
830 message = "\n\n".join(msg_list)
831 if self.verbose:
mergify[bot]7a3d3012022-03-08 10:42:30 +0530832 frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
Nabin Haita77b8c92020-12-21 14:45:50 +0530833 else:
834 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530835
Nabin Haita77b8c92020-12-21 14:45:50 +0530836 def update_bin(self):
837 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530838 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +0530839 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +0530840
Ankush Menat97060c42021-12-03 11:50:38 +0530841 frappe.db.set_value('Bin', bin_name, {
Nabin Haita77b8c92020-12-21 14:45:50 +0530842 "valuation_rate": data.valuation_rate,
843 "actual_qty": data.qty_after_transaction,
844 "stock_value": data.stock_value
845 })
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530846
marination8418c4b2021-06-22 21:35:25 +0530847
848def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
849 """get stock ledger entries filtered by specific posting datetime conditions"""
850
851 args['time_format'] = '%H:%i:%s'
852 if not args.get("posting_date"):
853 args["posting_date"] = "1900-01-01"
854 if not args.get("posting_time"):
855 args["posting_time"] = "00:00"
856
857 voucher_condition = ""
858 if exclude_current_voucher:
859 voucher_no = args.get("voucher_no")
860 voucher_condition = f"and voucher_no != '{voucher_no}'"
861
862 sle = frappe.db.sql("""
863 select *, timestamp(posting_date, posting_time) as "timestamp"
864 from `tabStock Ledger Entry`
865 where item_code = %(item_code)s
866 and warehouse = %(warehouse)s
867 and is_cancelled = 0
868 {voucher_condition}
869 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
870 order by timestamp(posting_date, posting_time) desc, creation desc
871 limit 1
872 for update""".format(voucher_condition=voucher_condition), args, as_dict=1)
873
874 return sle[0] if sle else frappe._dict()
875
Anand Doshi4dc7caa2013-01-11 11:44:49 +0530876def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +0530877 """
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530878 get the last sle on or before the current time-bucket,
Anand Doshi1b531862013-01-10 19:29:51 +0530879 to get actual qty before transaction, this function
880 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530881
Anand Doshi1b531862013-01-10 19:29:51 +0530882 args = {
883 "item_code": "ABC",
884 "warehouse": "XYZ",
885 "posting_date": "2012-12-12",
886 "posting_time": "12:00",
887 "sle": "name of reference Stock Ledger Entry"
888 }
889 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530890 args["name"] = args.get("sle", None) or ""
891 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +0530892 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +0530893
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +0530894def get_stock_ledger_entries(previous_sle, operator=None,
895 order="desc", limit=None, for_update=False, debug=False, check_serial_no=True):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530896 """get stock ledger entries filtered by specific posting datetime conditions"""
Nabin Haitb9ce1042018-02-01 14:58:50 +0530897 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator)
898 if previous_sle.get("warehouse"):
899 conditions += " and warehouse = %(warehouse)s"
900 elif previous_sle.get("warehouse_condition"):
901 conditions += " and " + previous_sle.get("warehouse_condition")
902
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +0530903 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530904 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
905 serial_no = previous_sle.get("serial_no")
906 conditions += (""" and
907 (
908 serial_no = {0}
909 or serial_no like {1}
910 or serial_no like {2}
911 or serial_no like {3}
912 )
913 """).format(frappe.db.escape(serial_no), frappe.db.escape('{}\n%'.format(serial_no)),
914 frappe.db.escape('%\n{}'.format(serial_no)), frappe.db.escape('%\n{}\n%'.format(serial_no)))
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530915
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530916 if not previous_sle.get("posting_date"):
917 previous_sle["posting_date"] = "1900-01-01"
918 if not previous_sle.get("posting_time"):
919 previous_sle["posting_time"] = "00:00"
920
921 if operator in (">", "<=") and previous_sle.get("name"):
922 conditions += " and name!=%(name)s"
923
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530924 return frappe.db.sql("""
925 select *, timestamp(posting_date, posting_time) as "timestamp"
926 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530927 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +0530928 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +0530929 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +0530930 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530931 %(limit)s %(for_update)s""" % {
932 "conditions": conditions,
933 "limit": limit or "",
934 "for_update": for_update and "for update" or "",
935 "order": order
Rushabh Mehta50dc4e92015-02-19 20:05:45 +0530936 }, previous_sle, as_dict=1, debug=debug)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530937
Nabin Haita77b8c92020-12-21 14:45:50 +0530938def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
939 return frappe.db.get_value('Stock Ledger Entry',
940 {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]},
941 ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
942 as_dict=1)
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530943
Ankush Menatab926522022-02-19 15:37:03 +0530944def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None):
Ankush Menatce0514c2022-02-15 11:41:41 +0530945
Ankush Menat102fff22022-02-19 15:51:04 +0530946 Timestamp = CustomFunction('timestamp', ['date', 'time'])
947
948 sle = frappe.qb.DocType("Stock Ledger Entry")
949
950 timestamp_condition = (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(posting_date, posting_time))
951 if creation:
952 timestamp_condition |= (
953 (Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time))
954 & (sle.creation < creation)
Ankush Menatce0514c2022-02-15 11:41:41 +0530955 )
Ankush Menat102fff22022-02-19 15:51:04 +0530956
957 batch_details = (
958 frappe.qb
959 .from_(sle)
960 .select(
961 Sum(sle.stock_value_difference).as_("batch_value"),
962 Sum(sle.actual_qty).as_("batch_qty")
963 )
964 .where(
965 (sle.item_code == item_code)
966 & (sle.warehouse == warehouse)
967 & (sle.batch_no == batch_no)
968 & (sle.is_cancelled == 0)
969 )
970 .where(timestamp_condition)
971 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +0530972
973 if batch_details and batch_details[0].batch_qty:
974 return batch_details[0].batch_value / batch_details[0].batch_qty
975
976
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530977def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
Ankush Menat342d09a2022-02-19 14:28:51 +0530978 allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True, batch_no=None):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +0530979
Ankush Menatf7ffe042021-11-01 13:21:14 +0530980 if not company:
981 company = frappe.get_cached_value("Warehouse", warehouse, "company")
982
Ankush Menat342d09a2022-02-19 14:28:51 +0530983 last_valuation_rate = None
984
985 # Get moving average rate of a specific batch number
986 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
987 last_valuation_rate = frappe.db.sql("""
988 select sum(stock_value_difference) / sum(actual_qty)
989 from `tabStock Ledger Entry`
990 where
991 item_code = %s
992 AND warehouse = %s
993 AND batch_no = %s
994 AND is_cancelled = 0
995 AND NOT (voucher_no = %s AND voucher_type = %s)
996 """,
997 (item_code, warehouse, batch_no, voucher_no, voucher_type))
998
Ankush Menatf7ffe042021-11-01 13:21:14 +0530999 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301000 if not last_valuation_rate or last_valuation_rate[0][0] is None:
1001 last_valuation_rate = frappe.db.sql("""select valuation_rate
1002 from `tabStock Ledger Entry` force index (item_warehouse)
1003 where
1004 item_code = %s
1005 AND warehouse = %s
1006 AND valuation_rate >= 0
1007 AND is_cancelled = 0
1008 AND NOT (voucher_no = %s AND voucher_type = %s)
1009 order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301010
1011 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301012 # Get valuation rate from last sle for the item against any warehouse
Nabin Haitfb6e4342014-10-15 11:34:40 +05301013 last_valuation_rate = frappe.db.sql("""select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301014 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301015 where
1016 item_code = %s
1017 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301018 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301019 AND NOT(voucher_no = %s AND voucher_type = %s)
1020 order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301021
Nabin Haita645f362018-03-01 10:31:24 +05301022 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301023 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301024
1025 # If negative stock allowed, and item delivered without any incoming entry,
1026 # system does not found any SLE, then take valuation rate from Item
1027 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301028
1029 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301030 # try Item Standard rate
1031 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301032
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301033 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301034 # try in price list
1035 valuation_rate = frappe.db.get_value('Item Price',
1036 dict(item_code=item_code, buying=1, currency=currency),
1037 'price_list_rate')
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301038
Nabin Hait7ba092e2018-02-01 10:51:27 +05301039 if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
Rohit Waghchauree9ff1912017-06-19 12:54:59 +05301040 and cint(erpnext.is_perpetual_inventory_enabled(company)):
Neil Trini Lasrado193c8912017-03-28 17:39:34 +05301041 frappe.local.message_log = []
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301042 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301043
1044 message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301045 message += "<br><br>" + _("Here are the options to proceed:")
Marica97715f22020-05-11 20:45:37 +05301046 solutions = "<li>" + _("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.").format(voucher_type) + "</li>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301047 solutions += "<li>" + _("If not, you can Cancel / Submit this entry") + " {0} ".format(frappe.bold("after")) + _("performing either one below:") + "</li>"
Marica97715f22020-05-11 20:45:37 +05301048 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1049 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1050 msg = message + solutions + sub_solutions + "</li>"
1051
1052 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301053
1054 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301055
Ankush Menate7109c12021-08-26 16:40:45 +05301056def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301057 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301058 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301059 qty_shift = args.actual_qty
1060
1061 # find difference/shift in qty caused by stock reconciliation
1062 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301063 qty_shift = get_stock_reco_qty_shift(args)
1064
1065 # find the next nearest stock reco so that we only recalculate SLEs till that point
1066 next_stock_reco_detail = get_next_stock_reco(args)
1067 if next_stock_reco_detail:
1068 detail = next_stock_reco_detail[0]
1069 # add condition to update SLEs before this date & time
1070 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301071
Nabin Hait186a0452021-02-18 14:14:21 +05301072 frappe.db.sql("""
1073 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301074 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301075 where
1076 item_code = %(item_code)s
1077 and warehouse = %(warehouse)s
1078 and voucher_no != %(voucher_no)s
1079 and is_cancelled = 0
1080 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1081 or (
1082 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1083 and creation > %(creation)s
1084 )
1085 )
marination40389772021-07-02 17:13:45 +05301086 {datetime_limit_condition}
1087 """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args)
Nabin Hait186a0452021-02-18 14:14:21 +05301088
1089 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1090
marination40389772021-07-02 17:13:45 +05301091def get_stock_reco_qty_shift(args):
1092 stock_reco_qty_shift = 0
1093 if args.get("is_cancelled"):
1094 if args.get("previous_qty_after_transaction"):
1095 # get qty (balance) that was set at submission
1096 last_balance = args.get("previous_qty_after_transaction")
1097 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1098 else:
1099 stock_reco_qty_shift = flt(args.actual_qty)
1100 else:
1101 # reco is being submitted
1102 last_balance = get_previous_sle_of_current_voucher(args,
1103 exclude_current_voucher=True).get("qty_after_transaction")
1104
1105 if last_balance is not None:
1106 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1107 else:
1108 stock_reco_qty_shift = args.qty_after_transaction
1109
1110 return stock_reco_qty_shift
1111
1112def get_next_stock_reco(args):
1113 """Returns next nearest stock reconciliaton's details."""
1114
1115 return frappe.db.sql("""
1116 select
1117 name, posting_date, posting_time, creation, voucher_no
1118 from
marination8c441262021-07-02 17:46:05 +05301119 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301120 where
1121 item_code = %(item_code)s
1122 and warehouse = %(warehouse)s
1123 and voucher_type = 'Stock Reconciliation'
1124 and voucher_no != %(voucher_no)s
1125 and is_cancelled = 0
1126 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1127 or (
1128 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1129 and creation > %(creation)s
1130 )
1131 )
1132 limit 1
1133 """, args, as_dict=1)
1134
1135def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301136 return f"""
1137 and
1138 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1139 or (
1140 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1141 and creation < '{detail.creation}'
1142 )
1143 )"""
1144
Ankush Menate7109c12021-08-26 16:40:45 +05301145def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301146 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301147 return
1148 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1149 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301150
Ankush Menat5eba5752021-12-07 23:03:52 +05301151 neg_sle = get_future_sle_with_negative_qty(args)
1152 if neg_sle:
1153 message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
1154 abs(neg_sle[0]["qty_after_transaction"]),
1155 frappe.get_desk_link('Item', args.item_code),
1156 frappe.get_desk_link('Warehouse', args.warehouse),
1157 neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
1158 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
1159
mergify[bot]7a3d3012022-03-08 10:42:30 +05301160 frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
Ankush Menat5eba5752021-12-07 23:03:52 +05301161
1162
1163 if not args.batch_no:
1164 return
1165
1166 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1167 if neg_batch_sle:
1168 message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
1169 abs(neg_batch_sle[0]["cumulative_total"]),
1170 frappe.get_desk_link('Batch', args.batch_no),
1171 frappe.get_desk_link('Warehouse', args.warehouse),
1172 neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
1173 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
mergify[bot]7a3d3012022-03-08 10:42:30 +05301174 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301175
Nabin Haita77b8c92020-12-21 14:45:50 +05301176
1177def get_future_sle_with_negative_qty(args):
1178 return frappe.db.sql("""
1179 select
1180 qty_after_transaction, posting_date, posting_time,
1181 voucher_type, voucher_no
1182 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301183 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301184 item_code = %(item_code)s
1185 and warehouse = %(warehouse)s
1186 and voucher_no != %(voucher_no)s
1187 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1188 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301189 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301190 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301191 limit 1
Sagar Vorae50324a2021-03-31 12:44:03 +05301192 """, args, as_dict=1)
Ankush Menat6a014d12021-04-12 20:21:27 +05301193
Ankush Menat5eba5752021-12-07 23:03:52 +05301194
1195def get_future_sle_with_negative_batch_qty(args):
1196 return frappe.db.sql("""
1197 with batch_ledger as (
1198 select
1199 posting_date, posting_time, voucher_type, voucher_no,
1200 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1201 from `tabStock Ledger Entry`
1202 where
1203 item_code = %(item_code)s
1204 and warehouse = %(warehouse)s
1205 and batch_no=%(batch_no)s
1206 and is_cancelled = 0
1207 order by posting_date, posting_time, creation
1208 )
1209 select * from batch_ledger
1210 where
1211 cumulative_total < 0.0
1212 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1213 limit 1
1214 """, args, as_dict=1)
Ankush Menateb8b4242022-02-12 13:08:28 +05301215
1216
1217def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1218 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1219 return True
1220 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1221 return True
1222 return False