mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-25 16:04:46 +00:00
feat: co product by product support (#52979)
This commit is contained in:
@@ -1435,7 +1435,7 @@ class StockController(AccountsController):
|
||||
elif self.doctype == "Stock Entry" and row.t_warehouse:
|
||||
qi_required = True # inward stock needs inspection
|
||||
|
||||
if row.get("is_scrap_item"):
|
||||
if row.get("type") or row.get("is_legacy_scrap_item"):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
|
||||
@@ -160,7 +160,7 @@ class SubcontractingController(StockController):
|
||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||
)
|
||||
|
||||
if not item.get("is_scrap_item"):
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
@@ -206,7 +206,7 @@ class SubcontractingController(StockController):
|
||||
).format(item.idx, item.item_name)
|
||||
)
|
||||
|
||||
if self.doctype != "Subcontracting Inward Order":
|
||||
if self.doctype not in ["Subcontracting Inward Order", "Subcontracting Receipt"]:
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if item.bom:
|
||||
@@ -238,7 +238,7 @@ class SubcontractingController(StockController):
|
||||
and self._doc_before_save
|
||||
):
|
||||
for row in self._doc_before_save.get("items"):
|
||||
item_dict[row.name] = (row.item_code, row.qty + (row.get("rejected_qty") or 0))
|
||||
item_dict[row.name] = (row.item_code, row.received_qty)
|
||||
|
||||
return item_dict
|
||||
|
||||
@@ -264,7 +264,7 @@ class SubcontractingController(StockController):
|
||||
self.__reference_name.append(row.name)
|
||||
if (row.name not in item_dict) or (
|
||||
row.item_code,
|
||||
row.qty + (row.get("rejected_qty") or 0),
|
||||
row.received_qty,
|
||||
) != item_dict[row.name]:
|
||||
self.__changed_name.append(row.name)
|
||||
|
||||
@@ -961,7 +961,7 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
qty = (
|
||||
flt(bom_item.qty_consumed_per_unit)
|
||||
* flt(row.qty + (row.get("rejected_qty") or 0))
|
||||
* flt(row.get("received_qty") or (row.qty + (row.get("rejected_qty") or 0)))
|
||||
* row.conversion_factor
|
||||
)
|
||||
bom_item.main_item_code = row.item_code
|
||||
@@ -1278,22 +1278,28 @@ class SubcontractingController(StockController):
|
||||
if self.total_additional_costs:
|
||||
if self.distribute_additional_costs_based_on == "Amount":
|
||||
total_amt = sum(
|
||||
flt(item.amount) for item in self.get("items") if not item.get("is_scrap_item")
|
||||
flt(item.amount)
|
||||
for item in self.get("items")
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item")
|
||||
)
|
||||
for item in self.items:
|
||||
if not item.get("is_scrap_item"):
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = (
|
||||
(item.amount * self.total_additional_costs) / total_amt
|
||||
) / item.qty
|
||||
else:
|
||||
total_qty = sum(flt(item.qty) for item in self.get("items") if not item.get("is_scrap_item"))
|
||||
total_qty = sum(
|
||||
flt(item.qty)
|
||||
for item in self.get("items")
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item")
|
||||
)
|
||||
additional_cost_per_qty = self.total_additional_costs / total_qty
|
||||
for item in self.items:
|
||||
if not item.get("is_scrap_item"):
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = additional_cost_per_qty
|
||||
else:
|
||||
for item in self.items:
|
||||
if not item.get("is_scrap_item"):
|
||||
if not item.get("type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = 0
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.query_builder import Case
|
||||
@@ -18,7 +20,7 @@ class SubcontractingInwardController:
|
||||
def on_submit_subcontracting_inward(self):
|
||||
self.update_inward_order_item()
|
||||
self.update_inward_order_received_items()
|
||||
self.update_inward_order_scrap_items()
|
||||
self.update_inward_order_secondary_items()
|
||||
self.create_stock_reservation_entries_for_inward()
|
||||
self.update_inward_order_status()
|
||||
|
||||
@@ -28,7 +30,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery()
|
||||
self.validate_receive_from_customer_cancel()
|
||||
self.update_inward_order_received_items()
|
||||
self.update_inward_order_scrap_items()
|
||||
self.update_inward_order_secondary_items()
|
||||
self.remove_reference_for_additional_items()
|
||||
self.update_inward_order_status()
|
||||
|
||||
@@ -239,7 +241,8 @@ class SubcontractingInwardController:
|
||||
item
|
||||
for item in self.get("items")
|
||||
if not item.is_finished_item
|
||||
and not item.is_scrap_item
|
||||
and not item.type
|
||||
and not item.is_legacy_scrap_item
|
||||
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
|
||||
]
|
||||
|
||||
@@ -368,7 +371,9 @@ class SubcontractingInwardController:
|
||||
if self.subcontracting_inward_order:
|
||||
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
|
||||
for item in self.items:
|
||||
if (item.is_finished_item or item.is_scrap_item) and item.valuation_rate == 0:
|
||||
if (
|
||||
item.is_finished_item or item.type or item.is_legacy_scrap_item
|
||||
) and item.valuation_rate == 0:
|
||||
item.allow_zero_valuation_rate = 1
|
||||
|
||||
def validate_warehouse_(self):
|
||||
@@ -467,7 +472,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery_on_save()
|
||||
else:
|
||||
for item in self.items:
|
||||
if not item.is_scrap_item:
|
||||
if not item.type and not item.is_legacy_scrap_item:
|
||||
delivered_qty, returned_qty = frappe.get_value(
|
||||
"Subcontracting Inward Order Item",
|
||||
item.scio_detail,
|
||||
@@ -519,7 +524,7 @@ class SubcontractingInwardController:
|
||||
if max_allowed_qty:
|
||||
max_allowed_qty = max_allowed_qty[0]
|
||||
else:
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty"))
|
||||
@@ -538,8 +543,8 @@ class SubcontractingInwardController:
|
||||
bold(
|
||||
frappe.get_cached_value(
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.is_scrap_item
|
||||
else "Subcontracting Inward Order Scrap Item",
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item",
|
||||
item.scio_detail,
|
||||
"stock_uom",
|
||||
)
|
||||
@@ -590,9 +595,9 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
for item in [item for item in self.items if not item.is_finished_item]:
|
||||
if item.is_scrap_item:
|
||||
scio_scrap_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
if item.type or item.is_legacy_scrap_item:
|
||||
scio_secondary_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"item_code": item.item_code,
|
||||
@@ -603,12 +608,13 @@ class SubcontractingInwardController:
|
||||
as_dict=True,
|
||||
)
|
||||
if (
|
||||
scio_scrap_item
|
||||
and scio_scrap_item.delivered_qty > scio_scrap_item.produced_qty - item.transfer_qty
|
||||
scio_secondary_item
|
||||
and scio_secondary_item.delivered_qty
|
||||
> scio_secondary_item.produced_qty - item.transfer_qty
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Scrap Item {1} produced cannot be less than quantity delivered."
|
||||
"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Secondary Item {1} produced cannot be less than quantity delivered."
|
||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||
)
|
||||
else:
|
||||
@@ -648,8 +654,8 @@ class SubcontractingInwardController:
|
||||
for item in self.items:
|
||||
doctype = (
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.is_scrap_item
|
||||
else "Subcontracting Inward Order Scrap Item"
|
||||
if not item.type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
doctype,
|
||||
@@ -763,7 +769,11 @@ class SubcontractingInwardController:
|
||||
customer_warehouse = frappe.get_cached_value(
|
||||
"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
|
||||
)
|
||||
items = [item for item in self.items if not item.is_finished_item and not item.is_scrap_item]
|
||||
items = [
|
||||
item
|
||||
for item in self.items
|
||||
if not item.is_finished_item and not item.type and not item.is_legacy_scrap_item
|
||||
]
|
||||
item_code_wh = frappe._dict(
|
||||
{
|
||||
(
|
||||
@@ -860,24 +870,24 @@ class SubcontractingInwardController:
|
||||
doc.insert()
|
||||
doc.submit()
|
||||
|
||||
def update_inward_order_scrap_items(self):
|
||||
def update_inward_order_secondary_items(self):
|
||||
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
|
||||
scrap_items_list = [item for item in self.items if item.is_scrap_item]
|
||||
scrap_items = frappe._dict(
|
||||
{
|
||||
(item.item_code, item.t_warehouse): item.transfer_qty
|
||||
if self._action == "submit"
|
||||
else -item.transfer_qty
|
||||
for item in scrap_items_list
|
||||
}
|
||||
)
|
||||
if scrap_items:
|
||||
item_codes, warehouses = zip(*list(scrap_items.keys()), strict=True)
|
||||
secondary_items_list = [item for item in self.items if item.type or item.is_legacy_scrap_item]
|
||||
|
||||
secondary_items = defaultdict(float)
|
||||
for item in secondary_items_list:
|
||||
secondary_items[(item.item_code, item.t_warehouse)] += (
|
||||
item.transfer_qty if self._action == "submit" else -item.transfer_qty
|
||||
)
|
||||
secondary_items = frappe._dict(secondary_items)
|
||||
|
||||
if secondary_items:
|
||||
item_codes, warehouses = zip(*list(secondary_items.keys()), strict=True)
|
||||
item_codes = list(item_codes)
|
||||
warehouses = list(warehouses)
|
||||
|
||||
result = frappe.get_all(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
filters={
|
||||
"item_code": ["in", item_codes],
|
||||
"warehouse": ["in", warehouses],
|
||||
@@ -890,7 +900,7 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
if result:
|
||||
scrap_item_dict = frappe._dict(
|
||||
secondary_items_dict = frappe._dict(
|
||||
{
|
||||
(d.item_code, d.warehouse): frappe._dict(
|
||||
{"name": d.name, "produced_qty": d.produced_qty}
|
||||
@@ -900,40 +910,45 @@ class SubcontractingInwardController:
|
||||
)
|
||||
deleted_docs = []
|
||||
case_expr = Case()
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Scrap Item")
|
||||
for key, value in scrap_item_dict.items():
|
||||
if self._action == "cancel" and value.produced_qty - abs(scrap_items.get(key)) == 0:
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
|
||||
for key, value in secondary_items_dict.items():
|
||||
if (
|
||||
self._action == "cancel"
|
||||
and value.produced_qty - abs(secondary_items.get(key)) == 0
|
||||
):
|
||||
deleted_docs.append(value.name)
|
||||
frappe.delete_doc("Subcontracting Inward Order Scrap Item", value.name)
|
||||
frappe.delete_doc("Subcontracting Inward Order Secondary Item", value.name)
|
||||
else:
|
||||
case_expr = case_expr.when(
|
||||
table.name == value.name, value.produced_qty + scrap_items.get(key)
|
||||
table.name == value.name, value.produced_qty + secondary_items.get(key)
|
||||
)
|
||||
|
||||
if final_list := list(
|
||||
set([v.name for v in scrap_item_dict.values()]) - set(deleted_docs)
|
||||
set([v.name for v in secondary_items_dict.values()]) - set(deleted_docs)
|
||||
):
|
||||
frappe.qb.update(table).set(table.produced_qty, case_expr).where(
|
||||
(table.name.isin(final_list)) & (table.docstatus == 1)
|
||||
).run()
|
||||
|
||||
fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
|
||||
for scrap_item in [
|
||||
for secondary_item in [
|
||||
item
|
||||
for item in scrap_items_list
|
||||
for item in secondary_items_list
|
||||
if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result]
|
||||
]:
|
||||
doc = frappe.new_doc(
|
||||
"Subcontracting Inward Order Scrap Item",
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
parent=scio,
|
||||
parenttype="Subcontracting Inward Order",
|
||||
parentfield="scrap_items",
|
||||
idx=frappe.db.count("Subcontracting Inward Order Scrap Item", {"parent": scio}) + 1,
|
||||
item_code=scrap_item.item_code,
|
||||
parentfield="secondary_items",
|
||||
idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio})
|
||||
+ 1,
|
||||
item_code=secondary_item.item_code,
|
||||
fg_item_code=fg_item_code,
|
||||
stock_uom=scrap_item.stock_uom,
|
||||
warehouse=scrap_item.t_warehouse,
|
||||
produced_qty=scrap_item.transfer_qty,
|
||||
stock_uom=secondary_item.stock_uom,
|
||||
warehouse=secondary_item.t_warehouse,
|
||||
produced_qty=secondary_item.transfer_qty,
|
||||
type=secondary_item.type,
|
||||
delivered_qty=0,
|
||||
reference_name=frappe.get_value(
|
||||
"Work Order", self.work_order, "subcontracting_inward_order_item"
|
||||
@@ -965,7 +980,7 @@ class SubcontractingInwardController:
|
||||
and (
|
||||
not frappe.db.exists("Subcontracting Inward Order Received Item", item.scio_detail)
|
||||
and not frappe.db.exists("Subcontracting Inward Order Item", item.scio_detail)
|
||||
and not frappe.db.exists("Subcontracting Inward Order Scrap Item", item.scio_detail)
|
||||
and not frappe.db.exists("Subcontracting Inward Order Secondary Item", item.scio_detail)
|
||||
)
|
||||
]
|
||||
for item in items:
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.controllers.subcontracting_controller import (
|
||||
@@ -500,8 +500,8 @@ class TestSubcontractingController(IntegrationTestCase):
|
||||
scr1.items[0].qty = 2
|
||||
add_second_row_in_scr(scr1)
|
||||
scr1.flags.ignore_mandatory = True
|
||||
scr1.save()
|
||||
scr1.set_missing_values()
|
||||
scr1.save()
|
||||
scr1.submit()
|
||||
|
||||
for _key, value in get_supplied_items(scr1).items():
|
||||
@@ -512,8 +512,8 @@ class TestSubcontractingController(IntegrationTestCase):
|
||||
scr2.items[0].qty = 2
|
||||
add_second_row_in_scr(scr2)
|
||||
scr2.flags.ignore_mandatory = True
|
||||
scr2.save()
|
||||
scr2.set_missing_values()
|
||||
scr2.save()
|
||||
scr2.submit()
|
||||
|
||||
for _key, value in get_supplied_items(scr2).items():
|
||||
@@ -522,8 +522,8 @@ class TestSubcontractingController(IntegrationTestCase):
|
||||
scr3 = make_subcontracting_receipt(sco.name)
|
||||
scr3.items[0].qty = 2
|
||||
scr3.flags.ignore_mandatory = True
|
||||
scr3.save()
|
||||
scr3.set_missing_values()
|
||||
scr3.save()
|
||||
scr3.submit()
|
||||
|
||||
for _key, value in get_supplied_items(scr3).items():
|
||||
@@ -1162,6 +1162,54 @@ class TestSubcontractingController(IntegrationTestCase):
|
||||
|
||||
self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected)
|
||||
|
||||
def test_co_by_product(self):
|
||||
frappe.set_value("UOM", "Nos", "must_be_whole_number", 0)
|
||||
|
||||
fg_item = make_item("FG Item", properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||
rm_item = make_item("RM Item", properties={"is_stock_item": 1}).name
|
||||
scrap_item = make_item("Scrap Item", properties={"is_stock_item": 1}).name
|
||||
make_bom(
|
||||
item=fg_item, raw_materials=[rm_item], scrap_items=[scrap_item], process_loss_percentage=10
|
||||
).name
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 11",
|
||||
"qty": 5,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 5,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
scr1 = make_subcontracting_receipt(sco.name)
|
||||
scr1.get_secondary_items()
|
||||
scr1.save()
|
||||
|
||||
self.assertEqual(scr1.items[0].received_qty, 5)
|
||||
self.assertEqual(scr1.items[0].process_loss_qty, 0.5)
|
||||
self.assertEqual(scr1.items[0].qty, 4.5)
|
||||
self.assertEqual(scr1.items[0].rate, 200)
|
||||
self.assertEqual(scr1.items[0].amount, 900)
|
||||
|
||||
self.assertEqual(scr1.items[1].item_code, scrap_item)
|
||||
self.assertEqual(scr1.items[1].received_qty, 5)
|
||||
self.assertEqual(scr1.items[1].process_loss_qty, 0.5)
|
||||
self.assertEqual(scr1.items[1].qty, 4.5)
|
||||
self.assertEqual(flt(scr1.items[1].rate, 3), 11.111)
|
||||
self.assertEqual(scr1.items[1].amount, 50)
|
||||
|
||||
frappe.set_value("UOM", "Nos", "must_be_whole_number", 1)
|
||||
|
||||
|
||||
def add_second_row_in_scr(scr):
|
||||
item_dict = {}
|
||||
|
||||
Reference in New Issue
Block a user