mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-23 12:06:43 +00:00
fix: rewrite item rate calculation (#56315)
Co-authored-by: Harsh Patadia <harsh@Harshs-MacBook-Air.local>
Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com>
(cherry picked from commit cb0689bd1e)
# Conflicts:
# erpnext/accounts/services/child_item_update.py
# erpnext/controllers/taxes_and_totals.py
# erpnext/public/js/controllers/taxes_and_totals.js
# erpnext/public/js/controllers/transaction.js
# erpnext/selling/doctype/quotation/test_quotation.py
# erpnext/utilities/transaction_base.py
This commit is contained in:
@@ -1922,11 +1922,14 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
def test_create_so_with_margin(self):
|
||||
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
|
||||
price_list_rate = flt(100) * flt(si.plc_conversion_rate)
|
||||
|
||||
si.items[0].price_list_rate = price_list_rate
|
||||
si.items[0].margin_type = "Percentage"
|
||||
si.items[0].margin_rate_or_amount = 25
|
||||
si.items[0].discount_amount = 0.0
|
||||
si.items[0].discount_percentage = 0.0
|
||||
# set rate to zero, so that it is recalculated on save
|
||||
si.items[0].rate = 0
|
||||
si.save()
|
||||
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate))
|
||||
|
||||
|
||||
588
erpnext/accounts/services/child_item_update.py
Normal file
588
erpnext/accounts/services/child_item_update.py
Normal file
@@ -0,0 +1,588 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Child item update service: ChildItemUpdater class and helpers for the update_child_qty_rate API."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.workflow import get_workflow_name
|
||||
from frappe.utils import flt, get_link_to_form, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||
from erpnext.buying.utils import update_last_purchase_rate
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
from erpnext.stock.get_item_details import (
|
||||
get_bin_details,
|
||||
get_conversion_factor,
|
||||
get_item_warehouse_,
|
||||
)
|
||||
|
||||
|
||||
class ChildItemUpdater:
|
||||
"""Validates and applies item-level edits on submitted orders and quotations."""
|
||||
|
||||
def __init__(self, parent_doctype: str, parent_doctype_name: str, child_docname: str = "items"):
|
||||
self.parent_doctype = parent_doctype
|
||||
self.parent_doctype_name = parent_doctype_name
|
||||
self.child_docname = child_docname
|
||||
self.parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
self.allow_zero_qty = get_allow_zero_qty(parent_doctype)
|
||||
self._ordered_items: dict | None = None
|
||||
self._purchased_items: dict | None = None
|
||||
|
||||
def update(self, trans_items: str | list) -> None:
|
||||
"""Process item additions, edits, and deletions from trans_items JSON."""
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
|
||||
from erpnext.selling.doctype.quotation.mapper import get_ordered_items
|
||||
|
||||
data = frappe.parse_json(trans_items)
|
||||
any_qty_changed = False
|
||||
items_added_or_removed = False
|
||||
any_conversion_factor_changed = False
|
||||
|
||||
self._check_permissions("write")
|
||||
|
||||
if self.parent_doctype == "Quotation":
|
||||
self._ordered_items = get_ordered_items(self.parent.name)
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data, self._ordered_items)
|
||||
elif self.parent_doctype == "Supplier Quotation":
|
||||
self._purchased_items = get_purchased_items(self.parent.name)
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data, self._purchased_items)
|
||||
else:
|
||||
items_added_or_removed |= validate_and_delete_children(self.parent, data)
|
||||
|
||||
for d in data:
|
||||
new_child_flag = False
|
||||
rate_unchanged = None
|
||||
|
||||
if not d.get("item_code"):
|
||||
continue
|
||||
|
||||
if not d.get("docname"):
|
||||
new_child_flag = True
|
||||
items_added_or_removed = True
|
||||
self._check_permissions("create")
|
||||
child_item = self._get_new_child_item(d)
|
||||
else:
|
||||
self._check_permissions("write")
|
||||
child_item = frappe.get_doc(self.parent_doctype + " Item", d.get("docname"))
|
||||
|
||||
change_state = get_child_item_change_state(self.parent_doctype, child_item, d)
|
||||
rate_unchanged = change_state.rate_unchanged
|
||||
any_conversion_factor_changed |= not change_state.conversion_factor_unchanged
|
||||
if is_child_item_unchanged(change_state):
|
||||
continue
|
||||
|
||||
self._validate_quantity_and_rate(child_item, d, rate_unchanged)
|
||||
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
if self.parent.doctype in ("Sales Order", "Purchase Order") and self.parent.is_subcontracted:
|
||||
self._validate_fg_item_for_subcontracting(d, new_child_flag)
|
||||
child_item.fg_item_qty = flt(d["fg_item_qty"])
|
||||
if new_child_flag:
|
||||
child_item.fg_item = d["fg_item"]
|
||||
|
||||
child_item.qty = flt(d.get("qty"))
|
||||
child_item.description = d.get("description")
|
||||
update_child_item_rate_and_discount(
|
||||
self.parent_doctype, child_item, d, self.allow_zero_qty, rate_unchanged=rate_unchanged
|
||||
)
|
||||
update_child_item_uom_and_weight(child_item, d)
|
||||
|
||||
if d.get("delivery_date") and self.parent_doctype == "Sales Order":
|
||||
child_item.delivery_date = d.get("delivery_date")
|
||||
|
||||
if d.get("schedule_date") and self.parent_doctype == "Purchase Order":
|
||||
child_item.schedule_date = d.get("schedule_date")
|
||||
|
||||
if d.get("bom_no") and self.parent_doctype == "Sales Order":
|
||||
child_item.bom_no = d.get("bom_no")
|
||||
|
||||
child_item.flags.ignore_validate_update_after_submit = True
|
||||
if new_child_flag:
|
||||
self.parent.load_from_db()
|
||||
child_item.idx = len(self.parent.items) + 1
|
||||
child_item.insert()
|
||||
else:
|
||||
child_item.save(ignore_permissions=True)
|
||||
|
||||
self._post_update(any_qty_changed, items_added_or_removed, any_conversion_factor_changed)
|
||||
|
||||
def _post_update(
|
||||
self, any_qty_changed: bool, items_added_or_removed: bool, any_conversion_factor_changed: bool
|
||||
) -> None:
|
||||
parent = self.parent
|
||||
parent.reload()
|
||||
parent.flags.ignore_validate_update_after_submit = True
|
||||
parent.set_qty_as_per_stock_uom()
|
||||
parent.calculate_taxes_and_totals()
|
||||
parent.set_total_in_words()
|
||||
|
||||
if self.parent_doctype == "Sales Order" and not parent.is_subcontracted:
|
||||
make_packing_list(parent)
|
||||
parent.set_gross_profit()
|
||||
|
||||
frappe.get_cached_doc("Authorization Control").validate_approving_authority(
|
||||
parent.doctype, parent.company, parent.base_grand_total
|
||||
)
|
||||
|
||||
if self.parent_doctype != "Supplier Quotation":
|
||||
from erpnext.accounts.services.payment_schedule import PaymentScheduleService
|
||||
|
||||
PaymentScheduleService(parent).set_payment_schedule()
|
||||
|
||||
if self.parent_doctype == "Purchase Order":
|
||||
parent.validate_minimum_order_qty()
|
||||
parent.validate_budget()
|
||||
if parent.is_against_so():
|
||||
parent.update_status_updater()
|
||||
elif self.parent_doctype == "Sales Order":
|
||||
parent.check_credit_limit()
|
||||
|
||||
for idx, row in enumerate(parent.get(self.child_docname), start=1):
|
||||
row.idx = idx
|
||||
|
||||
parent.save()
|
||||
|
||||
if self.parent_doctype == "Purchase Order":
|
||||
update_last_purchase_rate(parent, is_submit=1)
|
||||
|
||||
if any_qty_changed or items_added_or_removed or any_conversion_factor_changed:
|
||||
parent.update_prevdoc_status()
|
||||
|
||||
parent.update_requested_qty()
|
||||
parent.update_ordered_qty()
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
|
||||
if parent.is_subcontracted and not parent.can_update_items():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
|
||||
elif self.parent_doctype == "Sales Order":
|
||||
if parent.is_subcontracted and not parent.can_update_items():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order."
|
||||
)
|
||||
)
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
parent.update_project()
|
||||
parent.update_prevdoc_status("submit")
|
||||
parent.update_delivery_status()
|
||||
|
||||
parent.reload()
|
||||
self._validate_workflow()
|
||||
|
||||
if self.parent_doctype in ("Purchase Order", "Sales Order"):
|
||||
parent.update_blanket_order()
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
|
||||
parent.validate_uom_is_integer("uom", "qty")
|
||||
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
|
||||
if self.parent_doctype == "Sales Order" and not parent.is_subcontracted:
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
cancel_stock_reservation_entries,
|
||||
has_reserved_stock,
|
||||
)
|
||||
|
||||
if has_reserved_stock(parent.doctype, parent.name):
|
||||
cancel_stock_reservation_entries(parent.doctype, parent.name)
|
||||
if parent.per_picked == 0:
|
||||
parent.create_stock_reservation_entries()
|
||||
|
||||
def _check_permissions(self, perm_type: str = "create") -> None:
|
||||
try:
|
||||
self.parent.check_permission(perm_type)
|
||||
except frappe.PermissionError:
|
||||
actions = {"create": "add", "write": "update"}
|
||||
frappe.throw(
|
||||
_("You do not have permissions to {} items in a {}.").format(
|
||||
actions[perm_type], self.parent_doctype
|
||||
),
|
||||
title=_("Insufficient Permissions"),
|
||||
)
|
||||
|
||||
def _validate_workflow(self) -> None:
|
||||
workflow = get_workflow_name(self.parent.doctype)
|
||||
if not workflow:
|
||||
return
|
||||
|
||||
workflow_doc = frappe.get_doc("Workflow", workflow)
|
||||
current_state = self.parent.get(workflow_doc.workflow_state_field)
|
||||
roles = frappe.get_roles()
|
||||
|
||||
allowed = any(
|
||||
state.state == current_state and (not state.allow_edit or state.allow_edit in roles)
|
||||
for state in workflow_doc.states
|
||||
)
|
||||
|
||||
if not allowed:
|
||||
frappe.throw(
|
||||
_("You are not allowed to update as per the conditions set in {} Workflow.").format(
|
||||
get_link_to_form("Workflow", workflow)
|
||||
),
|
||||
title=_("Insufficient Permissions"),
|
||||
)
|
||||
|
||||
def _get_new_child_item(self, item_row) -> "frappe.model.document.Document":
|
||||
child_doctype = self.parent_doctype + " Item"
|
||||
return set_order_defaults(
|
||||
self.parent_doctype,
|
||||
self.parent_doctype_name,
|
||||
child_doctype,
|
||||
self.child_docname,
|
||||
item_row,
|
||||
)
|
||||
|
||||
def _validate_quantity_and_rate(self, child_item, new_data: dict, rate_unchanged: bool | None) -> None:
|
||||
if not flt(new_data.get("qty")) and not self.allow_zero_qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
|
||||
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||
),
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
qty_limits = {
|
||||
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")),
|
||||
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")),
|
||||
}
|
||||
|
||||
if self.parent_doctype in qty_limits:
|
||||
qty_field, error_message = qty_limits[self.parent_doctype]
|
||||
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
|
||||
frappe.throw(
|
||||
_("Row #{0}:").format(new_data.get("idx")) + error_message,
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
if self.parent_doctype not in ("Quotation", "Supplier Quotation"):
|
||||
return
|
||||
|
||||
items_map = self._ordered_items if self.parent_doctype == "Quotation" else self._purchased_items
|
||||
if not items_map:
|
||||
return
|
||||
|
||||
qty_to_check = items_map.get(child_item.name)
|
||||
if not qty_to_check:
|
||||
return
|
||||
|
||||
if not rate_unchanged:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot update rate as item {0} is already ordered or purchased against this quotation"
|
||||
).format(frappe.bold(new_data.get("item_code")))
|
||||
)
|
||||
|
||||
if flt(new_data.get("qty")) < qty_to_check:
|
||||
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||
|
||||
def _validate_fg_item_for_subcontracting(self, new_data: dict, is_new: bool) -> None:
|
||||
if is_new:
|
||||
if not new_data.get("fg_item"):
|
||||
frappe.throw(
|
||||
_("Finished Good Item is not specified for service item {0}").format(
|
||||
new_data["item_code"]
|
||||
)
|
||||
)
|
||||
|
||||
is_sub_contracted_item, default_bom = frappe.db.get_value(
|
||||
"Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"]
|
||||
)
|
||||
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"])
|
||||
)
|
||||
elif not default_bom:
|
||||
frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"]))
|
||||
|
||||
if not new_data.get("fg_item_qty"):
|
||||
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(
|
||||
parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items"
|
||||
) -> None:
|
||||
ChildItemUpdater(parent_doctype, parent_doctype_name, child_docname).update(trans_items)
|
||||
|
||||
|
||||
def set_order_defaults(
|
||||
parent_doctype: str,
|
||||
parent_doctype_name: str,
|
||||
child_doctype: str,
|
||||
child_docname: str,
|
||||
trans_item: dict,
|
||||
) -> "frappe.model.document.Document":
|
||||
"""Return a new child item populated with item master defaults."""
|
||||
from erpnext.accounts.services.taxes import add_taxes_from_tax_template, set_child_tax_template_and_map
|
||||
|
||||
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
child_item = frappe.new_doc(child_doctype, parent_doc=p_doc, parentfield=child_docname)
|
||||
item = frappe.get_doc("Item", trans_item.get("item_code"))
|
||||
|
||||
for field in ("item_code", "item_name", "description", "item_group", "weight_per_unit", "weight_uom"):
|
||||
child_item.update({field: item.get(field)})
|
||||
|
||||
date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
|
||||
child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
|
||||
child_item.stock_uom = item.stock_uom
|
||||
child_item.uom = trans_item.get("uom") or item.stock_uom
|
||||
child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True)
|
||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
||||
child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company")))
|
||||
|
||||
if child_doctype in ("Purchase Order Item", "Supplier Quotation Item"):
|
||||
child_item.base_rate = 1
|
||||
child_item.base_amount = 1
|
||||
|
||||
if child_doctype == "Sales Order Item":
|
||||
child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True)
|
||||
if not child_item.warehouse:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings."
|
||||
).format(frappe.bold(item.item_code))
|
||||
)
|
||||
|
||||
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||
add_taxes_from_tax_template(child_item, p_doc)
|
||||
return child_item
|
||||
|
||||
|
||||
def validate_child_on_delete(row, parent, ordered_item=None) -> None:
|
||||
"""Raise if a partially transacted child item is being deleted."""
|
||||
if parent.doctype == "Sales Order":
|
||||
if flt(row.delivered_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been delivered").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
if flt(row.work_order_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
if flt(row.ordered_qty):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order."
|
||||
).format(row.idx, row.item_code)
|
||||
)
|
||||
|
||||
if parent.doctype == "Purchase Order" and flt(row.received_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been received").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if parent.doctype in ("Purchase Order", "Sales Order") and flt(row.billed_amt):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if parent.doctype == "Quotation" and ordered_item and ordered_item.get(row.name):
|
||||
frappe.throw(_("Cannot delete an item which has been ordered"))
|
||||
|
||||
|
||||
def update_bin_on_delete(row, doctype: str) -> None:
|
||||
"""Update bin quantities after a child item row is deleted."""
|
||||
from erpnext.stock.stock_balance import (
|
||||
get_indented_qty,
|
||||
get_ordered_qty,
|
||||
get_reserved_qty,
|
||||
update_bin_qty,
|
||||
)
|
||||
|
||||
qty_dict = {}
|
||||
|
||||
if doctype == "Sales Order":
|
||||
qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse)
|
||||
else:
|
||||
if row.material_request_item:
|
||||
qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse)
|
||||
qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
|
||||
|
||||
if row.warehouse:
|
||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||
|
||||
|
||||
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
|
||||
"""Delete child rows not present in data; return True if any were removed."""
|
||||
updated_item_names = [d.get("docname") for d in data]
|
||||
deleted_children = [item for item in parent.items if item.name not in updated_item_names]
|
||||
|
||||
for d in deleted_children:
|
||||
validate_child_on_delete(d, parent, ordered_item)
|
||||
d.cancel()
|
||||
d.delete()
|
||||
|
||||
if parent.doctype == "Purchase Order":
|
||||
parent.update_ordered_qty_in_so_for_removed_items(deleted_children)
|
||||
|
||||
if parent.doctype not in ("Quotation", "Supplier Quotation"):
|
||||
parent.update_prevdoc_status()
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
return bool(deleted_children)
|
||||
|
||||
|
||||
def get_allow_zero_qty(parent_doctype: str) -> bool:
|
||||
if parent_doctype == "Sales Order":
|
||||
return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False
|
||||
if parent_doctype == "Purchase Order":
|
||||
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
|
||||
return False
|
||||
|
||||
|
||||
def get_child_item_change_state(parent_doctype: str, child_item, new_data) -> frappe._dict:
|
||||
prev_rate, new_rate = flt(child_item.get("rate")), flt(new_data.get("rate"))
|
||||
prev_qty, new_qty = flt(child_item.get("qty")), flt(new_data.get("qty"))
|
||||
prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(new_data.get("fg_item_qty"))
|
||||
prev_con_fac = flt(child_item.get("conversion_factor"))
|
||||
new_con_fac = flt(new_data.get("conversion_factor"))
|
||||
|
||||
if parent_doctype == "Sales Order":
|
||||
prev_date, new_date = child_item.get("delivery_date"), new_data.get("delivery_date")
|
||||
elif parent_doctype == "Purchase Order":
|
||||
prev_date, new_date = child_item.get("schedule_date"), new_data.get("schedule_date")
|
||||
else:
|
||||
prev_date, new_date = None, None
|
||||
|
||||
if parent_doctype in ("Quotation", "Supplier Quotation"):
|
||||
date_unchanged = False
|
||||
else:
|
||||
prev_date = getdate(prev_date) if prev_date else None
|
||||
new_date = getdate(new_date) if new_date else None
|
||||
date_unchanged = prev_date == new_date
|
||||
|
||||
return frappe._dict(
|
||||
rate_unchanged=prev_rate == new_rate,
|
||||
qty_unchanged=prev_qty == new_qty,
|
||||
fg_qty_unchanged=prev_fg_qty == new_fg_qty,
|
||||
uom_unchanged=child_item.get("uom") == new_data.get("uom"),
|
||||
conversion_factor_unchanged=prev_con_fac == new_con_fac,
|
||||
date_unchanged=date_unchanged,
|
||||
description_unchanged=child_item.get("description") == new_data.get("description"),
|
||||
)
|
||||
|
||||
|
||||
def is_child_item_unchanged(change_state: frappe._dict) -> bool:
|
||||
return (
|
||||
change_state.rate_unchanged
|
||||
and change_state.qty_unchanged
|
||||
and change_state.fg_qty_unchanged
|
||||
and change_state.conversion_factor_unchanged
|
||||
and change_state.uom_unchanged
|
||||
and change_state.date_unchanged
|
||||
and change_state.description_unchanged
|
||||
)
|
||||
|
||||
|
||||
def update_child_item_rate_and_discount(
|
||||
parent_doctype: str,
|
||||
child_item,
|
||||
new_data,
|
||||
allow_zero_qty: bool,
|
||||
rate_unchanged: bool | None = None,
|
||||
) -> None:
|
||||
rate_precision = child_item.precision("rate") or 2
|
||||
qty_precision = child_item.precision("qty") or 2
|
||||
|
||||
if rate_unchanged is None:
|
||||
rate_unchanged = flt(child_item.get("rate")) == flt(new_data.get("rate"))
|
||||
|
||||
if not rate_unchanged and not child_item.get("qty") and allow_zero_qty:
|
||||
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
||||
|
||||
row_rate = flt(new_data.get("rate"), rate_precision)
|
||||
|
||||
if parent_doctype in ("Purchase Order", "Sales Order"):
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(new_data.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
||||
).format(child_item.idx, child_item.item_code)
|
||||
)
|
||||
|
||||
child_item.rate = row_rate
|
||||
|
||||
if parent_doctype not in ("Sales Order", "Purchase Order") or not flt(child_item.price_list_rate):
|
||||
return
|
||||
|
||||
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
||||
child_item.discount_percentage = 0
|
||||
child_item.discount_amount = 0
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = child_item.price_list_rate
|
||||
child_item.discount_percentage = 0
|
||||
child_item.discount_amount = flt(child_item.rate_with_margin) - flt(child_item.rate)
|
||||
|
||||
|
||||
def update_child_item_uom_and_weight(child_item, new_data) -> None:
|
||||
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||
|
||||
if new_data.get("conversion_factor"):
|
||||
if child_item.stock_uom == child_item.uom:
|
||||
child_item.conversion_factor = 1
|
||||
else:
|
||||
child_item.conversion_factor = flt(new_data.get("conversion_factor"), conv_fac_precision)
|
||||
|
||||
if new_data.get("uom"):
|
||||
child_item.uom = new_data.get("uom")
|
||||
conversion_factor = flt(
|
||||
get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")
|
||||
)
|
||||
child_item.conversion_factor = (
|
||||
flt(new_data.get("conversion_factor"), conv_fac_precision) or conversion_factor
|
||||
)
|
||||
|
||||
if child_item.get("weight_per_unit"):
|
||||
child_item.total_weight = flt(
|
||||
child_item.weight_per_unit * child_item.qty * child_item.conversion_factor,
|
||||
child_item.precision("total_weight"),
|
||||
)
|
||||
|
||||
|
||||
def check_if_child_table_updated(
|
||||
child_table_before_update, child_table_after_update, fields_to_check
|
||||
) -> bool:
|
||||
"""Return True if any accounting-relevant field changed in a child table."""
|
||||
fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"]
|
||||
|
||||
for index, item in enumerate(child_table_before_update):
|
||||
for field in fields_to_check:
|
||||
if child_table_after_update[index].get(field) != item.get(field):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -180,16 +180,21 @@ class calculate_taxes_and_totals:
|
||||
|
||||
self.doc.conversion_rate = flt(self.doc.conversion_rate)
|
||||
|
||||
def calculate_item_values(self):
|
||||
if self.doc.get("is_consolidated"):
|
||||
def calculate_item_rate(self, item):
|
||||
if not item.price_list_rate:
|
||||
remove_margin(item)
|
||||
remove_discount(item)
|
||||
item.rate_with_margin = 0
|
||||
return
|
||||
|
||||
if not self.discount_amount_applied:
|
||||
do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"]
|
||||
has_pricing_rules = item.pricing_rules and not self.doc.ignore_pricing_rule
|
||||
if has_pricing_rules:
|
||||
remove_margin(item)
|
||||
|
||||
for item in self.doc.items:
|
||||
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
|
||||
for d in get_applied_pricing_rules(item.pricing_rules):
|
||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", d)
|
||||
|
||||
<<<<<<< HEAD
|
||||
if item.discount_percentage == 100:
|
||||
item.rate = 0.0
|
||||
elif item.price_list_rate:
|
||||
@@ -237,20 +242,73 @@ class calculate_taxes_and_totals:
|
||||
not item.qty
|
||||
and self.doc.get("is_return")
|
||||
and self.doc.get("doctype") != "Purchase Receipt"
|
||||
=======
|
||||
if not (
|
||||
pricing_rule.margin_type
|
||||
and pricing_rule.margin_rate_or_amount
|
||||
and (
|
||||
pricing_rule.margin_type == "Percentage" or pricing_rule.currency == self.doc.currency
|
||||
)
|
||||
>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315))
|
||||
):
|
||||
item.amount = flt(-1 * item.rate, item.precision("amount"))
|
||||
elif not item.qty and self.doc.get("is_debit_note"):
|
||||
item.amount = flt(item.rate, item.precision("amount"))
|
||||
else:
|
||||
item.amount = flt(item.rate * item.qty, item.precision("amount"))
|
||||
continue
|
||||
|
||||
item.net_amount = item.amount
|
||||
item.margin_type = pricing_rule.margin_type
|
||||
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
|
||||
|
||||
self._set_in_company_currency(
|
||||
item, ["price_list_rate", "rate", "net_rate", "amount", "net_amount"]
|
||||
)
|
||||
item.rate_with_margin = get_rate_with_margin(item)
|
||||
if item.discount_percentage > 0:
|
||||
item.discount_amount = flt(
|
||||
item.rate_with_margin * item.discount_percentage / 100.0, item.precision("discount_amount")
|
||||
)
|
||||
|
||||
item.item_tax_amount = 0.0
|
||||
calculated_rate = flt(item.rate_with_margin - item.discount_amount, item.precision("rate"))
|
||||
|
||||
# if rate is 0 or pricing rules are applicable, calculated rate is preferred
|
||||
if has_pricing_rules or not item.rate:
|
||||
item.rate = calculated_rate
|
||||
return
|
||||
|
||||
# discount and margin are correct, exit early
|
||||
if item.rate == calculated_rate:
|
||||
return
|
||||
|
||||
# item rate does not match calculated rate. prefer item rate, reset margin / discount
|
||||
if item.rate > item.price_list_rate:
|
||||
item.margin_type = "Amount"
|
||||
item.margin_rate_or_amount = flt(
|
||||
item.rate - item.price_list_rate, item.precision("margin_rate_or_amount")
|
||||
)
|
||||
item.rate_with_margin = item.rate
|
||||
remove_discount(item)
|
||||
return
|
||||
|
||||
item.rate_with_margin = item.price_list_rate
|
||||
item.discount_amount = flt(item.rate_with_margin - item.rate, item.precision("discount_amount"))
|
||||
item.discount_percentage = 0
|
||||
remove_margin(item)
|
||||
|
||||
def calculate_item_values(self):
|
||||
if self.doc.get("is_consolidated") or self.discount_amount_applied:
|
||||
return
|
||||
|
||||
do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"]
|
||||
for item in self.doc.items:
|
||||
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
|
||||
self.calculate_item_rate(item)
|
||||
|
||||
item.net_rate = item.rate
|
||||
if not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt":
|
||||
item.amount = flt(-1 * item.rate, item.precision("amount"))
|
||||
elif not item.qty and self.doc.get("is_debit_note"):
|
||||
item.amount = flt(item.rate, item.precision("amount"))
|
||||
else:
|
||||
item.amount = flt(item.rate * item.qty, item.precision("amount"))
|
||||
item.net_amount = item.amount
|
||||
self._set_in_company_currency(
|
||||
item, ["price_list_rate", "rate_with_margin", "rate", "net_rate", "amount", "net_amount"]
|
||||
)
|
||||
item.item_tax_amount = 0.0
|
||||
|
||||
def _set_in_company_currency(self, doc, fields):
|
||||
"""set values in base currency"""
|
||||
@@ -1024,48 +1082,6 @@ class calculate_taxes_and_totals:
|
||||
|
||||
self.calculate_outstanding_amount()
|
||||
|
||||
def calculate_margin(self, item):
|
||||
rate_with_margin = 0.0
|
||||
base_rate_with_margin = 0.0
|
||||
if item.price_list_rate:
|
||||
if item.pricing_rules and not self.doc.ignore_pricing_rule:
|
||||
has_margin = False
|
||||
for d in get_applied_pricing_rules(item.pricing_rules):
|
||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", d)
|
||||
|
||||
if pricing_rule.margin_rate_or_amount and (
|
||||
(
|
||||
pricing_rule.currency == self.doc.currency
|
||||
and pricing_rule.margin_type in ["Amount", "Percentage"]
|
||||
)
|
||||
or pricing_rule.margin_type == "Percentage"
|
||||
):
|
||||
item.margin_type = pricing_rule.margin_type
|
||||
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
|
||||
has_margin = True
|
||||
|
||||
if not has_margin:
|
||||
item.margin_type = None
|
||||
item.margin_rate_or_amount = 0.0
|
||||
|
||||
if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
|
||||
item.margin_type = "Amount"
|
||||
item.margin_rate_or_amount = flt(
|
||||
item.rate - item.price_list_rate, item.precision("margin_rate_or_amount")
|
||||
)
|
||||
item.rate_with_margin = item.rate
|
||||
|
||||
elif item.margin_type and item.margin_rate_or_amount:
|
||||
margin_value = (
|
||||
item.margin_rate_or_amount
|
||||
if item.margin_type == "Amount"
|
||||
else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
|
||||
)
|
||||
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
|
||||
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
|
||||
|
||||
return rate_with_margin, base_rate_with_margin
|
||||
|
||||
def set_item_wise_tax_breakup(self):
|
||||
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
|
||||
|
||||
@@ -1100,6 +1116,29 @@ class calculate_taxes_and_totals:
|
||||
)
|
||||
|
||||
|
||||
def remove_discount(item):
|
||||
item.discount_percentage = 0.0
|
||||
item.discount_amount = 0.0
|
||||
|
||||
|
||||
def remove_margin(item):
|
||||
item.margin_type = None
|
||||
item.margin_rate_or_amount = 0.0
|
||||
|
||||
|
||||
def get_rate_with_margin(item):
|
||||
if not item.margin_type:
|
||||
return item.price_list_rate
|
||||
|
||||
if item.margin_type == "Percentage":
|
||||
return flt(
|
||||
item.price_list_rate * (1 + (item.margin_rate_or_amount / 100.0)),
|
||||
item.precision("rate_with_margin"),
|
||||
)
|
||||
|
||||
return flt(item.price_list_rate + item.margin_rate_or_amount, item.precision("rate_with_margin"))
|
||||
|
||||
|
||||
def get_itemised_tax_breakup_html(doc):
|
||||
if not doc.taxes:
|
||||
return
|
||||
|
||||
@@ -8,18 +8,24 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
apply_pricing_rule_on_item(item) {
|
||||
let effective_item_rate = item.price_list_rate;
|
||||
let item_rate = item.rate;
|
||||
if (["Sales Order", "Quotation"].includes(item.parenttype) && item.blanket_order_rate) {
|
||||
effective_item_rate = item.blanket_order_rate;
|
||||
}
|
||||
|
||||
let rate_with_margin;
|
||||
if (item.margin_type == "Percentage") {
|
||||
<<<<<<< HEAD
|
||||
item.rate_with_margin = flt(effective_item_rate)
|
||||
+ flt(effective_item_rate) * ( flt(item.margin_rate_or_amount) / 100);
|
||||
=======
|
||||
rate_with_margin = effective_item_rate * (1 + item.margin_rate_or_amount / 100);
|
||||
>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315))
|
||||
} else {
|
||||
item.rate_with_margin = flt(effective_item_rate) + flt(item.margin_rate_or_amount);
|
||||
rate_with_margin = effective_item_rate + item.margin_rate_or_amount;
|
||||
}
|
||||
item.base_rate_with_margin = flt(item.rate_with_margin) * flt(this.frm.doc.conversion_rate);
|
||||
item.rate_with_margin = flt(rate_with_margin, precision("rate_with_margin", item));
|
||||
|
||||
<<<<<<< HEAD
|
||||
item_rate = flt(item.rate_with_margin , precision("rate", item));
|
||||
|
||||
if (item.discount_percentage && !item.discount_amount) {
|
||||
@@ -29,8 +35,20 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if (item.discount_amount > 0) {
|
||||
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
|
||||
item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin);
|
||||
=======
|
||||
if (item.discount_percentage) {
|
||||
item.discount_amount = flt(
|
||||
(item.rate_with_margin * item.discount_percentage) / 100,
|
||||
precision("discount_amount", item)
|
||||
);
|
||||
}
|
||||
|
||||
let item_rate = item.rate_with_margin;
|
||||
if (item.discount_amount) {
|
||||
item_rate = item.rate_with_margin - item.discount_amount;
|
||||
>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315))
|
||||
}
|
||||
item_rate = flt(item_rate, precision("rate", item));
|
||||
frappe.model.set_value(item.doctype, item.name, "rate", item_rate);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
<<<<<<< HEAD
|
||||
var has_margin_field = frappe.meta.has_field(cdt, 'margin_type');
|
||||
=======
|
||||
>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315))
|
||||
|
||||
frappe.model.round_floats_in(item, ["rate", "price_list_rate"]);
|
||||
frappe.model.round_floats_in(item, [
|
||||
"rate",
|
||||
"price_list_rate",
|
||||
"margin_rate_or_amount",
|
||||
"discount_amount",
|
||||
"discount_percentage",
|
||||
]);
|
||||
|
||||
<<<<<<< HEAD
|
||||
if(item.price_list_rate && !item.blanket_order_rate) {
|
||||
if(item.rate > item.price_list_rate && has_margin_field) {
|
||||
// if rate is greater than price_list_rate, set margin
|
||||
@@ -38,11 +48,51 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
} else {
|
||||
item.discount_percentage = 0.0;
|
||||
item.margin_type = '';
|
||||
=======
|
||||
if (item.price_list_rate && !item.blanket_order_rate) {
|
||||
const rate_with_margin = get_rate_with_margin(item);
|
||||
|
||||
if (item.discount_percentage) {
|
||||
item.discount_amount = flt(
|
||||
(rate_with_margin * item.discount_percentage) / 100.0,
|
||||
precision("discount_amount", item)
|
||||
);
|
||||
}
|
||||
|
||||
const calculated_rate = flt(rate_with_margin - item.discount_amount, precision("rate", item));
|
||||
|
||||
if (calculated_rate !== item.rate) {
|
||||
// if rate is greater than price_list_rate, set margin
|
||||
// otherwise, set discount
|
||||
if (item.rate > item.price_list_rate) {
|
||||
item.margin_type = "Amount";
|
||||
item.margin_rate_or_amount = flt(
|
||||
item.rate - item.price_list_rate,
|
||||
precision("margin_rate_or_amount", item)
|
||||
);
|
||||
item.rate_with_margin = item.rate;
|
||||
item.discount_amount = 0;
|
||||
item.discount_percentage = 0;
|
||||
} else {
|
||||
item.margin_type = "";
|
||||
item.margin_rate_or_amount = 0;
|
||||
item.rate_with_margin = item.price_list_rate;
|
||||
item.discount_percentage = 0;
|
||||
item.discount_amount = flt(
|
||||
item.rate_with_margin - item.rate,
|
||||
precision("discount_amount", item)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item.margin_type = "";
|
||||
>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315))
|
||||
item.margin_rate_or_amount = 0;
|
||||
item.rate_with_margin = 0;
|
||||
item.discount_amount = 0;
|
||||
item.discount_percentage = 0.0;
|
||||
}
|
||||
item.base_rate_with_margin = item.rate_with_margin * flt(frm.doc.conversion_rate);
|
||||
|
||||
me.set_in_company_currency(item, ["rate_with_margin"]);
|
||||
cur_frm.cscript.set_gross_profit(item);
|
||||
cur_frm.cscript.calculate_taxes_and_totals();
|
||||
cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
|
||||
@@ -2937,3 +2987,13 @@ erpnext.set_unit_price_items_note = (frm) => {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function get_rate_with_margin(item) {
|
||||
if (!item.margin_type) return item.price_list_rate;
|
||||
|
||||
if (item.margin_type === "Percentage") {
|
||||
return flt(item.price_list_rate * (1 + item.margin_rate_or_amount / 100), precision("rate", item));
|
||||
}
|
||||
|
||||
return flt(item.price_list_rate + item.margin_rate_or_amount, precision("rate", item));
|
||||
}
|
||||
|
||||
@@ -395,9 +395,9 @@ class TestQuotation(FrappeTestCase):
|
||||
quotation.save()
|
||||
quotation.submit()
|
||||
|
||||
self.assertEqual(quotation.payment_schedule[0].payment_amount, 8906.00)
|
||||
self.assertEqual(quotation.payment_schedule[0].payment_amount, 500.00)
|
||||
self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date)
|
||||
self.assertEqual(quotation.payment_schedule[1].payment_amount, 8906.00)
|
||||
self.assertEqual(quotation.payment_schedule[1].payment_amount, 500.00)
|
||||
self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30))
|
||||
|
||||
sales_order = make_sales_order(quotation.name)
|
||||
@@ -417,11 +417,11 @@ class TestQuotation(FrappeTestCase):
|
||||
sales_order.set("taxes", [])
|
||||
sales_order.save()
|
||||
|
||||
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
|
||||
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 500.00)
|
||||
self.assertEqual(
|
||||
getdate(sales_order.payment_schedule[0].due_date), getdate(quotation.transaction_date)
|
||||
)
|
||||
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
|
||||
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 500.00)
|
||||
self.assertEqual(
|
||||
getdate(sales_order.payment_schedule[1].due_date),
|
||||
getdate(add_days(quotation.transaction_date, 30)),
|
||||
@@ -457,11 +457,23 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
rate_with_margin = flt((1500 * 18.75) / 100 + 1500)
|
||||
|
||||
<<<<<<< HEAD
|
||||
test_records[0]["items"][0]["price_list_rate"] = 1500
|
||||
test_records[0]["items"][0]["margin_type"] = "Percentage"
|
||||
test_records[0]["items"][0]["margin_rate_or_amount"] = 18.75
|
||||
|
||||
quotation = frappe.copy_doc(test_records[0])
|
||||
=======
|
||||
test_record = frappe.copy_doc(self.globalTestRecords["Quotation"][0])
|
||||
|
||||
test_record.items[0].price_list_rate = 1500
|
||||
test_record.items[0].margin_type = "Percentage"
|
||||
test_record.items[0].margin_rate_or_amount = 18.75
|
||||
# set rate to zero, so that it is recalculated on save
|
||||
test_record.items[0].rate = 0
|
||||
|
||||
quotation = frappe.copy_doc(test_record)
|
||||
>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315))
|
||||
quotation.transaction_date = nowdate()
|
||||
quotation.valid_till = add_months(quotation.transaction_date, 1)
|
||||
quotation.insert()
|
||||
|
||||
@@ -1451,6 +1451,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
so.items[0].price_list_rate = price_list_rate = 100
|
||||
so.items[0].margin_type = "Percentage"
|
||||
so.items[0].margin_rate_or_amount = 25
|
||||
# set rate to zero, so that it is recalculated on save
|
||||
so.items[0].rate = 0
|
||||
so.save()
|
||||
|
||||
new_so = frappe.copy_doc(so)
|
||||
|
||||
@@ -292,6 +292,291 @@ class TransactionBase(StatusUpdater):
|
||||
)
|
||||
)
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
def fetch_item_details(self, item: dict) -> dict:
|
||||
return get_item_details(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item.get("item_code"),
|
||||
"barcode": item.get("barcode"),
|
||||
"serial_no": item.get("serial_no"),
|
||||
"batch_no": item.get("batch_no"),
|
||||
"set_warehouse": self.get("set_warehouse"),
|
||||
"warehouse": item.get("warehouse"),
|
||||
"customer": self.get("customer") or self.get("party_name"),
|
||||
"quotation_to": self.get("quotation_to"),
|
||||
"supplier": self.get("supplier"),
|
||||
"currency": self.get("currency"),
|
||||
"is_internal_supplier": self.get("is_internal_supplier"),
|
||||
"is_internal_customer": self.get("is_internal_customer"),
|
||||
"update_stock": self.update_stock
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]
|
||||
else False,
|
||||
"conversion_rate": self.get("conversion_rate"),
|
||||
"price_list": self.get("selling_price_list") or self.get("buying_price_list"),
|
||||
"price_list_currency": self.get("price_list_currency"),
|
||||
"plc_conversion_rate": self.get("plc_conversion_rate"),
|
||||
"company": self.get("company"),
|
||||
"order_type": self.get("order_type"),
|
||||
"is_pos": cint(self.get("is_pos")),
|
||||
"is_return": cint(self.get("is_return")),
|
||||
"is_subcontracted": self.get("is_subcontracted"),
|
||||
"ignore_pricing_rule": self.get("ignore_pricing_rule"),
|
||||
"doctype": self.get("doctype"),
|
||||
"name": self.get("name"),
|
||||
"project": item.get("project") or self.get("project"),
|
||||
"qty": item.get("qty") or 1,
|
||||
"net_rate": item.get("rate"),
|
||||
"base_net_rate": item.get("base_net_rate"),
|
||||
"stock_qty": item.get("stock_qty"),
|
||||
"conversion_factor": item.get("conversion_factor"),
|
||||
"weight_per_unit": item.get("weight_per_unit"),
|
||||
"uom": item.get("uom"),
|
||||
"weight_uom": item.get("weight_uom"),
|
||||
"manufacturer": item.get("manufacturer"),
|
||||
"stock_uom": item.get("stock_uom"),
|
||||
"pos_profile": self.get("pos_profile") if cint(self.get("is_pos")) else "",
|
||||
"cost_center": item.get("cost_center"),
|
||||
"tax_category": self.get("tax_category"),
|
||||
"item_tax_template": item.get("item_tax_template"),
|
||||
"child_doctype": item.get("doctype"),
|
||||
"child_docname": item.get("name"),
|
||||
}
|
||||
),
|
||||
self,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def process_item_selection(self, item_idx: int):
|
||||
# Server side 'item' doc. Update this to reflect in UI
|
||||
item_obj = self.get("items", {"idx": item_idx})[0]
|
||||
|
||||
if not item_obj.item_code:
|
||||
return
|
||||
|
||||
# 'item_details' has latest item related values
|
||||
item_details = self.fetch_item_details(item_obj)
|
||||
|
||||
self.set_fetched_values(item_obj, item_details)
|
||||
|
||||
if self.doctype == "Request for Quotation":
|
||||
return
|
||||
|
||||
self.set_item_rate_and_discounts(item_obj, item_details)
|
||||
self.add_taxes_from_item_template(item_obj, item_details)
|
||||
self.add_free_item(item_obj, item_details)
|
||||
self.handle_internal_parties(item_obj, item_details)
|
||||
self.conversion_factor(item_obj, item_details)
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def set_fetched_values(self, item_obj: object, item_details: dict) -> None:
|
||||
for k, v in item_details.items():
|
||||
if hasattr(item_obj, k):
|
||||
setattr(item_obj, k, v)
|
||||
|
||||
def handle_internal_parties(self, item_obj: object, item_details: dict) -> None:
|
||||
fetch_valuation_rate_for_internal_transaction = cint(
|
||||
frappe.get_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction")
|
||||
)
|
||||
if (
|
||||
self.get("is_internal_customer") or self.get("is_internal_supplier")
|
||||
) and fetch_valuation_rate_for_internal_transaction:
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": item_obj.item_code,
|
||||
"warehouse": item_obj.from_warehouse
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]
|
||||
else item_obj.warehouse,
|
||||
"qty": item_obj.qty * item_obj.conversion_factor,
|
||||
"voucher_type": self.doctype,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
|
||||
if self.doctype in ["Purchase Order", "Sales Order"]:
|
||||
args.update(
|
||||
{
|
||||
"posting_date": self.transaction_date,
|
||||
"posting_time": self.transaction_time,
|
||||
}
|
||||
)
|
||||
else:
|
||||
args.update(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"serial_no": item_obj.serial_no,
|
||||
"batch_no": item_obj.batch_no,
|
||||
"allow_zero_valuation_rate": item_obj.allow_zero_valuation_rate,
|
||||
}
|
||||
)
|
||||
|
||||
rate = get_incoming_rate(args=args)
|
||||
item_obj.rate = rate * item_obj.conversion_factor
|
||||
else:
|
||||
self.set_rate_based_on_price_list(item_obj, item_details)
|
||||
|
||||
def add_taxes_from_item_template(self, item_obj: object, item_details: dict) -> None:
|
||||
if item_details.item_tax_rate and frappe.get_single_value(
|
||||
"Accounts Settings", "add_taxes_from_item_tax_template"
|
||||
):
|
||||
item_tax_template = frappe.json.loads(item_details.item_tax_rate)
|
||||
for tax_head, _rate in item_tax_template.items():
|
||||
if _rate == NOT_APPLICABLE_TAX:
|
||||
continue
|
||||
|
||||
found = [x for x in self.taxes if x.account_head == tax_head]
|
||||
if not found:
|
||||
child_doctype = self.get_table_field_doctype("taxes")
|
||||
child = frappe.new_doc(child_doctype, parent_doc=self, parentfield="taxes")
|
||||
child.charge_type = "On Net Total"
|
||||
child.account_head = tax_head
|
||||
child.rate = 0
|
||||
self.append("taxes", child)
|
||||
|
||||
def set_rate_based_on_price_list(self, item_obj: object, item_details: dict) -> None:
|
||||
if item_obj.price_list_rate and item_obj.discount_percentage:
|
||||
item_obj.rate = flt(
|
||||
item_obj.price_list_rate * (1 - item_obj.discount_percentage / 100.0),
|
||||
item_obj.precision("rate"),
|
||||
)
|
||||
|
||||
def copy_from_first_row(self, row, fields):
|
||||
if self.items and row:
|
||||
fields.extend([x.get("fieldname") for x in get_dimensions(True)[0]])
|
||||
first_row = self.items[0]
|
||||
[setattr(row, k, first_row.get(k)) for k in fields if hasattr(first_row, k)]
|
||||
|
||||
def add_free_item(self, item_obj: object, item_details: dict) -> None:
|
||||
free_items = item_details.get("free_item_data")
|
||||
if free_items and len(free_items):
|
||||
existing_free_items = [x for x in self.items if x.is_free_item]
|
||||
for free_item in free_items:
|
||||
_matches = [
|
||||
x
|
||||
for x in existing_free_items
|
||||
if x.item_code == free_item.get("item_code")
|
||||
and x.pricing_rules == free_item.get("pricing_rules")
|
||||
]
|
||||
if _matches:
|
||||
row_to_modify = _matches[0]
|
||||
else:
|
||||
row_to_modify = self.append("items")
|
||||
|
||||
for k, _v in free_item.items():
|
||||
setattr(row_to_modify, k, free_item.get(k))
|
||||
|
||||
self.copy_from_first_row(row_to_modify, ["expense_account", "income_account"])
|
||||
|
||||
def conversion_factor(self, item_obj: object, item_details: dict) -> None:
|
||||
if frappe.get_meta(item_obj.doctype).has_field("stock_qty"):
|
||||
item_obj.stock_qty = flt(
|
||||
item_obj.qty * item_obj.conversion_factor, item_obj.precision("stock_qty")
|
||||
)
|
||||
|
||||
if self.doctype != "Material Request":
|
||||
item_obj.total_weight = flt(item_obj.stock_qty * item_obj.weight_per_unit)
|
||||
self.calculate_net_weight()
|
||||
|
||||
# TODO: for handling customization not to fetch price list rate
|
||||
if frappe.flags.dont_fetch_price_list_rate:
|
||||
return
|
||||
|
||||
if not frappe.flags.dont_fetch_price_list_rate and frappe.get_meta(self.doctype).has_field(
|
||||
"price_list_currency"
|
||||
):
|
||||
self._apply_price_list(item_obj, True)
|
||||
self.calculate_stock_uom_rate(item_obj)
|
||||
|
||||
def calculate_stock_uom_rate(self, item_obj: object) -> None:
|
||||
if item_obj.rate:
|
||||
item_obj.stock_uom_rate = flt(item_obj.rate) / flt(item_obj.conversion_factor)
|
||||
|
||||
def set_item_rate_and_discounts(self, item_obj: object, item_details: dict) -> None:
|
||||
effective_item_rate = item_details.price_list_rate
|
||||
item_rate = item_details.rate
|
||||
|
||||
# Field order precedance
|
||||
# blanket_order_rate -> margin_type -> discount_percentage -> discount_amount
|
||||
if item_obj.parenttype in ["Sales Order", "Quotation"] and item_obj.blanket_order_rate:
|
||||
effective_item_rate = item_obj.blanket_order_rate
|
||||
|
||||
if item_obj.margin_type == "Percentage":
|
||||
item_obj.rate_with_margin = flt(effective_item_rate) + flt(effective_item_rate) * (
|
||||
flt(item_obj.margin_rate_or_amount) / 100
|
||||
)
|
||||
else:
|
||||
item_obj.rate_with_margin = flt(effective_item_rate) + flt(item_obj.margin_rate_or_amount)
|
||||
|
||||
item_obj.base_rate_with_margin = flt(item_obj.rate_with_margin) * flt(self.conversion_rate)
|
||||
item_rate = flt(item_obj.rate_with_margin, item_obj.precision("rate"))
|
||||
|
||||
if item_obj.discount_percentage:
|
||||
item_obj.discount_amount = (
|
||||
flt(item_obj.rate_with_margin) * flt(item_obj.discount_percentage) / 100
|
||||
)
|
||||
|
||||
if item_obj.discount_amount:
|
||||
item_rate = flt(
|
||||
(item_obj.rate_with_margin) - (item_obj.discount_amount), item_obj.precision("rate")
|
||||
)
|
||||
|
||||
item_obj.rate = item_rate
|
||||
|
||||
def calculate_net_weight(self):
|
||||
self.total_net_weight = sum([x.get("total_weight") or 0 for x in self.items])
|
||||
self.apply_shipping_rule()
|
||||
|
||||
def _apply_price_list(self, item_obj: object, reset_plc_conversion: bool) -> None:
|
||||
if self.doctype == "Material Request":
|
||||
return
|
||||
|
||||
if not reset_plc_conversion:
|
||||
self.plc_conversion_rate = ""
|
||||
|
||||
if not self.items or not (item_obj.get("selling_price_list") or item_obj.get("buying_price_list")):
|
||||
return
|
||||
|
||||
if self.get("in_apply_price_list"):
|
||||
return
|
||||
|
||||
self.in_apply_price_list = True
|
||||
|
||||
from erpnext.stock.get_item_details import apply_price_list
|
||||
|
||||
args = {
|
||||
"items": [x.as_dict() for x in self.items],
|
||||
"customer": self.customer or self.party_name,
|
||||
"quotation_to": self.quotation_to,
|
||||
"customer_group": self.customer_group,
|
||||
"territory": self.territory,
|
||||
"supplier": self.supplier,
|
||||
"supplier_group": self.supplier_group,
|
||||
"currency": self.currency,
|
||||
"conversion_rate": self.conversion_rate,
|
||||
"price_list": self.selling_price_list or self.buying_price_list,
|
||||
"price_list_currency": self.price_list_currency,
|
||||
"plc_conversion_rate": self.plc_conversion_rate,
|
||||
"company": self.company,
|
||||
"transaction_date": self.transaction_date or self.posting_date,
|
||||
"campaign": self.campaign,
|
||||
"sales_partner": self.sales_partner,
|
||||
"ignore_pricing_rule": self.ignore_pricing_rule,
|
||||
"doctype": self.doctype,
|
||||
"name": self.name,
|
||||
"is_return": self.is_return,
|
||||
"update_stock": self.update_stock if self.doctype in ["Sales Invoice", "Purchase Invoice"] else 0,
|
||||
"conversion_factor": self.conversion_factor,
|
||||
"pos_profile": self.pos_profile if self.doctype == "Sales Invoice" else "",
|
||||
"coupon_code": self.coupon_code,
|
||||
"is_internal_supplier": self.is_internal_supplier,
|
||||
"is_internal_customer": self.is_internal_customer,
|
||||
}
|
||||
# TODO: test method call impact on document
|
||||
apply_price_list(cts=args, as_doc=True, doc=self)
|
||||
|
||||
>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315))
|
||||
|
||||
def delete_events(ref_type, ref_name):
|
||||
events = (
|
||||
|
||||
Reference in New Issue
Block a user