Compare commits
3 Commits
develop
...
new_mr_sc_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bcfce5663 | ||
|
|
da37e29798 | ||
|
|
080cd0cecf |
@@ -402,13 +402,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
me.make_subcontracting_order();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
if (!doc.items.every((item) => item.qty == item.sco_qty)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
me.make_subcontracting_order();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ class PurchaseOrder(BuyingController):
|
||||
if self.is_against_so():
|
||||
self.update_status_updater()
|
||||
|
||||
self.update_prevdoc_status()
|
||||
self.update_prevdoc_status(source_field="fg_item_qty")
|
||||
if not self.is_subcontracted or self.is_old_subcontracting_flow:
|
||||
self.update_requested_qty()
|
||||
|
||||
@@ -871,27 +871,40 @@ def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_order(source_name, target_doc=None, save=False, submit=False, notify=False):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
if not is_po_fully_subcontracted(source_name):
|
||||
target_doc = get_mapped_subcontracting_order(source_name, target_doc)
|
||||
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
if (save or submit) and frappe.has_permission(target_doc.doctype, "create"):
|
||||
target_doc.save()
|
||||
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc):
|
||||
try:
|
||||
target_doc.submit()
|
||||
except Exception as e:
|
||||
target_doc.add_comment("Comment", _("Submit Action Failed") + "<br><br>" + str(e))
|
||||
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
if notify:
|
||||
frappe.msgprint(
|
||||
_("Subcontracting Order {0} created.").format(
|
||||
get_link_to_form(target_doc.doctype, target_doc.name)
|
||||
),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
|
||||
return target_doc
|
||||
return target_doc
|
||||
else:
|
||||
frappe.throw(_("This PO has been fully subcontracted."))
|
||||
|
||||
|
||||
def is_po_fully_subcontracted(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.sco_qty))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
@@ -943,7 +956,8 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": [],
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.sco_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
@@ -951,12 +965,3 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_subcontracting_order_created(po_name) -> bool:
|
||||
return (
|
||||
True
|
||||
if frappe.db.exists("Subcontracting Order", {"purchase_order": po_name, "docstatus": ["=", 1]})
|
||||
else False
|
||||
)
|
||||
|
||||
@@ -1025,7 +1025,7 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
)
|
||||
|
||||
def update_items(po, qty):
|
||||
trans_items = [po.items[0].as_dict()]
|
||||
trans_items = [po.items[0].as_dict().update({"docname": po.items[0].name})]
|
||||
trans_items[0]["qty"] = qty
|
||||
trans_items[0]["fg_item_qty"] = qty
|
||||
trans_items = json.dumps(trans_items, default=str)
|
||||
@@ -1080,6 +1080,73 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertEqual(po.items[0].qty, 30)
|
||||
self.assertEqual(po.items[0].fg_item_qty, 30)
|
||||
|
||||
def test_new_sc_flow(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_subcontracting_order
|
||||
|
||||
po = create_po_for_sc_testing()
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
sco.items[0].qty = 5
|
||||
sco.items.pop(1)
|
||||
sco.items[1].qty = 25
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 1: Quantity of Service Items should change based on change in Quantity of its corresponding Finished Goods Item
|
||||
self.assertEqual(sco.service_items[0].qty, 5)
|
||||
|
||||
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].sco_qty, 5)
|
||||
self.assertEqual(po.items[1].sco_qty, 0)
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
|
||||
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
|
||||
self.assertEqual(sco.items[0].amount, 2000)
|
||||
self.assertEqual(sco.service_items[0].amount, 500)
|
||||
|
||||
# Test - 4: Service Items should be removed if its corresponding Finished Good line item is deleted
|
||||
self.assertEqual(len(sco.service_items), 2)
|
||||
|
||||
# Test - 5: Service Item quantity calculation should be based upon conversion factor calculated from its corresponding PO Item
|
||||
self.assertEqual(sco.service_items[1].qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
sco.items[0].qty = 6
|
||||
|
||||
# Test - 6: Saving document should not be allowed if Quantity exceeds available Subcontracting Quantity of any Purchase Order Item
|
||||
self.assertRaises(frappe.ValidationError, sco.save)
|
||||
|
||||
sco.items[0].qty = 5
|
||||
sco.items.pop()
|
||||
sco.items.pop()
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
|
||||
# Test - 7: Since line item 1 is now fully subcontracted, new SCO should by default only have the remaining 2 line items
|
||||
self.assertEqual(len(sco.items), 2)
|
||||
|
||||
sco.items.pop(0)
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 25)
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].sco_qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
sco.save()
|
||||
sco.submit()
|
||||
|
||||
# Test - 8: Since this PO is now fully subcontracted, creating a new SCO from it should throw error
|
||||
self.assertRaises(frappe.ValidationError, make_subcontracting_order, po.name)
|
||||
|
||||
@IntegrationTestCase.change_settings("Buying Settings", {"auto_create_subcontracting_order": 1})
|
||||
def test_auto_create_subcontracting_order(self):
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
@@ -1173,6 +1240,53 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
self.assertEqual(po.per_billed, 100)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
make_bom_for_subcontracted_items,
|
||||
make_raw_materials,
|
||||
make_service_items,
|
||||
make_subcontracted_items,
|
||||
)
|
||||
|
||||
make_subcontracted_items()
|
||||
make_raw_materials()
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA1",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 2",
|
||||
"qty": 20,
|
||||
"rate": 25,
|
||||
"fg_item": "Subcontracted Item SA2",
|
||||
"fg_item_qty": 15,
|
||||
},
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 3",
|
||||
"qty": 25,
|
||||
"rate": 10,
|
||||
"fg_item": "Subcontracted Item SA3",
|
||||
"fg_item_qty": 50,
|
||||
},
|
||||
]
|
||||
|
||||
return create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-05-24 19:29:06",
|
||||
"creation": "2024-12-09 12:54:24.652161",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
@@ -26,6 +26,7 @@
|
||||
"quantity_and_rate",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"sco_qty",
|
||||
"col_break2",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
@@ -929,13 +930,21 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Distributed Discount Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "sco_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-02 06:20:10.508290",
|
||||
"modified": "2024-12-10 12:11:18.536089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -52,6 +52,7 @@ class PurchaseOrderItem(Document):
|
||||
item_name: DF.Data
|
||||
item_tax_rate: DF.Code | None
|
||||
item_tax_template: DF.Link | None
|
||||
job_card: DF.Link | None
|
||||
last_purchase_rate: DF.Currency
|
||||
manufacturer: DF.Link | None
|
||||
manufacturer_part_no: DF.Data | None
|
||||
@@ -81,6 +82,7 @@ class PurchaseOrderItem(Document):
|
||||
sales_order_item: DF.Data | None
|
||||
sales_order_packed_item: DF.Data | None
|
||||
schedule_date: DF.Date
|
||||
sco_qty: DF.Float
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
stock_uom_rate: DF.Currency
|
||||
|
||||
@@ -149,7 +149,8 @@ class BuyingController(SubcontractingController):
|
||||
self.set_missing_item_details(for_validate)
|
||||
|
||||
def set_supplier_from_item_default(self):
|
||||
if self.meta.get_field("supplier") and not self.supplier:
|
||||
is_scpo_from_mr = all([item.material_request for item in self.items]) if self.doctype == "Purchase Order" else False
|
||||
if self.meta.get_field("supplier") and not self.supplier and (frappe.flags.args.default_supplier if is_scpo_from_mr else True):
|
||||
for d in self.get("items"):
|
||||
supplier = frappe.db.get_value(
|
||||
"Item Default", {"parent": d.item_code, "company": self.company}, "default_supplier"
|
||||
|
||||
@@ -171,7 +171,11 @@ class StatusUpdater(Document):
|
||||
Installation Note: Update Installed Qty, Update Percent Qty and Validate over installation
|
||||
"""
|
||||
|
||||
def update_prevdoc_status(self):
|
||||
def update_prevdoc_status(self, source_field = None):
|
||||
if source_field:
|
||||
for item in self.status_updater:
|
||||
item.update({"source_field": source_field})
|
||||
|
||||
self.update_qty()
|
||||
self.validate_qty()
|
||||
|
||||
|
||||
@@ -103,6 +103,19 @@ class SubcontractingController(StockController):
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
)
|
||||
|
||||
if (
|
||||
self.doctype not in "Subcontracting Receipt"
|
||||
and item.qty
|
||||
> flt(get_pending_sco_qty(self.purchase_order).get(item.purchase_order_item))
|
||||
/ item.sc_conversion_factor
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
|
||||
).format(item.idx, item.item_name)
|
||||
)
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if item.bom:
|
||||
is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
|
||||
|
||||
@@ -1116,6 +1129,12 @@ def get_item_details(items):
|
||||
return item_details
|
||||
|
||||
|
||||
def get_pending_sco_qty(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
query = frappe.qb.from_(table).select(table.name, table.qty, table.sco_qty).where(table.parent == po_name)
|
||||
return {item.name: item.qty - item.sco_qty for item in query.run(as_dict=True)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(
|
||||
subcontract_order, rm_items=None, order_doctype="Subcontracting Order", target_doc=None
|
||||
|
||||
@@ -1261,6 +1261,7 @@ def make_raw_materials():
|
||||
for item, properties in raw_materials.items():
|
||||
if not frappe.db.exists("Item", item):
|
||||
properties.update({"is_stock_item": 1})
|
||||
properties.update({"valuation_rate": 100})
|
||||
make_item(item, properties)
|
||||
|
||||
|
||||
@@ -1311,7 +1312,7 @@ def make_bom_for_subcontracted_items():
|
||||
|
||||
for item_code, raw_materials in boms.items():
|
||||
if not frappe.db.exists("BOM", {"item": item_code}):
|
||||
make_bom(item=item_code, raw_materials=raw_materials, rate=100)
|
||||
make_bom(item=item_code, raw_materials=raw_materials, rate=100, currency="INR")
|
||||
|
||||
|
||||
def set_backflush_based_on(based_on):
|
||||
|
||||
@@ -122,6 +122,12 @@ frappe.ui.form.on("Material Request", {
|
||||
() => frm.events.make_purchase_order(frm),
|
||||
__("Create")
|
||||
);
|
||||
} else if (frm.doc.material_request_type === "Subcontracting") {
|
||||
frm.add_custom_button(
|
||||
__("Subcontracted Purchase Order"),
|
||||
() => frm.events.make_purchase_order(frm),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Purpose",
|
||||
"options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided",
|
||||
"options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nSubcontracting\nCustomer Provided",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -357,7 +357,7 @@
|
||||
"idx": 70,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:04.971211",
|
||||
"modified": "2024-12-16 12:46:02.262167",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request",
|
||||
|
||||
@@ -19,6 +19,10 @@ from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty
|
||||
|
||||
from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import (
|
||||
get_subcontracting_boms_for_finished_goods,
|
||||
)
|
||||
|
||||
form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"}
|
||||
|
||||
|
||||
@@ -29,9 +33,8 @@ class MaterialRequest(BuyingController):
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.stock.doctype.material_request_item.material_request_item import MaterialRequestItem
|
||||
from frappe.types import DF
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
@@ -39,9 +42,7 @@ class MaterialRequest(BuyingController):
|
||||
items: DF.Table[MaterialRequestItem]
|
||||
job_card: DF.Link | None
|
||||
letter_head: DF.Link | None
|
||||
material_request_type: DF.Literal[
|
||||
"Purchase", "Material Transfer", "Material Issue", "Manufacture", "Customer Provided"
|
||||
]
|
||||
material_request_type: DF.Literal["Purchase", "Material Transfer", "Material Issue", "Manufacture", "Subcontracting", "Customer Provided"]
|
||||
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
|
||||
per_ordered: DF.Percent
|
||||
per_received: DF.Percent
|
||||
@@ -50,20 +51,7 @@ class MaterialRequest(BuyingController):
|
||||
select_print_heading: DF.Link | None
|
||||
set_from_warehouse: DF.Link | None
|
||||
set_warehouse: DF.Link | None
|
||||
status: DF.Literal[
|
||||
"",
|
||||
"Draft",
|
||||
"Submitted",
|
||||
"Stopped",
|
||||
"Cancelled",
|
||||
"Pending",
|
||||
"Partially Ordered",
|
||||
"Partially Received",
|
||||
"Ordered",
|
||||
"Issued",
|
||||
"Transferred",
|
||||
"Received",
|
||||
]
|
||||
status: DF.Literal["", "Draft", "Submitted", "Stopped", "Cancelled", "Pending", "Partially Ordered", "Partially Received", "Ordered", "Issued", "Transferred", "Received"]
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
title: DF.Data | None
|
||||
@@ -385,6 +373,17 @@ def update_item(obj, target, source_parent):
|
||||
if getdate(target.schedule_date) < getdate(nowdate()):
|
||||
target.schedule_date = None
|
||||
|
||||
if target.fg_item:
|
||||
target.fg_item_qty = obj.stock_qty
|
||||
if sc_bom := get_subcontracting_boms_for_finished_goods(target.fg_item):
|
||||
target.item_code = sc_bom.service_item
|
||||
target.uom = sc_bom.service_item_uom
|
||||
target.conversion_factor = frappe.db.get_value("UOM Conversion Detail", {"parent": sc_bom.service_item, "uom": sc_bom.service_item_uom}, "conversion_factor")
|
||||
target.qty = target.fg_item_qty * sc_bom.conversion_factor
|
||||
target.stock_qty = target.qty / sc_bom.conversion_factor
|
||||
else:
|
||||
target.uom = "Nos"
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||
@@ -416,11 +415,16 @@ def make_purchase_order(source_name, target_doc=None, args=None):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
is_subcontracted = frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting"
|
||||
|
||||
def postprocess(source, target_doc):
|
||||
target_doc.is_subcontracted = is_subcontracted
|
||||
if frappe.flags.args and frappe.flags.args.default_supplier:
|
||||
# items only for given default supplier
|
||||
supplier_items = []
|
||||
for d in target_doc.items:
|
||||
if not d.item_code:
|
||||
continue
|
||||
default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier")
|
||||
if frappe.flags.args.default_supplier == default_supplier:
|
||||
supplier_items.append(d)
|
||||
@@ -442,19 +446,22 @@ def make_purchase_order(source_name, target_doc=None, args=None):
|
||||
{
|
||||
"Material Request": {
|
||||
"doctype": "Purchase Order",
|
||||
"validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]},
|
||||
"validation": {"docstatus": ["=", 1], "material_request_type": ["in", ["Purchase", "Subcontracting"]]},
|
||||
},
|
||||
"Material Request Item": {
|
||||
"doctype": "Purchase Order Item",
|
||||
"field_map": [
|
||||
"field_map": [item for item in [
|
||||
["name", "material_request_item"],
|
||||
["parent", "material_request"],
|
||||
["uom", "stock_uom"],
|
||||
["uom", "uom"],
|
||||
["qty", "fg_item_qty"] if is_subcontracted else [],
|
||||
["item_code", "fg_item"] if is_subcontracted else [],
|
||||
["uom", "stock_uom"] if not is_subcontracted else [],
|
||||
["uom", "uom"] if not is_subcontracted else [],
|
||||
["sales_order", "sales_order"],
|
||||
["sales_order_item", "sales_order_item"],
|
||||
["wip_composite_asset", "wip_composite_asset"],
|
||||
],
|
||||
] if item], # this list comprehension will remove all empty lists
|
||||
"field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [],
|
||||
"postprocess": update_item,
|
||||
"condition": select_item,
|
||||
},
|
||||
|
||||
@@ -5,10 +5,38 @@ frappe.provide("erpnext.buying");
|
||||
|
||||
erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Order");
|
||||
|
||||
// client script for Subcontracting Order Item is not necessarily required as the server side code will do everything that is necessary.
|
||||
// this is just so that the user does not get potentially confused
|
||||
frappe.ui.form.on("Subcontracting Order Item", {
|
||||
qty(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "amount", row.qty * row.rate);
|
||||
const service_item = frm.doc.service_items[row.idx - 1];
|
||||
frappe.model.set_value(
|
||||
service_item.doctype,
|
||||
service_item.name,
|
||||
"qty",
|
||||
row.qty * row.sc_conversion_factor
|
||||
);
|
||||
frappe.model.set_value(service_item.doctype, service_item.name, "fg_item_qty", row.qty);
|
||||
frappe.model.set_value(
|
||||
service_item.doctype,
|
||||
service_item.name,
|
||||
"amount",
|
||||
row.qty * row.sc_conversion_factor * service_item.rate
|
||||
);
|
||||
},
|
||||
before_items_remove(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
frm.toggle_enable(["service_items"], true);
|
||||
frm.get_field("service_items").grid.grid_rows[row.idx - 1].remove();
|
||||
frm.toggle_enable(["service_items"], false);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Subcontracting Order", {
|
||||
setup: (frm) => {
|
||||
frm.get_field("items").grid.cannot_add_rows = true;
|
||||
frm.get_field("items").grid.only_sortable();
|
||||
frm.trigger("set_queries");
|
||||
|
||||
frm.set_indicator_formatter("item_code", (doc) => (doc.qty <= doc.received_qty ? "green" : "orange"));
|
||||
|
||||
@@ -6,7 +6,6 @@ from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.stock_balance import update_bin_qty
|
||||
@@ -120,20 +119,15 @@ class SubcontractingOrder(SubcontractingController):
|
||||
def on_submit(self):
|
||||
self.update_prevdoc_status()
|
||||
self.update_status()
|
||||
self.update_sco_qty_in_po()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_prevdoc_status()
|
||||
self.update_status()
|
||||
self.update_sco_qty_in_po(cancel=True)
|
||||
|
||||
def validate_purchase_order_for_subcontracting(self):
|
||||
if self.purchase_order:
|
||||
if is_subcontracting_order_created(self.purchase_order):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one."
|
||||
)
|
||||
)
|
||||
|
||||
po = frappe.get_doc("Purchase Order", self.purchase_order)
|
||||
|
||||
if not po.is_subcontracted:
|
||||
@@ -154,10 +148,23 @@ class SubcontractingOrder(SubcontractingController):
|
||||
frappe.throw(_("Please select a Subcontracting Purchase Order."))
|
||||
|
||||
def validate_service_items(self):
|
||||
for item in self.service_items:
|
||||
if frappe.get_value("Item", item.item_code, "is_stock_item"):
|
||||
msg = f"Service Item {item.item_name} must be a non-stock item."
|
||||
frappe.throw(_(msg))
|
||||
purchase_order_items = [item.purchase_order_item for item in self.items]
|
||||
self.service_items = [
|
||||
service_item
|
||||
for service_item in self.service_items
|
||||
if service_item.purchase_order_item in purchase_order_items
|
||||
]
|
||||
|
||||
for service_item in self.service_items:
|
||||
if frappe.get_value("Item", service_item.item_code, "is_stock_item"):
|
||||
frappe.throw(_("Service Item {0} must be a non-stock item.").format(service_item.item_code))
|
||||
|
||||
item = next(
|
||||
item for item in self.items if item.purchase_order_item == service_item.purchase_order_item
|
||||
)
|
||||
service_item.qty = item.qty * item.sc_conversion_factor
|
||||
service_item.fg_item_qty = item.qty
|
||||
service_item.amount = service_item.qty * service_item.rate
|
||||
|
||||
def validate_supplied_items(self):
|
||||
if self.supplier_warehouse:
|
||||
@@ -241,6 +248,18 @@ class SubcontractingOrder(SubcontractingController):
|
||||
for si in self.service_items:
|
||||
if si.fg_item:
|
||||
item = frappe.get_doc("Item", si.fg_item)
|
||||
|
||||
po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item)
|
||||
available_qty = po_item.qty - po_item.sco_qty
|
||||
|
||||
if available_qty == 0:
|
||||
continue
|
||||
|
||||
si.qty = available_qty
|
||||
conversion_factor = po_item.qty / po_item.fg_item_qty
|
||||
si.fg_item_qty = available_qty / conversion_factor
|
||||
si.amount = available_qty * si.rate
|
||||
|
||||
bom = (
|
||||
frappe.db.get_value(
|
||||
"Subcontracting BOM",
|
||||
@@ -257,6 +276,7 @@ class SubcontractingOrder(SubcontractingController):
|
||||
"schedule_date": self.schedule_date,
|
||||
"description": item.description,
|
||||
"qty": si.fg_item_qty,
|
||||
"sc_conversion_factor": conversion_factor,
|
||||
"stock_uom": item.stock_uom,
|
||||
"bom": bom,
|
||||
"purchase_order_item": si.purchase_order_item,
|
||||
@@ -310,6 +330,12 @@ class SubcontractingOrder(SubcontractingController):
|
||||
self.update_ordered_qty_for_subcontracting()
|
||||
self.update_reserved_qty_for_subcontracting()
|
||||
|
||||
def update_sco_qty_in_po(self, cancel=False):
|
||||
for service_item in self.service_items:
|
||||
doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item)
|
||||
doc.sco_qty = (doc.sco_qty + service_item.qty) if not cancel else (doc.sco_qty - service_item.qty)
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_receipt(source_name, target_doc=None):
|
||||
|
||||
@@ -49,12 +49,6 @@ class TestSubcontractingOrder(IntegrationTestCase):
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
def test_populate_items_table(self):
|
||||
sco = get_subcontracting_order()
|
||||
sco.items = None
|
||||
sco.populate_items_table()
|
||||
self.assertEqual(len(sco.service_items), len(sco.items))
|
||||
|
||||
def test_set_missing_values(self):
|
||||
sco = get_subcontracting_order()
|
||||
before = {sco.total_qty, sco.total, sco.total_additional_costs}
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
"column_break_nfod",
|
||||
"section_break_34",
|
||||
"purchase_order_item",
|
||||
"page_break"
|
||||
"page_break",
|
||||
"sc_conversion_factor"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -147,8 +148,8 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"print_width": "60px",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "60px"
|
||||
},
|
||||
@@ -400,13 +401,20 @@
|
||||
{
|
||||
"fieldname": "column_break_nfod",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sc_conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "SC Conversion Factor",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-06 15:23:05.252346",
|
||||
"modified": "2024-12-13 13:35:28.935898",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Item",
|
||||
|
||||
@@ -42,6 +42,7 @@ class SubcontractingOrderItem(Document):
|
||||
received_qty: DF.Float
|
||||
returned_qty: DF.Float
|
||||
rm_cost_per_qty: DF.Currency
|
||||
sc_conversion_factor: DF.Float
|
||||
schedule_date: DF.Date | None
|
||||
service_cost_per_qty: DF.Currency
|
||||
stock_uom: DF.Link
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:46.534662",
|
||||
"modified": "2024-12-05 17:33:46.099601",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Order Service Item",
|
||||
|
||||
@@ -19,6 +19,8 @@ class SubcontractingOrderServiceItem(Document):
|
||||
fg_item_qty: DF.Float
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data
|
||||
material_request: DF.Link | None
|
||||
material_request_item: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
Reference in New Issue
Block a user