diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 5eb6016dbb2..eb10decd702 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -2,11 +2,9 @@ # License: GNU General Public License v3. See license.txt import json -from collections import defaultdict import frappe from frappe import _, bold -from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext @@ -287,89 +285,23 @@ class StockController(AccountsController): return StockLedgerService(self).get_sl_entries(d, args) def set_landed_cost_voucher_amount(self): - for d in self.get("items"): - lcv_item = frappe.qb.DocType("Landed Cost Item") - query = ( - frappe.qb.from_(lcv_item) - .select(Sum(lcv_item.applicable_charges), lcv_item.cost_center) - .where((lcv_item.docstatus == 1) & (lcv_item.receipt_document == self.name)) - ) - - if self.doctype == "Stock Entry": - query = query.where(lcv_item.stock_entry_item == d.name) - else: - query = query.where(lcv_item.purchase_receipt_item == d.name) - - lc_voucher_data = query.run(as_list=True) - - d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0 - if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: - d.db_set("cost_center", lc_voucher_data[0][1]) - - def has_landed_cost_amount(self): - for row in self.items: - if row.get("landed_cost_voucher_amount"): - return True - - return False - - def get_item_account_wise_lcv_entries(self): - if not self.has_landed_cost_amount(): - return - - landed_cost_vouchers = frappe.get_all( - "Landed Cost Purchase Receipt", - fields=["parent"], - filters={"receipt_document": self.name, "docstatus": 1}, + from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( + set_landed_cost_voucher_amount, ) - if not landed_cost_vouchers: - return + return set_landed_cost_voucher_amount(self) - item_account_wise_cost = {} + def has_landed_cost_amount(self): + from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import has_landed_cost_amount - row_fieldname = "purchase_receipt_item" - if self.doctype == "Stock Entry": - row_fieldname = "stock_entry_item" + return has_landed_cost_amount(self) - for lcv in landed_cost_vouchers: - landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) + def get_item_account_wise_lcv_entries(self): + from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( + get_item_account_wise_lcv_entries, + ) - based_on_field = "applicable_charges" - # Use amount field for total item cost for manually cost distributed LCVs - if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually": - based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) - - total_item_cost = 0 - - if based_on_field: - for item in landed_cost_voucher_doc.items: - total_item_cost += item.get(based_on_field) - - for item in landed_cost_voucher_doc.items: - if item.receipt_document == self.name: - for account in landed_cost_voucher_doc.taxes: - exchange_rate = account.exchange_rate or 1 - item_account_wise_cost.setdefault((item.item_code, item.get(row_fieldname)), {}) - item_account_wise_cost[(item.item_code, item.get(row_fieldname))].setdefault( - account.expense_account, {"amount": 0.0, "base_amount": 0.0} - ) - - item_row = item_account_wise_cost[(item.item_code, item.get(row_fieldname))][ - account.expense_account - ] - - if total_item_cost > 0: - item_row["amount"] += account.amount * item.get(based_on_field) / total_item_cost - - item_row["base_amount"] += ( - account.base_amount * item.get(based_on_field) / total_item_cost - ) - else: - item_row["amount"] += item.applicable_charges / exchange_rate - item_row["base_amount"] += item.applicable_charges - - return item_account_wise_cost + return get_item_account_wise_lcv_entries(self) def update_inventory_dimensions(self, row, sl_dict) -> None: from erpnext.stock.services.stock_ledger import StockLedgerService @@ -472,72 +404,9 @@ class StockController(AccountsController): return StockInternalTransferService(self).validate_internal_transfer() def validate_putaway_capacity(self): - # if over receipt is attempted while 'apply putaway rule' is disabled - # and if rule was applied on the transaction, validate it. - from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity + from erpnext.stock.doctype.putaway_rule.putaway_rule import validate_putaway_capacity - valid_doctype = self.doctype in ( - "Purchase Receipt", - "Stock Entry", - "Purchase Invoice", - "Stock Reconciliation", - ) - - if not frappe.get_all("Putaway Rule", limit=1): - return - - if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: - valid_doctype = False - - if valid_doctype: - rule_map = defaultdict(dict) - for item in self.get("items"): - warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse" - rule = frappe.db.get_value( - "Putaway Rule", - {"item_code": item.get("item_code"), "warehouse": item.get(warehouse_field)}, - ["stock_capacity", "name", "disable"], - as_dict=True, - ) - if rule: - if rule.get("disabled"): - continue # dont validate for disabled rule - - if self.doctype == "Stock Reconciliation": - stock_qty = flt(item.qty) - else: - stock_qty = ( - flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty) - ) - - rule_name = rule.get("name") - if not rule_map[rule_name]: - rule_map[rule_name]["warehouse"] = item.get(warehouse_field) - rule_map[rule_name]["item"] = item.get("item_code") - rule_map[rule_name]["qty_put"] = 0 - rule_map[rule_name]["capacity"] = ( - rule.stock_capacity - if self.doctype == "Stock Reconciliation" - else get_available_putaway_capacity(rule_name) - ) - rule_map[rule_name]["qty_put"] += flt(stock_qty) - - for rule, values in rule_map.items(): - if flt(values["qty_put"]) > flt(values["capacity"]): - message = self.prepare_over_receipt_message(rule, values) - frappe.throw(msg=message, title=_("Over Receipt")) - - def prepare_over_receipt_message(self, rule, values): - message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.").format( - frappe.bold(values["qty_put"]), - frappe.bold(values["item"]), - frappe.bold(values["warehouse"]), - frappe.bold(values["capacity"]), - ) - message += "

" - rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) - message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link) - return message + return validate_putaway_capacity(self) def repost_future_sle_and_gle(self, force=False, via_landed_cost_voucher=False): from erpnext.stock.services.stock_ledger import StockLedgerService diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 932ce242b01..dc5384f76ee 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -9,6 +9,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Sum from frappe.utils import cint, flt import erpnext @@ -523,3 +524,93 @@ def get_vendor_invoice_query(filters): query = query.where(doctype.name == filters.get("name")) return query + + +def set_landed_cost_voucher_amount(doc): + """Set landed_cost_voucher_amount on the receipt document's items from submitted LCVs.""" + for d in doc.get("items"): + lcv_item = frappe.qb.DocType("Landed Cost Item") + query = ( + frappe.qb.from_(lcv_item) + .select(Sum(lcv_item.applicable_charges), lcv_item.cost_center) + .where((lcv_item.docstatus == 1) & (lcv_item.receipt_document == doc.name)) + ) + + if doc.doctype == "Stock Entry": + query = query.where(lcv_item.stock_entry_item == d.name) + else: + query = query.where(lcv_item.purchase_receipt_item == d.name) + + lc_voucher_data = query.run(as_list=True) + + d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0 + if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: + d.db_set("cost_center", lc_voucher_data[0][1]) + + +def has_landed_cost_amount(doc): + for row in doc.items: + if row.get("landed_cost_voucher_amount"): + return True + + return False + + +def get_item_account_wise_lcv_entries(doc): + """Account-wise landed-cost map for a receipt document, consumed by the GL composers.""" + if not has_landed_cost_amount(doc): + return + + landed_cost_vouchers = frappe.get_all( + "Landed Cost Purchase Receipt", + fields=["parent"], + filters={"receipt_document": doc.name, "docstatus": 1}, + ) + + if not landed_cost_vouchers: + return + + item_account_wise_cost = {} + + row_fieldname = "purchase_receipt_item" + if doc.doctype == "Stock Entry": + row_fieldname = "stock_entry_item" + + for lcv in landed_cost_vouchers: + landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) + + based_on_field = "applicable_charges" + # Use amount field for total item cost for manually cost distributed LCVs + if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually": + based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) + + total_item_cost = 0 + + if based_on_field: + for item in landed_cost_voucher_doc.items: + total_item_cost += item.get(based_on_field) + + for item in landed_cost_voucher_doc.items: + if item.receipt_document == doc.name: + for account in landed_cost_voucher_doc.taxes: + exchange_rate = account.exchange_rate or 1 + item_account_wise_cost.setdefault((item.item_code, item.get(row_fieldname)), {}) + item_account_wise_cost[(item.item_code, item.get(row_fieldname))].setdefault( + account.expense_account, {"amount": 0.0, "base_amount": 0.0} + ) + + item_row = item_account_wise_cost[(item.item_code, item.get(row_fieldname))][ + account.expense_account + ] + + if total_item_cost > 0: + item_row["amount"] += account.amount * item.get(based_on_field) / total_item_cost + + item_row["base_amount"] += ( + account.base_amount * item.get(based_on_field) / total_item_cost + ) + else: + item_row["amount"] += item.applicable_charges / exchange_rate + item_row["base_amount"] += item.applicable_charges + + return item_account_wise_cost diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py index 382b7106552..ea2474b112b 100644 --- a/erpnext/stock/doctype/putaway_rule/putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py @@ -334,3 +334,71 @@ def get_serial_nos_to_allocate(serial_nos, to_allocate): return "\n".join(allocated_serial_nos) if allocated_serial_nos else "" else: return "" + + +def validate_putaway_capacity(doc): + # if over receipt is attempted while 'apply putaway rule' is disabled + # and if rule was applied on the transaction, validate it. + valid_doctype = doc.doctype in ( + "Purchase Receipt", + "Stock Entry", + "Purchase Invoice", + "Stock Reconciliation", + ) + + if not frappe.get_all("Putaway Rule", limit=1): + return + + if doc.doctype == "Purchase Invoice" and doc.get("update_stock") == 0: + valid_doctype = False + + if valid_doctype: + rule_map = defaultdict(dict) + for item in doc.get("items"): + warehouse_field = "t_warehouse" if doc.doctype == "Stock Entry" else "warehouse" + rule = frappe.db.get_value( + "Putaway Rule", + {"item_code": item.get("item_code"), "warehouse": item.get(warehouse_field)}, + ["stock_capacity", "name", "disable"], + as_dict=True, + ) + if rule: + if rule.get("disabled"): + continue # dont validate for disabled rule + + if doc.doctype == "Stock Reconciliation": + stock_qty = flt(item.qty) + else: + stock_qty = ( + flt(item.transfer_qty) if doc.doctype == "Stock Entry" else flt(item.stock_qty) + ) + + rule_name = rule.get("name") + if not rule_map[rule_name]: + rule_map[rule_name]["warehouse"] = item.get(warehouse_field) + rule_map[rule_name]["item"] = item.get("item_code") + rule_map[rule_name]["qty_put"] = 0 + rule_map[rule_name]["capacity"] = ( + rule.stock_capacity + if doc.doctype == "Stock Reconciliation" + else get_available_putaway_capacity(rule_name) + ) + rule_map[rule_name]["qty_put"] += flt(stock_qty) + + for rule, values in rule_map.items(): + if flt(values["qty_put"]) > flt(values["capacity"]): + message = _prepare_over_receipt_message(rule, values) + frappe.throw(msg=message, title=_("Over Receipt")) + + +def _prepare_over_receipt_message(rule, values): + message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.").format( + frappe.bold(values["qty_put"]), + frappe.bold(values["item"]), + frappe.bold(values["warehouse"]), + frappe.bold(values["capacity"]), + ) + message += "

" + rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) + message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link) + return message