[item-variants] allow production order of variant #2224
This commit is contained in:
parent
1a7637459c
commit
baef96b5a2
@ -1,6 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"doctype": "Pricing Rule",
|
|
||||||
"name": "_Test Pricing Rule 1"
|
|
||||||
}
|
|
||||||
]
|
|
@ -197,27 +197,14 @@ class BOM(Document):
|
|||||||
if self.with_operations and cstr(m.operation_no) not in self.op:
|
if self.with_operations and cstr(m.operation_no) not in self.op:
|
||||||
frappe.throw(_("Operation {0} not present in Operations Table").format(m.operation_no))
|
frappe.throw(_("Operation {0} not present in Operations Table").format(m.operation_no))
|
||||||
|
|
||||||
if m.bom:
|
if m.bom_no:
|
||||||
self.validate_bom_no(m.item_code, m.bom_no, m.idx)
|
validate_bom_no(m.item_code, m.bom_no)
|
||||||
|
|
||||||
if flt(m.qty) <= 0:
|
if flt(m.qty) <= 0:
|
||||||
frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx))
|
frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx))
|
||||||
|
|
||||||
self.check_if_item_repeated(m.item_code, m.operation_no, check_list)
|
self.check_if_item_repeated(m.item_code, m.operation_no, check_list)
|
||||||
|
|
||||||
def validate_bom_no(self, item, bom_no, idx):
|
|
||||||
"""Validate BOM No of sub-contracted items"""
|
|
||||||
bom = frappe.get_doc("BOM", bom_no)
|
|
||||||
if not bom.is_active:
|
|
||||||
frappe.throw(_("BOM {0} must be active").format(bom_no))
|
|
||||||
if not bom.docstatus!=1:
|
|
||||||
frappe.throw(_("BOM {0} must be submitted").format(bom_no))
|
|
||||||
|
|
||||||
bom = frappe.db.sql("""select name from `tabBOM` where name = %s and item = %s
|
|
||||||
and is_active=1 and docstatus=1""",
|
|
||||||
(bom_no, item), as_dict =1)
|
|
||||||
if not bom:
|
|
||||||
frappe.throw(_("BOM {0} for Item {1} in row {2} is inactive or not submitted").format(bom_no, item, idx))
|
|
||||||
|
|
||||||
def check_if_item_repeated(self, item, op, check_list):
|
def check_if_item_repeated(self, item, op, check_list):
|
||||||
if [cstr(item), cstr(op)] in check_list:
|
if [cstr(item), cstr(op)] in check_list:
|
||||||
@ -425,3 +412,16 @@ def get_bom_items(bom, qty=1, fetch_exploded=1):
|
|||||||
items = get_bom_items_as_dict(bom, qty, fetch_exploded).values()
|
items = get_bom_items_as_dict(bom, qty, fetch_exploded).values()
|
||||||
items.sort(lambda a, b: a.item_code > b.item_code and 1 or -1)
|
items.sort(lambda a, b: a.item_code > b.item_code and 1 or -1)
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
def validate_bom_no(item, bom_no):
|
||||||
|
"""Validate BOM No of sub-contracted items"""
|
||||||
|
bom = frappe.get_doc("BOM", bom_no)
|
||||||
|
if not bom.is_active:
|
||||||
|
frappe.throw(_("BOM {0} must be active").format(bom_no))
|
||||||
|
if not bom.docstatus!=1:
|
||||||
|
if not getattr(frappe.flags, "in_test", False):
|
||||||
|
frappe.throw(_("BOM {0} must be submitted").format(bom_no))
|
||||||
|
if item and not (bom.item == item or \
|
||||||
|
bom.item == frappe.db.get_value("Item", item, "variant_of")):
|
||||||
|
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
|
||||||
|
|
||||||
|
@ -2,98 +2,128 @@
|
|||||||
{
|
{
|
||||||
"bom_materials": [
|
"bom_materials": [
|
||||||
{
|
{
|
||||||
"amount": 5000.0,
|
"amount": 5000.0,
|
||||||
"doctype": "BOM Item",
|
"doctype": "BOM Item",
|
||||||
"item_code": "_Test Serialized Item With Series",
|
"item_code": "_Test Serialized Item With Series",
|
||||||
"parentfield": "bom_materials",
|
"parentfield": "bom_materials",
|
||||||
"qty": 1.0,
|
"qty": 1.0,
|
||||||
"rate": 5000.0,
|
"rate": 5000.0,
|
||||||
"stock_uom": "_Test UOM"
|
"stock_uom": "_Test UOM"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 2000.0,
|
"amount": 2000.0,
|
||||||
"doctype": "BOM Item",
|
"doctype": "BOM Item",
|
||||||
"item_code": "_Test Item 2",
|
"item_code": "_Test Item 2",
|
||||||
"parentfield": "bom_materials",
|
"parentfield": "bom_materials",
|
||||||
"qty": 2.0,
|
"qty": 2.0,
|
||||||
"rate": 1000.0,
|
"rate": 1000.0,
|
||||||
"stock_uom": "_Test UOM"
|
"stock_uom": "_Test UOM"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
"doctype": "BOM",
|
"doctype": "BOM",
|
||||||
"is_active": 1,
|
"is_active": 1,
|
||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"item": "_Test Item Home Desktop Manufactured",
|
"item": "_Test Item Home Desktop Manufactured",
|
||||||
"quantity": 1.0
|
"quantity": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bom_materials": [
|
"bom_materials": [
|
||||||
{
|
{
|
||||||
"amount": 5000.0,
|
"amount": 5000.0,
|
||||||
"doctype": "BOM Item",
|
"doctype": "BOM Item",
|
||||||
"item_code": "_Test Item",
|
"item_code": "_Test Item",
|
||||||
"parentfield": "bom_materials",
|
"parentfield": "bom_materials",
|
||||||
"qty": 1.0,
|
"qty": 1.0,
|
||||||
"rate": 5000.0,
|
"rate": 5000.0,
|
||||||
"stock_uom": "_Test UOM"
|
"stock_uom": "_Test UOM"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amount": 2000.0,
|
"amount": 2000.0,
|
||||||
"doctype": "BOM Item",
|
"doctype": "BOM Item",
|
||||||
"item_code": "_Test Item Home Desktop 100",
|
"item_code": "_Test Item Home Desktop 100",
|
||||||
"parentfield": "bom_materials",
|
"parentfield": "bom_materials",
|
||||||
"qty": 2.0,
|
"qty": 2.0,
|
||||||
"rate": 1000.0,
|
"rate": 1000.0,
|
||||||
"stock_uom": "_Test UOM"
|
"stock_uom": "_Test UOM"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
"doctype": "BOM",
|
"doctype": "BOM",
|
||||||
"is_active": 1,
|
"is_active": 1,
|
||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"item": "_Test FG Item",
|
"item": "_Test FG Item",
|
||||||
"quantity": 1.0
|
"quantity": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"bom_operations": [
|
"bom_operations": [
|
||||||
{
|
{
|
||||||
"operation_no": "1",
|
"operation_no": "1",
|
||||||
"opn_description": "_Test",
|
"opn_description": "_Test",
|
||||||
"workstation": "_Test Workstation 1",
|
"workstation": "_Test Workstation 1",
|
||||||
"time_in_min": 60,
|
"time_in_min": 60,
|
||||||
"operating_cost": 100
|
"operating_cost": 100
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"bom_materials": [
|
"bom_materials": [
|
||||||
{
|
{
|
||||||
"operation_no": 1,
|
"operation_no": 1,
|
||||||
"amount": 5000.0,
|
"amount": 5000.0,
|
||||||
"doctype": "BOM Item",
|
"doctype": "BOM Item",
|
||||||
"item_code": "_Test Item",
|
"item_code": "_Test Item",
|
||||||
"parentfield": "bom_materials",
|
"parentfield": "bom_materials",
|
||||||
"qty": 1.0,
|
"qty": 1.0,
|
||||||
"rate": 5000.0,
|
"rate": 5000.0,
|
||||||
"stock_uom": "_Test UOM"
|
"stock_uom": "_Test UOM"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"operation_no": 1,
|
"operation_no": 1,
|
||||||
"amount": 2000.0,
|
"amount": 2000.0,
|
||||||
"bom_no": "BOM/_Test Item Home Desktop Manufactured/001",
|
"bom_no": "BOM/_Test Item Home Desktop Manufactured/001",
|
||||||
"doctype": "BOM Item",
|
"doctype": "BOM Item",
|
||||||
"item_code": "_Test Item Home Desktop Manufactured",
|
"item_code": "_Test Item Home Desktop Manufactured",
|
||||||
"parentfield": "bom_materials",
|
"parentfield": "bom_materials",
|
||||||
"qty": 2.0,
|
"qty": 2.0,
|
||||||
"rate": 1000.0,
|
"rate": 1000.0,
|
||||||
"stock_uom": "_Test UOM"
|
"stock_uom": "_Test UOM"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
"doctype": "BOM",
|
"doctype": "BOM",
|
||||||
"is_active": 1,
|
"is_active": 1,
|
||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"item": "_Test FG Item 2",
|
"item": "_Test FG Item 2",
|
||||||
|
"quantity": 1.0,
|
||||||
|
"with_operations": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bom_operations": [
|
||||||
|
{
|
||||||
|
"operation_no": "1",
|
||||||
|
"opn_description": "_Test",
|
||||||
|
"workstation": "_Test Workstation 1",
|
||||||
|
"time_in_min": 60,
|
||||||
|
"operating_cost": 140
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bom_materials": [
|
||||||
|
{
|
||||||
|
"operation_no": 1,
|
||||||
|
"amount": 5000.0,
|
||||||
|
"doctype": "BOM Item",
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"parentfield": "bom_materials",
|
||||||
|
"qty": 2.0,
|
||||||
|
"rate": 3000.0,
|
||||||
|
"stock_uom": "_Test UOM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"docstatus": 1,
|
||||||
|
"doctype": "BOM",
|
||||||
|
"is_active": 1,
|
||||||
|
"is_default": 1,
|
||||||
|
"item": "_Test Variant Item",
|
||||||
"quantity": 1.0,
|
"quantity": 1.0,
|
||||||
"with_operations": 1
|
"with_operations": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -7,12 +7,12 @@ import frappe
|
|||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
||||||
|
|
||||||
class OverProductionError(frappe.ValidationError): pass
|
class OverProductionError(frappe.ValidationError): pass
|
||||||
class StockOverProductionError(frappe.ValidationError): pass
|
class StockOverProductionError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class ProductionOrder(Document):
|
class ProductionOrder(Document):
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
self.status = "Draft"
|
self.status = "Draft"
|
||||||
@ -21,7 +21,9 @@ class ProductionOrder(Document):
|
|||||||
validate_status(self.status, ["Draft", "Submitted", "Stopped",
|
validate_status(self.status, ["Draft", "Submitted", "Stopped",
|
||||||
"In Process", "Completed", "Cancelled"])
|
"In Process", "Completed", "Cancelled"])
|
||||||
|
|
||||||
self.validate_bom_no()
|
if self.bom_no:
|
||||||
|
validate_bom_no(self.production_item, self.bom_no)
|
||||||
|
|
||||||
self.validate_sales_order()
|
self.validate_sales_order()
|
||||||
self.validate_warehouse()
|
self.validate_warehouse()
|
||||||
self.set_fixed_cost()
|
self.set_fixed_cost()
|
||||||
@ -29,14 +31,6 @@ class ProductionOrder(Document):
|
|||||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||||
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
|
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
|
||||||
|
|
||||||
def validate_bom_no(self):
|
|
||||||
if self.bom_no:
|
|
||||||
bom = frappe.db.sql("""select name from `tabBOM` where name=%s and docstatus=1
|
|
||||||
and is_active=1 and item=%s"""
|
|
||||||
, (self.bom_no, self.production_item), as_dict =1)
|
|
||||||
if not bom:
|
|
||||||
frappe.throw(_("BOM {0} is not active or not submitted").format(self.bom_no))
|
|
||||||
|
|
||||||
def validate_sales_order(self):
|
def validate_sales_order(self):
|
||||||
if self.sales_order:
|
if self.sales_order:
|
||||||
so = frappe.db.sql("""select name, delivery_date from `tabSales Order`
|
so = frappe.db.sql("""select name, delivery_date from `tabSales Order`
|
||||||
|
@ -264,6 +264,7 @@
|
|||||||
"is_sales_item": "Yes",
|
"is_sales_item": "Yes",
|
||||||
"is_service_item": "No",
|
"is_service_item": "No",
|
||||||
"is_stock_item": "Yes",
|
"is_stock_item": "Yes",
|
||||||
|
"is_manufactured_item": "Yes",
|
||||||
"is_sub_contracted_item": "Yes",
|
"is_sub_contracted_item": "Yes",
|
||||||
"item_code": "_Test Variant Item",
|
"item_code": "_Test Variant Item",
|
||||||
"item_group": "_Test Item Group Desktops",
|
"item_group": "_Test Item Group Desktops",
|
||||||
|
@ -12,6 +12,7 @@ from erpnext.stock.utils import get_incoming_rate
|
|||||||
from erpnext.stock.stock_ledger import get_previous_sle
|
from erpnext.stock.stock_ledger import get_previous_sle
|
||||||
from erpnext.controllers.queries import get_match_cond
|
from erpnext.controllers.queries import get_match_cond
|
||||||
from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center
|
from erpnext.stock.get_item_details import get_available_qty, get_default_cost_center
|
||||||
|
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
||||||
|
|
||||||
class NotUpdateStockError(frappe.ValidationError): pass
|
class NotUpdateStockError(frappe.ValidationError): pass
|
||||||
class StockOverReturnError(frappe.ValidationError): pass
|
class StockOverReturnError(frappe.ValidationError): pass
|
||||||
@ -293,10 +294,8 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def validate_bom(self):
|
def validate_bom(self):
|
||||||
for d in self.get('mtn_details'):
|
for d in self.get('mtn_details'):
|
||||||
if d.bom_no and not frappe.db.sql("""select name from `tabBOM`
|
if d.bom_no:
|
||||||
where item = %s and name = %s and docstatus = 1 and is_active = 1""",
|
validate_bom_no(d.item_code, d.bom_no)
|
||||||
(d.item_code, d.bom_no)):
|
|
||||||
frappe.throw(_("BOM {0} is not submitted or inactive BOM for Item {1}").format(d.bom_no, d.item_code))
|
|
||||||
|
|
||||||
def validate_finished_goods(self):
|
def validate_finished_goods(self):
|
||||||
"""validation: finished good quantity should be same as manufacturing quantity"""
|
"""validation: finished good quantity should be same as manufacturing quantity"""
|
||||||
@ -497,13 +496,20 @@ class StockEntry(StockController):
|
|||||||
# add raw materials to Stock Entry Detail table
|
# add raw materials to Stock Entry Detail table
|
||||||
self.add_to_stock_entry_detail(item_dict)
|
self.add_to_stock_entry_detail(item_dict)
|
||||||
|
|
||||||
# add finished good item to Stock Entry Detail table -- along with bom_no
|
if self.bom_no:
|
||||||
if self.production_order and self.purpose == "Manufacture":
|
if self.production_order:
|
||||||
item = frappe.db.get_value("Item", pro_obj.production_item, ["item_name",
|
item_code = pro_obj.production_item
|
||||||
"description", "stock_uom", "expense_account", "buying_cost_center"], as_dict=1)
|
to_warehouse = pro_obj.fg_warehouse
|
||||||
|
else:
|
||||||
|
item_code = frappe.db.get_value("BOM", self.bom_no, "item")
|
||||||
|
to_warehouse = ""
|
||||||
|
|
||||||
|
item = frappe.db.get_value("Item", item_code, ["item_name",
|
||||||
|
"description", "stock_uom", "expense_account", "buying_cost_center", "name"], as_dict=1)
|
||||||
|
|
||||||
self.add_to_stock_entry_detail({
|
self.add_to_stock_entry_detail({
|
||||||
cstr(pro_obj.production_item): {
|
item.name: {
|
||||||
"to_warehouse": pro_obj.fg_warehouse,
|
"to_warehouse": to_warehouse,
|
||||||
"from_warehouse": "",
|
"from_warehouse": "",
|
||||||
"qty": self.fg_completed_qty,
|
"qty": self.fg_completed_qty,
|
||||||
"item_name": item.item_name,
|
"item_name": item.item_name,
|
||||||
@ -512,27 +518,7 @@ class StockEntry(StockController):
|
|||||||
"expense_account": item.expense_account,
|
"expense_account": item.expense_account,
|
||||||
"cost_center": item.buying_cost_center,
|
"cost_center": item.buying_cost_center,
|
||||||
}
|
}
|
||||||
}, bom_no=pro_obj.bom_no)
|
}, bom_no = self.bom_no)
|
||||||
|
|
||||||
elif self.purpose in ["Material Receipt", "Repack"]:
|
|
||||||
if self.purpose=="Material Receipt":
|
|
||||||
self.from_warehouse = ""
|
|
||||||
|
|
||||||
item = frappe.db.sql("""select name, item_name, description,
|
|
||||||
stock_uom, expense_account, buying_cost_center from `tabItem`
|
|
||||||
where name=(select item from tabBOM where name=%s)""",
|
|
||||||
self.bom_no, as_dict=1)
|
|
||||||
self.add_to_stock_entry_detail({
|
|
||||||
item[0]["name"] : {
|
|
||||||
"qty": self.fg_completed_qty,
|
|
||||||
"item_name": item[0].item_name,
|
|
||||||
"description": item[0]["description"],
|
|
||||||
"stock_uom": item[0]["stock_uom"],
|
|
||||||
"from_warehouse": "",
|
|
||||||
"expense_account": item[0].expense_account,
|
|
||||||
"cost_center": item[0].buying_cost_center,
|
|
||||||
}
|
|
||||||
}, bom_no=self.bom_no)
|
|
||||||
|
|
||||||
self.get_stock_and_rate()
|
self.get_stock_and_rate()
|
||||||
|
|
||||||
|
@ -858,6 +858,29 @@ class TestStockEntry(unittest.TestCase):
|
|||||||
fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test Item"][0]
|
fg_rate = [d.amount for d in stock_entry.get("mtn_details") if d.item_code=="_Test Item"][0]
|
||||||
self.assertEqual(fg_rate, 100.00)
|
self.assertEqual(fg_rate, 100.00)
|
||||||
|
|
||||||
|
def test_variant_production_order(self):
|
||||||
|
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
|
||||||
|
"is_default": 1, "docstatus": 1})
|
||||||
|
|
||||||
|
production_order = frappe.new_doc("Production Order")
|
||||||
|
production_order.update({
|
||||||
|
"company": "_Test Company",
|
||||||
|
"fg_warehouse": "_Test Warehouse 1 - _TC",
|
||||||
|
"production_item": "_Test Variant Item-S",
|
||||||
|
"bom_no": bom_no,
|
||||||
|
"qty": 1.0,
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"wip_warehouse": "_Test Warehouse - _TC"
|
||||||
|
})
|
||||||
|
production_order.insert()
|
||||||
|
production_order.submit()
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(production_order.name, "Manufacture", 1))
|
||||||
|
stock_entry.insert()
|
||||||
|
self.assertTrue("_Test Variant Item-S" in [d.item_code for d in stock_entry.mtn_details])
|
||||||
|
|
||||||
def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None):
|
def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None):
|
||||||
se = frappe.copy_doc(test_records[0])
|
se = frappe.copy_doc(test_records[0])
|
||||||
se.get("mtn_details")[0].item_code = item_code or "_Test Serialized Item With Series"
|
se.get("mtn_details")[0].item_code = item_code or "_Test Serialized Item With Series"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user