mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-25 16:04:46 +00:00
Merge pull request #48634 from mihir-kandoi/46788
This commit is contained in:
@@ -1504,7 +1504,7 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=
|
|||||||
|
|
||||||
|
|
||||||
def add_operating_cost_component_wise(
|
def add_operating_cost_component_wise(
|
||||||
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None, job_card=None
|
stock_entry, work_order=None, consumed_operating_cost=None, op_expense_account=None, job_card=None
|
||||||
):
|
):
|
||||||
if not work_order:
|
if not work_order:
|
||||||
return False
|
return False
|
||||||
@@ -1528,11 +1528,11 @@ def add_operating_cost_component_wise(
|
|||||||
get_component_account(wc.operating_component, stock_entry.company) or op_expense_account
|
get_component_account(wc.operating_component, stock_entry.company) or op_expense_account
|
||||||
)
|
)
|
||||||
actual_cp_operating_cost = flt(
|
actual_cp_operating_cost = flt(
|
||||||
flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0),
|
flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0) - consumed_operating_cost,
|
||||||
row.precision("actual_operating_cost"),
|
row.precision("actual_operating_cost"),
|
||||||
)
|
)
|
||||||
|
|
||||||
per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty)
|
per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty - work_order.produced_qty)
|
||||||
|
|
||||||
if per_unit_cost and expense_account:
|
if per_unit_cost and expense_account:
|
||||||
stock_entry.append(
|
stock_entry.append(
|
||||||
@@ -1543,6 +1543,7 @@ def add_operating_cost_component_wise(
|
|||||||
wc.operating_component, row.operation
|
wc.operating_component, row.operation
|
||||||
),
|
),
|
||||||
"amount": per_unit_cost * flt(stock_entry.fg_completed_qty),
|
"amount": per_unit_cost * flt(stock_entry.fg_completed_qty),
|
||||||
|
"has_operating_cost": 1,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1559,13 +1560,20 @@ def get_component_account(parent, company):
|
|||||||
|
|
||||||
|
|
||||||
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
|
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
|
from erpnext.stock.doctype.stock_entry.stock_entry import (
|
||||||
|
get_consumed_operating_cost,
|
||||||
|
get_operating_cost_per_unit,
|
||||||
|
)
|
||||||
|
|
||||||
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
||||||
|
|
||||||
if operating_cost_per_unit:
|
if operating_cost_per_unit:
|
||||||
cost_added = add_operating_cost_component_wise(
|
cost_added = add_operating_cost_component_wise(
|
||||||
stock_entry, work_order, operating_cost_per_unit, expense_account, job_card=job_card
|
stock_entry,
|
||||||
|
work_order,
|
||||||
|
get_consumed_operating_cost(work_order.name, stock_entry.bom_no),
|
||||||
|
expense_account,
|
||||||
|
job_card=job_card,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not cost_added:
|
if not cost_added:
|
||||||
@@ -1575,6 +1583,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_
|
|||||||
"expense_account": expense_account,
|
"expense_account": expense_account,
|
||||||
"description": _("Operating Cost as per Work Order / BOM"),
|
"description": _("Operating Cost as per Work Order / BOM"),
|
||||||
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
|
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
|
||||||
|
"has_operating_cost": 1,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -708,6 +708,119 @@ class TestJobCard(ERPNextTestSuite):
|
|||||||
self.assertEqual(wo_doc.process_loss_qty, 2)
|
self.assertEqual(wo_doc.process_loss_qty, 2)
|
||||||
self.assertEqual(wo_doc.status, "Completed")
|
self.assertEqual(wo_doc.status, "Completed")
|
||||||
|
|
||||||
|
def test_op_cost_calculation(self):
|
||||||
|
from erpnext.manufacturing.doctype.routing.test_routing import (
|
||||||
|
create_routing,
|
||||||
|
setup_bom,
|
||||||
|
setup_operations,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||||
|
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||||
|
make_stock_entry as make_stock_entry_for_wo,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
|
||||||
|
make_workstation(workstation_name="Test Workstation Z", hour_rate_rent=240)
|
||||||
|
operations = [
|
||||||
|
{"operation": "Test Operation A1", "workstation": "Test Workstation Z", "time_in_mins": 30},
|
||||||
|
]
|
||||||
|
|
||||||
|
warehouse = create_warehouse("Test Warehouse 123 for Job Card")
|
||||||
|
setup_operations(operations)
|
||||||
|
|
||||||
|
item_code = "Test Job Card Process Qty Item"
|
||||||
|
for item in [item_code, item_code + "RM 1", item_code + "RM 2"]:
|
||||||
|
if not frappe.db.exists("Item", item):
|
||||||
|
make_item(
|
||||||
|
item,
|
||||||
|
{
|
||||||
|
"item_name": item,
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
|
||||||
|
bom_doc = setup_bom(
|
||||||
|
item_code=item_code,
|
||||||
|
routing=routing_doc.name,
|
||||||
|
raw_materials=[item_code + "RM 1", item_code + "RM 2"],
|
||||||
|
source_warehouse=warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in bom_doc.items:
|
||||||
|
make_stock_entry(
|
||||||
|
item_code=row.item_code,
|
||||||
|
target=row.source_warehouse,
|
||||||
|
qty=10,
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
wo_doc = make_wo_order_test_record(
|
||||||
|
production_item=item_code,
|
||||||
|
bom_no=bom_doc.name,
|
||||||
|
qty=10,
|
||||||
|
skip_transfer=1,
|
||||||
|
wip_warehouse=warehouse,
|
||||||
|
source_warehouse=warehouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
first_job_card = frappe.get_all(
|
||||||
|
"Job Card",
|
||||||
|
filters={"work_order": wo_doc.name, "sequence_id": 1},
|
||||||
|
fields=["name"],
|
||||||
|
order_by="sequence_id",
|
||||||
|
limit=1,
|
||||||
|
)[0].name
|
||||||
|
|
||||||
|
jc = frappe.get_doc("Job Card", first_job_card)
|
||||||
|
for _ in jc.scheduled_time_logs:
|
||||||
|
jc.append(
|
||||||
|
"time_logs",
|
||||||
|
{
|
||||||
|
"from_time": now(),
|
||||||
|
"to_time": add_to_date(now(), minutes=1),
|
||||||
|
"completed_qty": 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
jc.for_quantity = 4
|
||||||
|
jc.save()
|
||||||
|
jc.submit()
|
||||||
|
|
||||||
|
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 4))
|
||||||
|
s.submit()
|
||||||
|
|
||||||
|
self.assertEqual(s.additional_costs[0].amount, 4)
|
||||||
|
|
||||||
|
make_job_card(
|
||||||
|
wo_doc.name,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": wo_doc.operations[0].name,
|
||||||
|
"operation": "Test Operation A1",
|
||||||
|
"qty": 6,
|
||||||
|
"pending_qty": 6,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||||
|
job_card.append(
|
||||||
|
"time_logs",
|
||||||
|
{
|
||||||
|
"from_time": add_to_date(now(), hours=1),
|
||||||
|
"to_time": add_to_date(now(), hours=1, minutes=2),
|
||||||
|
"completed_qty": 6,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
job_card.for_quantity = 6
|
||||||
|
job_card.save()
|
||||||
|
job_card.submit()
|
||||||
|
|
||||||
|
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6))
|
||||||
|
self.assertEqual(s.additional_costs[0].amount, 8)
|
||||||
|
|
||||||
|
|
||||||
def create_bom_with_multiple_operations():
|
def create_bom_with_multiple_operations():
|
||||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||||
|
|||||||
@@ -166,9 +166,10 @@ class WorkOrder(Document):
|
|||||||
operation_details = frappe._dict(
|
operation_details = frappe._dict(
|
||||||
frappe.get_all(
|
frappe.get_all(
|
||||||
"Job Card",
|
"Job Card",
|
||||||
fields=["operation", "for_quantity"],
|
fields=["operation", "sum(for_quantity)"],
|
||||||
filters={"docstatus": ("<", 2), "work_order": self.name},
|
filters={"docstatus": ("<", 2), "work_order": self.name},
|
||||||
as_list=1,
|
as_list=1,
|
||||||
|
group_by="operation_id",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"col_break3",
|
"col_break3",
|
||||||
"amount",
|
"amount",
|
||||||
"base_amount",
|
"base_amount",
|
||||||
"has_corrective_cost"
|
"has_corrective_cost",
|
||||||
|
"has_operating_cost"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -70,13 +71,20 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Has Corrective Cost",
|
"label": "Has Corrective Cost",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "has_operating_cost",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Has Operating Cost",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-09 10:22:20.286641",
|
"modified": "2025-07-16 15:27:59.175530",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Landed Cost Taxes and Charges",
|
"name": "Landed Cost Taxes and Charges",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class LandedCostTaxesandCharges(Document):
|
|||||||
exchange_rate: DF.Float
|
exchange_rate: DF.Float
|
||||||
expense_account: DF.Link | None
|
expense_account: DF.Link | None
|
||||||
has_corrective_cost: DF.Check
|
has_corrective_cost: DF.Check
|
||||||
|
has_operating_cost: DF.Check
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -3417,6 +3417,26 @@ def get_work_order_details(work_order, company):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_consumed_operating_cost(wo_name, bom_no):
|
||||||
|
table = frappe.qb.DocType("Stock Entry")
|
||||||
|
child_table = frappe.qb.DocType("Landed Cost Taxes and Charges")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(child_table)
|
||||||
|
.join(table)
|
||||||
|
.on(child_table.parent == table.name)
|
||||||
|
.select(Sum(child_table.amount).as_("consumed_cost"))
|
||||||
|
.where(
|
||||||
|
(table.docstatus == 1)
|
||||||
|
& (table.work_order == wo_name)
|
||||||
|
& (table.purpose == "Manufacture")
|
||||||
|
& (table.bom_no == bom_no)
|
||||||
|
& (child_table.has_operating_cost == 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cost = query.run(pluck="consumed_cost")
|
||||||
|
return cost[0] if cost and cost[0] else 0
|
||||||
|
|
||||||
|
|
||||||
def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||||
operating_cost_per_unit = 0
|
operating_cost_per_unit = 0
|
||||||
if work_order:
|
if work_order:
|
||||||
@@ -3434,7 +3454,9 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
|||||||
|
|
||||||
for d in work_order.get("operations"):
|
for d in work_order.get("operations"):
|
||||||
if flt(d.completed_qty):
|
if flt(d.completed_qty):
|
||||||
operating_cost_per_unit += flt(d.actual_operating_cost) / flt(d.completed_qty)
|
operating_cost_per_unit += flt(
|
||||||
|
d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no)
|
||||||
|
) / flt(d.completed_qty - work_order.produced_qty)
|
||||||
elif work_order.qty:
|
elif work_order.qty:
|
||||||
operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty)
|
operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user