feat: Repost item costing (#24183)
* Repost item valuation (#24031)
* feat: Reposting logic for future finished/transferred item
* feat: added fields to identify needs to recalculate rate while reposting
* refactor: Set rate for outgoing and finished items
* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item
* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item
* refactor: Get outgoing rate for purchase return
* refactor: Get incoming rate for sales return
* test: Added tests for reposting valuation of transferred/finished/returned items
* feat: added incoming rate field in DN, SI and Packed Item table
* feat: get incoming rate for returned item
* fix: no error while getting valuation rate in stock entry
* fix: update stock ledger for DN and SI
* feat: update item valuation rate in PR and PI based on supplied items cost
* feat: SLE reposting logic for sales return and subcontracted item with test cases
* feat: update qty in future sle
* feat: repost future sle and gle via Repost Item Valuation
* fix: Skip unwanted function calling while reposting
* fix: repost sle for specific item and warehouse
* test: Modified tests for backdated stock reco
* fix: ignore cancelled sle in few methods
* feat: role allowed to do backdated entry
* feat: Show reposting status on stock valuation related reports
* fix: minor fixes
* fix: fixed sider issues
* fix: serial no fix related to immutable ledger
* fix: Test cases fixes related to perpetual inventory
* fix: Test cases fixed
* fix: Fixed reposting on cancel and test cases
* feat: Restart reposting item valuation
* refactor: Code cleanup using small functions and test case fixes
* fix: minor fixes
* fix: Raise on error while reposting item valuation
* fix: minor fix
* fix: Tests fixed
* fix: skip some validation ig gle made from reposting
* fix: test fixes
* fix: debugging stock and account validation
* fix: debugging stock and account validation
* fix: debugging travis for stock and account sync validation
* fix: debugging travis
* fix: debugging travis
* fix: debugging travis
* fix: removed duplicate field from pos profile
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 286c4f4..dc61870 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -16,6 +16,8 @@
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
+from erpnext.stock.utils import get_incoming_rate
class BuyingController(StockController):
def __setup__(self):
@@ -63,7 +65,7 @@
self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
- self.update_valuation_rate("items")
+ self.update_valuation_rate()
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@@ -177,7 +179,7 @@
self.in_words = money_in_words(amount, self.currency)
# update valuation rate
- def update_valuation_rate(self, parentfield):
+ def update_valuation_rate(self, reset_outgoing_rate=True):
"""
item_tax_amount is the total tax amount applied on that item
stored for valuation
@@ -188,7 +190,7 @@
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
- for d in self.get(parentfield):
+ for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items:
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
@@ -198,7 +200,7 @@
if d.category in ["Valuation", "Valuation and Total"]])
valuation_amount_adjustment = total_valuation_amount
- for i, item in enumerate(self.get(parentfield)):
+ for i, item in enumerate(self.get("items")):
if item.item_code and item.qty and item.item_code in stock_and_asset_items:
item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
else flt(item.qty) / stock_and_asset_items_qty
@@ -216,16 +218,34 @@
item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
- rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
- landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
- item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + rm_supp_cost
- + landed_cost_voucher_amount) / qty_in_stock_uom)
+ item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
+ item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost
+ + flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom)
else:
item.valuation_rate = 0.0
+ def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
+ supplied_items_cost = 0.0
+ for d in self.get("supplied_items"):
+ if d.reference_name == item_row_id:
+ if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
+ rate = get_incoming_rate({
+ "item_code": d.rm_item_code,
+ "warehouse": self.supplier_warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": -1 * d.consumed_qty,
+ "serial_no": d.serial_no
+ })
+
+ if rate > 0:
+ d.rate = rate
+
+ d.amount = flt(d.consumed_qty) * flt(d.rate)
+ supplied_items_cost += flt(d.amount)
+
+ return supplied_items_cost
+
def validate_for_subcontracting(self):
if not self.is_subcontracted and self.sub_contracted_items:
frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
@@ -352,35 +372,17 @@
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
- def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty):
+ def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
if not rm.main_item_code:
- rm.main_item_code = fg_item_doc.item_code
+ rm.main_item_code = fg_item_row.item_code
- rm.reference_name = fg_item_doc.name
+ rm.reference_name = fg_item_row.name
rm.required_qty = qty
rm.consumed_qty = qty
- if not raw_material_data.get('non_stock_item'):
- from erpnext.stock.utils import get_incoming_rate
- rm.rate = get_incoming_rate({
- "item_code": raw_material_data.rm_item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * qty,
- "serial_no": rm.serial_no
- })
-
- if not rm.rate:
- rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
- self.doctype, self.name, currency=self.company_currency, company=self.company)
-
- rm.amount = qty * flt(rm.rate)
- fg_item_doc.rm_supp_cost += rm.amount
-
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
@@ -389,7 +391,7 @@
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = []
- if self.doctype == 'Purchase Receipt' and item.purchase_order:
+ if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
raw_materials_cost = 0
@@ -406,7 +408,7 @@
reserve_warehouse = None
conversion_factor = item.conversion_factor
- if (self.doctype == 'Purchase Receipt' and item.purchase_order and
+ if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
bom_item.item_code in used_alternative_items):
alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code
@@ -434,9 +436,7 @@
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
- if self.doctype == "Purchase Order" and not rm.reserve_warehouse:
- rm.reserve_warehouse = reserve_warehouse
-
+ rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@@ -444,29 +444,8 @@
rm.description = bom_item.description
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
rm.batch_no = item.batch_no
-
- # get raw materials rate
- if self.doctype == "Purchase Receipt":
- from erpnext.stock.utils import get_incoming_rate
- rm.rate = get_incoming_rate({
- "item_code": bom_item.item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * required_qty,
- "serial_no": rm.serial_no
- })
- if not rm.rate:
- rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse,
- self.doctype, self.name, currency=self.company_currency, company = self.company)
- else:
- rm.rate = bom_item.rate
-
- rm.amount = required_qty * flt(rm.rate)
- raw_materials_cost += flt(rm.amount)
-
- if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
- item.rm_supp_cost = raw_materials_cost
+ elif not rm.reserve_warehouse:
+ rm.reserve_warehouse = reserve_warehouse
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
"""Remove all those child items which are no longer present in main item table"""
@@ -579,7 +558,8 @@
or (cint(self.is_return) and self.docstatus==2)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
- "warehouse": d.from_warehouse
+ "warehouse": d.from_warehouse,
+ "dependant_sle_voucher_detail_no": d.name
})
sl_entries.append(from_warehouse_sle)
@@ -589,28 +569,20 @@
"serial_no": cstr(d.serial_no).strip()
})
if self.is_return:
- filters = {
- "voucher_type": self.doctype,
- "voucher_no": self.return_against,
- "item_code": d.item_code
- }
-
- if (self.doctype == "Purchase Invoice" and self.update_stock
- and d.get("purchase_invoice_item")):
- filters["voucher_detail_no"] = d.purchase_invoice_item
- elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
- filters["voucher_detail_no"] = d.purchase_receipt_item
-
- original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
+ outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
sle.update({
- "outgoing_rate": original_incoming_rate
+ "outgoing_rate": outgoing_rate,
+ "recalculate_rate": 1
})
+ if d.from_warehouse:
+ sle.dependant_sle_voucher_detail_no = d.name
else:
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
sle.update({
- "incoming_rate": incoming_rate
+ "incoming_rate": incoming_rate,
+ "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0
})
sl_entries.append(sle)
@@ -618,7 +590,8 @@
or (cint(self.is_return) and self.docstatus==1)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
- "warehouse": d.from_warehouse
+ "warehouse": d.from_warehouse,
+ "recalculate_rate": 1
})
sl_entries.append(from_warehouse_sle)
@@ -666,6 +639,7 @@
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1*flt(d.consumed_qty),
+ "dependant_sle_voucher_detail_no": d.reference_name
}))
def on_submit(self):
@@ -857,6 +831,7 @@
else:
validate_item_type(self, "is_purchase_item", "purchase")
+
def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"