Merge branch 'develop' into bom-update-tool

This commit is contained in:
Ankush Menat
2022-03-28 20:28:21 +05:30
1466 changed files with 94420 additions and 63150 deletions

View File

@@ -11,33 +11,39 @@ import erpnext
def get_data():
return frappe._dict({
"dashboards": get_dashboards(),
"charts": get_charts(),
"number_cards": get_number_cards(),
})
return frappe._dict(
{
"dashboards": get_dashboards(),
"charts": get_charts(),
"number_cards": get_number_cards(),
}
)
def get_dashboards():
return [{
"name": "Manufacturing",
"dashboard_name": "Manufacturing",
"charts": [
{ "chart": "Produced Quantity", "width": "Half" },
{ "chart": "Completed Operation", "width": "Half" },
{ "chart": "Work Order Analysis", "width": "Half" },
{ "chart": "Quality Inspection Analysis", "width": "Half" },
{ "chart": "Pending Work Order", "width": "Half" },
{ "chart": "Last Month Downtime Analysis", "width": "Half" },
{ "chart": "Work Order Qty Analysis", "width": "Full" },
{ "chart": "Job Card Analysis", "width": "Full" }
],
"cards": [
{ "card": "Monthly Total Work Order" },
{ "card": "Monthly Completed Work Order" },
{ "card": "Ongoing Job Card" },
{ "card": "Monthly Quality Inspection"}
]
}]
return [
{
"name": "Manufacturing",
"dashboard_name": "Manufacturing",
"charts": [
{"chart": "Produced Quantity", "width": "Half"},
{"chart": "Completed Operation", "width": "Half"},
{"chart": "Work Order Analysis", "width": "Half"},
{"chart": "Quality Inspection Analysis", "width": "Half"},
{"chart": "Pending Work Order", "width": "Half"},
{"chart": "Last Month Downtime Analysis", "width": "Half"},
{"chart": "Work Order Qty Analysis", "width": "Full"},
{"chart": "Job Card Analysis", "width": "Full"},
],
"cards": [
{"card": "Monthly Total Work Order"},
{"card": "Monthly Completed Work Order"},
{"card": "Ongoing Job Card"},
{"card": "Monthly Quality Inspection"},
],
}
]
def get_charts():
company = erpnext.get_default_company()
@@ -45,200 +51,198 @@ def get_charts():
if not company:
company = frappe.db.get_value("Company", {"is_group": 0}, "name")
return [{
"doctype": "Dashboard Chart",
"based_on": "modified",
"chart_type": "Sum",
"chart_name": _("Produced Quantity"),
"name": "Produced Quantity",
"document_type": "Work Order",
"filters_json": json.dumps([['Work Order', 'docstatus', '=', 1, False]]),
"group_by_type": "Count",
"time_interval": "Monthly",
"timespan": "Last Year",
"owner": "Administrator",
"type": "Line",
"value_based_on": "produced_qty",
"is_public": 1,
"timeseries": 1
}, {
"doctype": "Dashboard Chart",
"based_on": "creation",
"chart_type": "Sum",
"chart_name": _("Completed Operation"),
"name": "Completed Operation",
"document_type": "Work Order Operation",
"filters_json": json.dumps([['Work Order Operation', 'docstatus', '=', 1, False]]),
"group_by_type": "Count",
"time_interval": "Quarterly",
"timespan": "Last Year",
"owner": "Administrator",
"type": "Line",
"value_based_on": "completed_qty",
"is_public": 1,
"timeseries": 1
}, {
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Work Order Analysis"),
"name": "Work Order Analysis",
"timespan": "Last Year",
"report_name": "Work Order Summary",
"owner": "Administrator",
"filters_json": json.dumps({"company": company, "charts_based_on": "Status"}),
"type": "Donut",
"is_public": 1,
"is_custom": 1,
"custom_options": json.dumps({
"axisOptions": {
"shortenYAxisNumbers": 1
},
"height": 300
}),
}, {
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Quality Inspection Analysis"),
"name": "Quality Inspection Analysis",
"timespan": "Last Year",
"report_name": "Quality Inspection Summary",
"owner": "Administrator",
"filters_json": json.dumps({}),
"type": "Donut",
"is_public": 1,
"is_custom": 1,
"custom_options": json.dumps({
"axisOptions": {
"shortenYAxisNumbers": 1
},
"height": 300
}),
}, {
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Pending Work Order"),
"name": "Pending Work Order",
"timespan": "Last Year",
"report_name": "Work Order Summary",
"filters_json": json.dumps({"company": company, "charts_based_on": "Age"}),
"owner": "Administrator",
"type": "Donut",
"is_public": 1,
"is_custom": 1,
"custom_options": json.dumps({
"axisOptions": {
"shortenYAxisNumbers": 1
},
"height": 300
}),
}, {
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Last Month Downtime Analysis"),
"name": "Last Month Downtime Analysis",
"timespan": "Last Year",
"filters_json": json.dumps({}),
"report_name": "Downtime Analysis",
"owner": "Administrator",
"is_public": 1,
"is_custom": 1,
"type": "Bar"
}, {
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Work Order Qty Analysis"),
"name": "Work Order Qty Analysis",
"timespan": "Last Year",
"report_name": "Work Order Summary",
"filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}),
"owner": "Administrator",
"type": "Bar",
"is_public": 1,
"is_custom": 1,
"custom_options": json.dumps({
"barOptions": { "stacked": 1 }
}),
}, {
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Job Card Analysis"),
"name": "Job Card Analysis",
"timespan": "Last Year",
"report_name": "Job Card Summary",
"owner": "Administrator",
"is_public": 1,
"is_custom": 1,
"filters_json": json.dumps({"company": company, "docstatus": 1, "range":"Monthly"}),
"custom_options": json.dumps({
"barOptions": { "stacked": 1 }
}),
"type": "Bar"
}]
return [
{
"doctype": "Dashboard Chart",
"based_on": "modified",
"chart_type": "Sum",
"chart_name": _("Produced Quantity"),
"name": "Produced Quantity",
"document_type": "Work Order",
"filters_json": json.dumps([["Work Order", "docstatus", "=", 1, False]]),
"group_by_type": "Count",
"time_interval": "Monthly",
"timespan": "Last Year",
"owner": "Administrator",
"type": "Line",
"value_based_on": "produced_qty",
"is_public": 1,
"timeseries": 1,
},
{
"doctype": "Dashboard Chart",
"based_on": "creation",
"chart_type": "Sum",
"chart_name": _("Completed Operation"),
"name": "Completed Operation",
"document_type": "Work Order Operation",
"filters_json": json.dumps([["Work Order Operation", "docstatus", "=", 1, False]]),
"group_by_type": "Count",
"time_interval": "Quarterly",
"timespan": "Last Year",
"owner": "Administrator",
"type": "Line",
"value_based_on": "completed_qty",
"is_public": 1,
"timeseries": 1,
},
{
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Work Order Analysis"),
"name": "Work Order Analysis",
"timespan": "Last Year",
"report_name": "Work Order Summary",
"owner": "Administrator",
"filters_json": json.dumps({"company": company, "charts_based_on": "Status"}),
"type": "Donut",
"is_public": 1,
"is_custom": 1,
"custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}),
},
{
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Quality Inspection Analysis"),
"name": "Quality Inspection Analysis",
"timespan": "Last Year",
"report_name": "Quality Inspection Summary",
"owner": "Administrator",
"filters_json": json.dumps({}),
"type": "Donut",
"is_public": 1,
"is_custom": 1,
"custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}),
},
{
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Pending Work Order"),
"name": "Pending Work Order",
"timespan": "Last Year",
"report_name": "Work Order Summary",
"filters_json": json.dumps({"company": company, "charts_based_on": "Age"}),
"owner": "Administrator",
"type": "Donut",
"is_public": 1,
"is_custom": 1,
"custom_options": json.dumps({"axisOptions": {"shortenYAxisNumbers": 1}, "height": 300}),
},
{
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Last Month Downtime Analysis"),
"name": "Last Month Downtime Analysis",
"timespan": "Last Year",
"filters_json": json.dumps({}),
"report_name": "Downtime Analysis",
"owner": "Administrator",
"is_public": 1,
"is_custom": 1,
"type": "Bar",
},
{
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Work Order Qty Analysis"),
"name": "Work Order Qty Analysis",
"timespan": "Last Year",
"report_name": "Work Order Summary",
"filters_json": json.dumps({"company": company, "charts_based_on": "Quantity"}),
"owner": "Administrator",
"type": "Bar",
"is_public": 1,
"is_custom": 1,
"custom_options": json.dumps({"barOptions": {"stacked": 1}}),
},
{
"doctype": "Dashboard Chart",
"time_interval": "Yearly",
"chart_type": "Report",
"chart_name": _("Job Card Analysis"),
"name": "Job Card Analysis",
"timespan": "Last Year",
"report_name": "Job Card Summary",
"owner": "Administrator",
"is_public": 1,
"is_custom": 1,
"filters_json": json.dumps({"company": company, "docstatus": 1, "range": "Monthly"}),
"custom_options": json.dumps({"barOptions": {"stacked": 1}}),
"type": "Bar",
},
]
def get_number_cards():
start_date = add_months(nowdate(), -1)
end_date = nowdate()
return [{
"doctype": "Number Card",
"document_type": "Work Order",
"name": "Monthly Total Work Order",
"filters_json": json.dumps([
['Work Order', 'docstatus', '=', 1],
['Work Order', 'creation', 'between', [start_date, end_date]]
]),
"function": "Count",
"is_public": 1,
"label": _("Monthly Total Work Orders"),
"show_percentage_stats": 1,
"stats_time_interval": "Weekly"
},
{
"doctype": "Number Card",
"document_type": "Work Order",
"name": "Monthly Completed Work Order",
"filters_json": json.dumps([
['Work Order', 'status', '=', 'Completed'],
['Work Order', 'docstatus', '=', 1],
['Work Order', 'creation', 'between', [start_date, end_date]]
]),
"function": "Count",
"is_public": 1,
"label": _("Monthly Completed Work Orders"),
"show_percentage_stats": 1,
"stats_time_interval": "Weekly"
},
{
"doctype": "Number Card",
"document_type": "Job Card",
"name": "Ongoing Job Card",
"filters_json": json.dumps([
['Job Card', 'status','!=','Completed'],
['Job Card', 'docstatus', '=', 1]
]),
"function": "Count",
"is_public": 1,
"label": _("Ongoing Job Cards"),
"show_percentage_stats": 1,
"stats_time_interval": "Weekly"
},
{
"doctype": "Number Card",
"document_type": "Quality Inspection",
"name": "Monthly Quality Inspection",
"filters_json": json.dumps([
['Quality Inspection', 'docstatus', '=', 1],
['Quality Inspection', 'creation', 'between', [start_date, end_date]]
]),
"function": "Count",
"is_public": 1,
"label": _("Monthly Quality Inspections"),
"show_percentage_stats": 1,
"stats_time_interval": "Weekly"
}]
return [
{
"doctype": "Number Card",
"document_type": "Work Order",
"name": "Monthly Total Work Order",
"filters_json": json.dumps(
[
["Work Order", "docstatus", "=", 1],
["Work Order", "creation", "between", [start_date, end_date]],
]
),
"function": "Count",
"is_public": 1,
"label": _("Monthly Total Work Orders"),
"show_percentage_stats": 1,
"stats_time_interval": "Weekly",
},
{
"doctype": "Number Card",
"document_type": "Work Order",
"name": "Monthly Completed Work Order",
"filters_json": json.dumps(
[
["Work Order", "status", "=", "Completed"],
["Work Order", "docstatus", "=", 1],
["Work Order", "creation", "between", [start_date, end_date]],
]
),
"function": "Count",
"is_public": 1,
"label": _("Monthly Completed Work Orders"),
"show_percentage_stats": 1,
"stats_time_interval": "Weekly",
},
{
"doctype": "Number Card",
"document_type": "Job Card",
"name": "Ongoing Job Card",
"filters_json": json.dumps(
[["Job Card", "status", "!=", "Completed"], ["Job Card", "docstatus", "=", 1]]
),
"function": "Count",
"is_public": 1,
"label": _("Ongoing Job Cards"),
"show_percentage_stats": 1,
"stats_time_interval": "Weekly",
},
{
"doctype": "Number Card",
"document_type": "Quality Inspection",
"name": "Monthly Quality Inspection",
"filters_json": json.dumps(
[
["Quality Inspection", "docstatus", "=", 1],
["Quality Inspection", "creation", "between", [start_date, end_date]],
]
),
"function": "Count",
"is_public": 1,
"label": _("Monthly Quality Inspections"),
"show_percentage_stats": 1,
"stats_time_interval": "Weekly",
},
]

View File

@@ -29,7 +29,9 @@ class BlanketOrder(Document):
def update_ordered_qty(self):
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
item_ordered_qty = frappe._dict(frappe.db.sql("""
item_ordered_qty = frappe._dict(
frappe.db.sql(
"""
select trans_item.item_code, sum(trans_item.stock_qty) as qty
from `tab{0} Item` trans_item, `tab{0}` trans
where trans.name = trans_item.parent
@@ -37,18 +39,24 @@ class BlanketOrder(Document):
and trans.docstatus=1
and trans.status not in ('Closed', 'Stopped')
group by trans_item.item_code
""".format(ref_doctype), self.name))
""".format(
ref_doctype
),
self.name,
)
)
for d in self.items:
d.db_set("ordered_qty", item_ordered_qty.get(d.item_code, 0))
@frappe.whitelist()
def make_order(source_name):
doctype = frappe.flags.args.doctype
def update_doc(source_doc, target_doc, source_parent):
if doctype == 'Quotation':
target_doc.quotation_to = 'Customer'
if doctype == "Quotation":
target_doc.quotation_to = "Customer"
target_doc.party_name = source_doc.customer
def update_item(source, target, source_parent):
@@ -62,18 +70,16 @@ def make_order(source_name):
target.against_blanket_order = 1
target.blanket_order = source_name
target_doc = get_mapped_doc("Blanket Order", source_name, {
"Blanket Order": {
"doctype": doctype,
"postprocess": update_doc
},
"Blanket Order Item": {
"doctype": doctype + " Item",
"field_map": {
"rate": "blanket_order_rate",
"parent": "blanket_order"
target_doc = get_mapped_doc(
"Blanket Order",
source_name,
{
"Blanket Order": {"doctype": doctype, "postprocess": update_doc},
"Blanket Order Item": {
"doctype": doctype + " Item",
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
"postprocess": update_item,
},
"postprocess": update_item
}
})
},
)
return target_doc

View File

@@ -1,9 +1,5 @@
def get_data():
return {
'fieldname': 'blanket_order',
'transactions': [
{
'items': ['Purchase Order', 'Sales Order', 'Quotation']
}
]
"fieldname": "blanket_order",
"transactions": [{"items": ["Purchase Order", "Sales Order", "Quotation"]}],
}

View File

@@ -16,7 +16,7 @@ class TestBlanketOrder(FrappeTestCase):
def test_sales_order_creation(self):
bo = make_blanket_order(blanket_order_type="Selling")
frappe.flags.args.doctype = 'Sales Order'
frappe.flags.args.doctype = "Sales Order"
so = make_order(bo.name)
so.currency = get_company_currency(so.company)
so.delivery_date = today()
@@ -33,16 +33,15 @@ class TestBlanketOrder(FrappeTestCase):
self.assertEqual(so.items[0].qty, bo.items[0].ordered_qty)
# test the quantity
frappe.flags.args.doctype = 'Sales Order'
frappe.flags.args.doctype = "Sales Order"
so1 = make_order(bo.name)
so1.currency = get_company_currency(so1.company)
self.assertEqual(so1.items[0].qty, (bo.items[0].qty-bo.items[0].ordered_qty))
self.assertEqual(so1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
def test_purchase_order_creation(self):
bo = make_blanket_order(blanket_order_type="Purchasing")
frappe.flags.args.doctype = 'Purchase Order'
frappe.flags.args.doctype = "Purchase Order"
po = make_order(bo.name)
po.currency = get_company_currency(po.company)
po.schedule_date = today()
@@ -59,11 +58,10 @@ class TestBlanketOrder(FrappeTestCase):
self.assertEqual(po.items[0].qty, bo.items[0].ordered_qty)
# test the quantity
frappe.flags.args.doctype = 'Purchase Order'
frappe.flags.args.doctype = "Purchase Order"
po1 = make_order(bo.name)
po1.currency = get_company_currency(po1.company)
self.assertEqual(po1.items[0].qty, (bo.items[0].qty-bo.items[0].ordered_qty))
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
def make_blanket_order(**args):
@@ -80,11 +78,14 @@ def make_blanket_order(**args):
bo.from_date = today()
bo.to_date = add_months(bo.from_date, months=12)
bo.append("items", {
"item_code": args.item_code or "_Test Item",
"qty": args.quantity or 1000,
"rate": args.rate or 100
})
bo.append(
"items",
{
"item_code": args.item_code or "_Test Item",
"qty": args.quantity or 1000,
"rate": args.rate or 100,
},
)
bo.insert()
bo.submit()

File diff suppressed because it is too large Load Diff

View File

@@ -3,27 +3,28 @@ from frappe import _
def get_data():
return {
'fieldname': 'bom_no',
'non_standard_fieldnames': {
'Item': 'default_bom',
'Purchase Order': 'bom',
'Purchase Receipt': 'bom',
'Purchase Invoice': 'bom'
"fieldname": "bom_no",
"non_standard_fieldnames": {
"Item": "default_bom",
"Purchase Order": "bom",
"Purchase Receipt": "bom",
"Purchase Invoice": "bom",
},
'transactions': [
"transactions": [
{"label": _("Stock"), "items": ["Item", "Stock Entry", "Quality Inspection"]},
{"label": _("Manufacture"), "items": ["BOM", "Work Order", "Job Card"]},
{
'label': _('Stock'),
'items': ['Item', 'Stock Entry', 'Quality Inspection']
"label": _("Subcontract"),
"items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"],
},
{
'label': _('Manufacture'),
'items': ['BOM', 'Work Order', 'Job Card']
},
{
'label': _('Subcontract'),
'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
}
],
'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt",
"Purchase Invoice", "Job Card", "Stock Entry", "BOM"]
"disable_create_buttons": [
"Item",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
"Job Card",
"Stock Entry",
"BOM",
],
}

View File

@@ -6,7 +6,6 @@ from collections import deque
from functools import partial
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt
@@ -19,25 +18,27 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
)
from erpnext.tests.test_subcontracting import set_backflush_based_on
test_records = frappe.get_test_records('BOM')
test_records = frappe.get_test_records("BOM")
test_dependencies = ["Item", "Quality Inspection Template"]
class TestBOM(FrappeTestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
items_dict = get_bom_items_as_dict(bom=get_default_bom(),
company="_Test Company", qty=1, fetch_exploded=0)
items_dict = get_bom_items_as_dict(
bom=get_default_bom(), company="_Test Company", qty=1, fetch_exploded=0
)
self.assertTrue(test_records[2]["items"][0]["item_code"] in items_dict)
self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict)
self.assertEqual(len(items_dict.values()), 2)
def test_get_items_exploded(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
items_dict = get_bom_items_as_dict(bom=get_default_bom(),
company="_Test Company", qty=1, fetch_exploded=1)
items_dict = get_bom_items_as_dict(
bom=get_default_bom(), company="_Test Company", qty=1, fetch_exploded=1
)
self.assertTrue(test_records[2]["items"][0]["item_code"] in items_dict)
self.assertFalse(test_records[2]["items"][1]["item_code"] in items_dict)
self.assertTrue(test_records[0]["items"][0]["item_code"] in items_dict)
@@ -46,13 +47,14 @@ class TestBOM(FrappeTestCase):
def test_get_items_list(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3)
def test_default_bom(self):
def _get_default_bom_in_item():
return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"))
bom = frappe.get_doc("BOM", {"item":"_Test FG Item 2", "is_default": 1})
bom = frappe.get_doc("BOM", {"item": "_Test FG Item 2", "is_default": 1})
self.assertEqual(_get_default_bom_in_item(), bom.name)
bom.is_active = 0
@@ -60,28 +62,33 @@ class TestBOM(FrappeTestCase):
self.assertEqual(_get_default_bom_in_item(), "")
bom.is_active = 1
bom.is_default=1
bom.is_default = 1
bom.save()
self.assertTrue(_get_default_bom_in_item(), bom.name)
def test_update_bom_cost_in_all_boms(self):
# get current rate for '_Test Item 2'
rm_rate = frappe.db.sql("""select rate from `tabBOM Item`
rm_rate = frappe.db.sql(
"""select rate from `tabBOM Item`
where parent='BOM-_Test Item Home Desktop Manufactured-001'
and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""")
and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'"""
)
rm_rate = rm_rate[0][0] if rm_rate else 0
# Reset item valuation rate
reset_item_valuation_rate(item_code='_Test Item 2', qty=200, rate=rm_rate + 10)
reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10)
# update cost of all BOMs based on latest valuation rate
update_cost()
# check if new valuation rate updated in all BOMs
for d in frappe.db.sql("""select rate from `tabBOM Item`
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""", as_dict=1):
self.assertEqual(d.rate, rm_rate + 10)
for d in frappe.db.sql(
"""select rate from `tabBOM Item`
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
as_dict=1,
):
self.assertEqual(d.rate, rm_rate + 10)
def test_bom_cost(self):
bom = frappe.copy_doc(test_records[2])
@@ -96,7 +103,9 @@ class TestBOM(FrappeTestCase):
for row in bom.items:
raw_material_cost += row.amount
base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
base_raw_material_cost = raw_material_cost * flt(
bom.conversion_rate, bom.precision("conversion_rate")
)
base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate"))
# test amounts in selected currency, almostEqual checks for 7 digits by default
@@ -124,14 +133,15 @@ class TestBOM(FrappeTestCase):
for op_row in bom.operations:
self.assertAlmostEqual(op_row.cost_per_unit, op_row.operating_cost / 2)
self.assertAlmostEqual(bom.operating_cost, op_cost/2)
self.assertAlmostEqual(bom.operating_cost, op_cost / 2)
bom.delete()
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
frappe.db.sql("delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s",
item_code)
frappe.db.sql(
"delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", item_code
)
item_price = frappe.new_doc("Item Price")
item_price.price_list = "_Test Price List"
item_price.item_code = item_code
@@ -146,7 +156,7 @@ class TestBOM(FrappeTestCase):
bom.items[0].conversion_factor = 5
bom.insert()
bom.update_cost(update_hour_rate = False)
bom.update_cost(update_hour_rate=False)
# test amounts in selected currency
self.assertEqual(bom.items[0].rate, 300)
@@ -171,11 +181,12 @@ class TestBOM(FrappeTestCase):
bom.insert()
reset_item_valuation_rate(
item_code='_Test Item',
warehouse_list=frappe.get_all("Warehouse",
{"is_group":0, "company": bom.company}, pluck="name"),
item_code="_Test Item",
warehouse_list=frappe.get_all(
"Warehouse", {"is_group": 0, "company": bom.company}, pluck="name"
),
qty=200,
rate=200
rate=200,
)
bom.update_cost()
@@ -184,68 +195,64 @@ class TestBOM(FrappeTestCase):
def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1"
set_backflush_based_on('Material Transferred for Subcontract')
set_backflush_based_on("Material Transferred for Subcontract")
if not frappe.db.exists('Item', item_code):
make_item(item_code, {
'is_stock_item': 1,
'is_sub_contracted_item': 1,
'stock_uom': 'Nos'
})
if not frappe.db.exists("Item", item_code):
make_item(item_code, {"is_stock_item": 1, "is_sub_contracted_item": 1, "stock_uom": "Nos"})
if not frappe.db.exists('Item', "Test Extra Item 1"):
make_item("Test Extra Item 1", {
'is_stock_item': 1,
'stock_uom': 'Nos'
})
if not frappe.db.exists("Item", "Test Extra Item 1"):
make_item("Test Extra Item 1", {"is_stock_item": 1, "stock_uom": "Nos"})
if not frappe.db.exists('Item', "Test Extra Item 2"):
make_item("Test Extra Item 2", {
'is_stock_item': 1,
'stock_uom': 'Nos'
})
if not frappe.db.exists("Item", "Test Extra Item 2"):
make_item("Test Extra Item 2", {"is_stock_item": 1, "stock_uom": "Nos"})
if not frappe.db.exists('Item', "Test Extra Item 3"):
make_item("Test Extra Item 3", {
'is_stock_item': 1,
'stock_uom': 'Nos'
})
bom = frappe.get_doc({
'doctype': 'BOM',
'is_default': 1,
'item': item_code,
'currency': 'USD',
'quantity': 1,
'company': '_Test Company'
})
if not frappe.db.exists("Item", "Test Extra Item 3"):
make_item("Test Extra Item 3", {"is_stock_item": 1, "stock_uom": "Nos"})
bom = frappe.get_doc(
{
"doctype": "BOM",
"is_default": 1,
"item": item_code,
"currency": "USD",
"quantity": 1,
"company": "_Test Company",
}
)
for item in ["Test Extra Item 1", "Test Extra Item 2"]:
item_doc = frappe.get_doc('Item', item)
item_doc = frappe.get_doc("Item", item)
bom.append('items', {
'item_code': item,
'qty': 1,
'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom,
'rate': item_doc.valuation_rate
})
bom.append(
"items",
{
"item_code": item,
"qty": 1,
"uom": item_doc.stock_uom,
"stock_uom": item_doc.stock_uom,
"rate": item_doc.valuation_rate,
},
)
bom.append('items', {
'item_code': "Test Extra Item 3",
'qty': 1,
'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom,
'rate': 0,
'sourced_by_supplier': 1
})
bom.append(
"items",
{
"item_code": "Test Extra Item 3",
"qty": 1,
"uom": item_doc.stock_uom,
"stock_uom": item_doc.stock_uom,
"rate": 0,
"sourced_by_supplier": 1,
},
)
bom.insert(ignore_permissions=True)
bom.update_cost()
bom.submit()
# test that sourced_by_supplier rate is zero even after updating cost
self.assertEqual(bom.items[2].rate, 0)
# test in Purchase Order sourced_by_supplier is not added to Supplied Item
po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
po = create_purchase_order(
item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC"
)
bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1])
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEqual(bom_items, supplied_items)
@@ -253,7 +260,10 @@ class TestBOM(FrappeTestCase):
def test_bom_tree_representation(self):
bom_tree = {
"Assembly": {
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
"SubAssembly1": {
"ChildPart1": {},
"ChildPart2": {},
},
"SubAssembly2": {"ChildPart3": {}},
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
"ChildPart5": {},
@@ -264,7 +274,7 @@ class TestBOM(FrappeTestCase):
parent_bom = create_nested_bom(bom_tree, prefix="")
created_tree = parent_bom.get_tree_representation()
reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
reqd_order = level_order_traversal(bom_tree)[1:] # skip first item
created_order = created_tree.level_order_traversal()
self.assertEqual(len(reqd_order), len(created_order))
@@ -276,14 +286,23 @@ class TestBOM(FrappeTestCase):
from erpnext.controllers.item_variant import create_variant
template_item = make_item(
"_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]}
"_TestTemplateItem",
{
"has_variants": 1,
"attributes": [
{"attribute": "Test Size"},
],
},
)
variant = create_variant(template_item.item_code, {"Test Size": "Large"})
variant.insert(ignore_if_duplicate=True)
bom_tree = {
template_item.item_code: {
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
"SubAssembly1": {
"ChildPart1": {},
"ChildPart2": {},
},
"ChildPart5": {},
}
}
@@ -306,7 +325,7 @@ class TestBOM(FrappeTestCase):
def test_bom_recursion_1st_level(self):
"""BOM should not allow BOM item again in child"""
item_code = "_Test BOM Recursion"
make_item(item_code, {'is_stock_item': 1})
make_item(item_code, {"is_stock_item": 1})
bom = frappe.new_doc("BOM")
bom.item = item_code
@@ -320,8 +339,8 @@ class TestBOM(FrappeTestCase):
def test_bom_recursion_transitive(self):
item1 = "_Test BOM Recursion"
item2 = "_Test BOM Recursion 2"
make_item(item1, {'is_stock_item': 1})
make_item(item2, {'is_stock_item': 1})
make_item(item1, {"is_stock_item": 1})
make_item(item2, {"is_stock_item": 1})
bom1 = frappe.new_doc("BOM")
bom1.item = item1
@@ -377,19 +396,29 @@ class TestBOM(FrappeTestCase):
self.assertRaises(frappe.ValidationError, bom_doc.submit)
def test_bom_item_query(self):
query = partial(item_query, doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters={"is_stock_item": 1})
query = partial(
item_query,
doctype="Item",
txt="",
searchfield="name",
start=0,
page_len=20,
filters={"is_stock_item": 1},
)
test_items = query(txt="_Test")
filtered = query(txt="_Test Item 2")
self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results")
self.assertNotEqual(
len(test_items), len(filtered), msg="Item filtering showing excessive results"
)
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
def test_exclude_exploded_items_from_bom(self):
bom_no = get_default_bom()
new_bom = frappe.copy_doc(frappe.get_doc('BOM', bom_no))
new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no))
for row in new_bom.items:
if row.item_code == '_Test Item Home Desktop Manufactured':
if row.item_code == "_Test Item Home Desktop Manufactured":
self.assertTrue(row.bom_no)
row.do_not_explode = True
@@ -398,13 +427,15 @@ class TestBOM(FrappeTestCase):
new_bom.load_from_db()
for row in new_bom.items:
if row.item_code == '_Test Item Home Desktop Manufactured' and row.do_not_explode:
if row.item_code == "_Test Item Home Desktop Manufactured" and row.do_not_explode:
self.assertFalse(row.bom_no)
new_bom.delete()
def test_valid_transfer_defaults(self):
bom_with_op = frappe.db.get_value("BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1})
bom_with_op = frappe.db.get_value(
"BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}
)
bom = frappe.copy_doc(frappe.get_doc("BOM", bom_with_op), ignore_no_copy=False)
# test defaults
@@ -432,10 +463,107 @@ class TestBOM(FrappeTestCase):
self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete()
def test_bom_name_length(self):
"""test >140 char names"""
bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}}
create_nested_bom(bom_tree, prefix="")
def test_version_index(self):
bom = frappe.new_doc("BOM")
version_index_test_cases = [
(1, []),
(1, ["BOM#XYZ"]),
(2, ["BOM/ITEM/001"]),
(2, ["BOM-ITEM-001"]),
(3, ["BOM-ITEM-001", "BOM-ITEM-002"]),
(4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]),
]
for expected_index, existing_boms in version_index_test_cases:
with self.subTest():
self.assertEqual(
expected_index,
bom.get_next_version_index(existing_boms),
msg=f"Incorrect index for {existing_boms}",
)
def test_bom_versioning(self):
bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}}
bom = create_nested_bom(bom_tree, prefix="")
self.assertEqual(int(bom.name.split("-")[-1]), 1)
original_bom_name = bom.name
bom.cancel()
bom.reload()
self.assertEqual(bom.name, original_bom_name)
# create a new amendment
amendment = frappe.copy_doc(bom)
amendment.docstatus = 0
amendment.amended_from = bom.name
amendment.save()
amendment.submit()
amendment.reload()
self.assertNotEqual(amendment.name, bom.name)
# `origname-001-1` version
self.assertEqual(int(amendment.name.split("-")[-1]), 1)
self.assertEqual(int(amendment.name.split("-")[-2]), 1)
# create a new version
version = frappe.copy_doc(amendment)
version.docstatus = 0
version.amended_from = None
version.save()
self.assertNotEqual(amendment.name, version.name)
self.assertEqual(int(version.name.split("-")[-1]), 2)
def test_clear_inpection_quality(self):
bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
bom.docstatus = 0
bom.is_default = 0
bom.quality_inspection_template = "_Test Quality Inspection Template"
bom.inspection_required = 1
bom.save()
bom.reload()
self.assertEqual(bom.quality_inspection_template, "_Test Quality Inspection Template")
bom.inspection_required = 0
bom.save()
bom.reload()
self.assertEqual(bom.quality_inspection_template, None)
def test_bom_pricing_based_on_lpp(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
parent = frappe.generate_hash(length=10)
child = frappe.generate_hash(length=10)
bom_tree = {parent: {child: {}}}
bom = create_nested_bom(bom_tree, prefix="")
# add last purchase price
make_purchase_receipt(item_code=child, rate=42)
bom = frappe.copy_doc(bom)
bom.docstatus = 0
bom.amended_from = None
bom.rm_cost_as_per = "Last Purchase Rate"
bom.conversion_rate = 1
bom.save()
bom.submit()
self.assertEqual(bom.items[0].rate, 42)
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
def level_order_traversal(node):
traversal = []
q = deque()
@@ -450,9 +578,9 @@ def level_order_traversal(node):
return traversal
def create_nested_bom(tree, prefix="_Test bom "):
""" Helper function to create a simple nested bom from tree describing item names. (along with required items)
"""
"""Helper function to create a simple nested bom from tree describing item names. (along with required items)"""
def create_items(bom_tree):
for item_code, subtree in bom_tree.items():
@@ -460,6 +588,7 @@ def create_nested_bom(tree, prefix="_Test bom "):
if not frappe.db.exists("Item", bom_item_code):
frappe.get_doc(doctype="Item", item_code=bom_item_code, item_group="_Test Item Group").insert()
create_items(subtree)
create_items(tree)
def dfs(tree, node):
@@ -481,6 +610,7 @@ def create_nested_bom(tree, prefix="_Test bom "):
bom = frappe.get_doc(doctype="BOM", item=bom_item_code)
for child_item in child_items.keys():
bom.append("items", {"item_code": prefix + child_item})
bom.company = "_Test Company"
bom.currency = "INR"
bom.insert()
bom.submit()
@@ -493,10 +623,13 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
warehouse_list = [warehouse_list]
if not warehouse_list:
warehouse_list = frappe.db.sql_list("""
warehouse_list = frappe.db.sql_list(
"""
select warehouse from `tabBin`
where item_code=%s and actual_qty > 0
""", item_code)
""",
item_code,
)
if not warehouse_list:
warehouse_list.append("_Test Warehouse - _TC")
@@ -504,44 +637,51 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
for warehouse in warehouse_list:
create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate)
def create_bom_with_process_loss_item(
fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1):
fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1
):
bom_doc = frappe.new_doc("BOM")
bom_doc.item = fg_item.item_code
bom_doc.quantity = fg_qty
bom_doc.append("items", {
"item_code": bom_item.item_code,
"qty": 1,
"uom": bom_item.stock_uom,
"stock_uom": bom_item.stock_uom,
"rate": 100.0
})
bom_doc.append("scrap_items", {
"item_code": fg_item.item_code,
"qty": scrap_qty,
"stock_qty": scrap_qty,
"uom": fg_item.stock_uom,
"stock_uom": fg_item.stock_uom,
"rate": scrap_rate,
"is_process_loss": is_process_loss
})
bom_doc.append(
"items",
{
"item_code": bom_item.item_code,
"qty": 1,
"uom": bom_item.stock_uom,
"stock_uom": bom_item.stock_uom,
"rate": 100.0,
},
)
bom_doc.append(
"scrap_items",
{
"item_code": fg_item.item_code,
"qty": scrap_qty,
"stock_qty": scrap_qty,
"uom": fg_item.stock_uom,
"stock_uom": fg_item.stock_uom,
"rate": scrap_rate,
"is_process_loss": is_process_loss,
},
)
bom_doc.currency = "INR"
return bom_doc
def create_process_loss_bom_items():
item_list = [
("_Test Item - Non Whole UOM", "Kg"),
("_Test Item - Whole UOM", "Unit"),
("_Test PL BOM Item", "Unit")
("_Test PL BOM Item", "Unit"),
]
return [create_process_loss_bom_item(it) for it in item_list]
def create_process_loss_bom_item(item_tuple):
item_code, stock_uom = item_tuple
if frappe.db.exists("Item", item_code) is None:
return make_item(
item_code,
{'stock_uom':stock_uom, 'valuation_rate':100}
)
return make_item(item_code, {"stock_uom": stock_uom, "valuation_rate": 100})
else:
return frappe.get_doc("Item", item_code)

View File

@@ -66,7 +66,8 @@
"label": "Hour Rate",
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
"options": "currency"
"options": "currency",
"precision": "2"
},
{
"description": "In minutes",
@@ -186,7 +187,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-12-15 03:00:00.473173",
"modified": "2022-03-10 06:19:08.462027",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",

View File

@@ -16,8 +16,11 @@ from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
class BOMUpdateTool(Document):
pass
@frappe.whitelist()
def enqueue_replace_bom(boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None) -> "BOMUpdateLog":
def enqueue_replace_bom(
boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None
) -> "BOMUpdateLog":
"""Returns a BOM Update Log (that queues a job) for BOM Replacement."""
boms = boms or args
if isinstance(boms, str):
@@ -26,6 +29,7 @@ def enqueue_replace_bom(boms: Optional[Union[Dict, str]] = None, args: Optional[
update_log = create_bom_update_log(boms=boms)
return update_log
@frappe.whitelist()
def enqueue_update_cost() -> "BOMUpdateLog":
"""Returns a BOM Update Log (that queues a job) for BOM Cost Updation."""
@@ -38,20 +42,26 @@ def auto_update_latest_price_in_all_boms() -> None:
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
update_cost()
def update_cost() -> None:
"""Updates Cost for all BOMs from bottom to top."""
bom_list = get_boms_in_bottom_up_order()
for bom in bom_list:
frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
def create_bom_update_log(boms: Optional[Dict] = None, update_type: str = "Replace BOM") -> "BOMUpdateLog":
def create_bom_update_log(
boms: Optional[Dict] = None, update_type: str = "Replace BOM"
) -> "BOMUpdateLog":
"""Creates a BOM Update Log that handles the background job."""
boms = boms or {}
current_bom = boms.get("current_bom")
new_bom = boms.get("new_bom")
return frappe.get_doc({
"doctype": "BOM Update Log",
"current_bom": current_bom,
"new_bom": new_bom,
"update_type": update_type,
}).submit()
return frappe.get_doc(
{
"doctype": "BOM Update Log",
"current_bom": current_bom,
"new_bom": new_bom,
"update_type": update_type,
}
).submit()

View File

@@ -4,12 +4,13 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import create_item
test_records = frappe.get_test_records('BOM')
test_records = frappe.get_test_records("BOM")
class TestBOMUpdateTool(FrappeTestCase):
def test_replace_bom(self):
@@ -19,10 +20,7 @@ class TestBOMUpdateTool(FrappeTestCase):
bom_doc.items[1].item_code = "_Test Item"
bom_doc.insert()
boms = frappe._dict(
current_bom=current_bom,
new_bom=bom_doc.name
)
boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
replace_bom(boms)
self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom))
@@ -39,10 +37,13 @@ class TestBOMUpdateTool(FrappeTestCase):
if item_doc.valuation_rate != 100.00:
frappe.db.set_value("Item", item_doc.name, "valuation_rate", 100)
bom_no = frappe.db.get_value('BOM', {'item': 'BOM Cost Test Item 1'}, "name")
bom_no = frappe.db.get_value("BOM", {"item": "BOM Cost Test Item 1"}, "name")
if not bom_no:
doc = make_bom(item = 'BOM Cost Test Item 1',
raw_materials =['BOM Cost Test Item 2', 'BOM Cost Test Item 3'], currency="INR")
doc = make_bom(
item="BOM Cost Test Item 1",
raw_materials=["BOM Cost Test Item 2", "BOM Cost Test Item 3"],
currency="INR",
)
else:
doc = frappe.get_doc("BOM", bom_no)

View File

@@ -26,15 +26,27 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
)
class OverlapError(frappe.ValidationError): pass
class OverlapError(frappe.ValidationError):
pass
class OperationMismatchError(frappe.ValidationError):
pass
class OperationSequenceError(frappe.ValidationError):
pass
class JobCardCancelError(frappe.ValidationError):
pass
class OperationMismatchError(frappe.ValidationError): pass
class OperationSequenceError(frappe.ValidationError): pass
class JobCardCancelError(frappe.ValidationError): pass
class JobCard(Document):
def onload(self):
excess_transfer = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
excess_transfer = frappe.db.get_single_value(
"Manufacturing Settings", "job_card_excess_transfer"
)
self.set_onload("job_card_excess_transfer", excess_transfer)
self.set_onload("work_order_closed", self.is_work_order_closed())
@@ -48,27 +60,35 @@ class JobCard(Document):
self.validate_work_order()
def set_sub_operations(self):
if self.operation:
if not self.sub_operations and self.operation:
self.sub_operations = []
for row in frappe.get_all('Sub Operation',
filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'):
row.status = 'Pending'
for row in frappe.get_all(
"Sub Operation",
filters={"parent": self.operation},
fields=["operation", "idx"],
order_by="idx",
):
row.status = "Pending"
row.sub_operation = row.operation
self.append('sub_operations', row)
self.append("sub_operations", row)
def validate_time_logs(self):
self.total_time_in_mins = 0.0
self.total_completed_qty = 0.0
if self.get('time_logs'):
for d in self.get('time_logs'):
if self.get("time_logs"):
for d in self.get("time_logs"):
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
data = self.get_overlap_for(d)
if data:
frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}")
.format(d.idx, self.name, data.name), OverlapError)
frappe.throw(
_("Row {0}: From Time and To Time of {1} is overlapping with {2}").format(
d.idx, self.name, data.name
),
OverlapError,
)
if d.from_time and d.to_time:
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
@@ -86,8 +106,9 @@ class JobCard(Document):
production_capacity = 1
if self.workstation:
production_capacity = frappe.get_cached_value("Workstation",
self.workstation, 'production_capacity') or 1
production_capacity = (
frappe.get_cached_value("Workstation", self.workstation, "production_capacity") or 1
)
validate_overlap_for = " and jc.workstation = %(workstation)s "
if args.get("employee"):
@@ -95,11 +116,12 @@ class JobCard(Document):
production_capacity = 1
validate_overlap_for = " and jctl.employee = %(employee)s "
extra_cond = ''
extra_cond = ""
if check_next_available_slot:
extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)"
existing = frappe.db.sql("""select jc.name as name, jctl.to_time from
existing = frappe.db.sql(
"""select jc.name as name, jctl.to_time from
`tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and
(
(%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or
@@ -107,15 +129,19 @@ class JobCard(Document):
(%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0}
)
and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1}
order by jctl.to_time desc limit 1""".format(extra_cond, validate_overlap_for),
order by jctl.to_time desc limit 1""".format(
extra_cond, validate_overlap_for
),
{
"from_time": args.from_time,
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name",
"employee": args.get("employee"),
"workstation": self.workstation
}, as_dict=True)
"workstation": self.workstation,
},
as_dict=True,
)
if existing and production_capacity > len(existing):
return
@@ -125,10 +151,7 @@ class JobCard(Document):
def schedule_time_logs(self, row):
row.remaining_time_in_mins = row.time_in_mins
while row.remaining_time_in_mins > 0:
args = frappe._dict({
"from_time": row.planned_start_time,
"to_time": row.planned_end_time
})
args = frappe._dict({"from_time": row.planned_start_time, "to_time": row.planned_end_time})
self.validate_overlap_for_workstation(args, row)
self.check_workstation_time(row)
@@ -141,13 +164,16 @@ class JobCard(Document):
def check_workstation_time(self, row):
workstation_doc = frappe.get_cached_doc("Workstation", self.workstation)
if (not workstation_doc.working_hours or
cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime"))):
if not workstation_doc.working_hours or cint(
frappe.db.get_single_value("Manufacturing Settings", "allow_overtime")
):
if get_datetime(row.planned_end_time) < get_datetime(row.planned_start_time):
row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.time_in_mins)
row.remaining_time_in_mins = 0.0
else:
row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time, row.planned_start_time)
row.remaining_time_in_mins -= time_diff_in_minutes(
row.planned_end_time, row.planned_start_time
)
self.update_time_logs(row)
return
@@ -167,14 +193,15 @@ class JobCard(Document):
workstation_start_time = datetime.datetime.combine(start_date, get_time(time_slot.start_time))
workstation_end_time = datetime.datetime.combine(start_date, get_time(time_slot.end_time))
if (get_datetime(row.planned_start_time) >= workstation_start_time and
get_datetime(row.planned_start_time) <= workstation_end_time):
if (
get_datetime(row.planned_start_time) >= workstation_start_time
and get_datetime(row.planned_start_time) <= workstation_end_time
):
time_in_mins = time_diff_in_minutes(workstation_end_time, row.planned_start_time)
# If remaining time fit in workstation time logs else split hours as per workstation time
if time_in_mins > row.remaining_time_in_mins:
row.planned_end_time = add_to_date(row.planned_start_time,
minutes=row.remaining_time_in_mins)
row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.remaining_time_in_mins)
row.remaining_time_in_mins = 0
else:
row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins)
@@ -182,14 +209,16 @@ class JobCard(Document):
self.update_time_logs(row)
if total_idx != (i+1) and row.remaining_time_in_mins > 0:
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[i+1].start_time))
if total_idx != (i + 1) and row.remaining_time_in_mins > 0:
row.planned_start_time = datetime.datetime.combine(
start_date, get_time(workstation_doc.working_hours[i + 1].start_time)
)
if row.remaining_time_in_mins > 0:
start_date = add_days(start_date, 1)
row.planned_start_time = datetime.datetime.combine(start_date,
get_time(workstation_doc.working_hours[0].start_time))
row.planned_start_time = datetime.datetime.combine(
start_date, get_time(workstation_doc.working_hours[0].start_time)
)
def add_time_log(self, args):
last_row = []
@@ -204,21 +233,25 @@ class JobCard(Document):
if last_row and args.get("complete_time"):
for row in self.time_logs:
if not row.to_time:
row.update({
"to_time": get_datetime(args.get("complete_time")),
"operation": args.get("sub_operation"),
"completed_qty": args.get("completed_qty") or 0.0
})
row.update(
{
"to_time": get_datetime(args.get("complete_time")),
"operation": args.get("sub_operation"),
"completed_qty": args.get("completed_qty") or 0.0,
}
)
elif args.get("start_time"):
new_args = frappe._dict({
"from_time": get_datetime(args.get("start_time")),
"operation": args.get("sub_operation"),
"completed_qty": 0.0
})
new_args = frappe._dict(
{
"from_time": get_datetime(args.get("start_time")),
"operation": args.get("sub_operation"),
"completed_qty": 0.0,
}
)
if employees:
for name in employees:
new_args.employee = name.get('employee')
new_args.employee = name.get("employee")
self.add_start_time_log(new_args)
else:
self.add_start_time_log(new_args)
@@ -236,10 +269,7 @@ class JobCard(Document):
def set_employees(self, employees):
for name in employees:
self.append('employee', {
'employee': name.get('employee'),
'completed_qty': 0.0
})
self.append("employee", {"employee": name.get("employee"), "completed_qty": 0.0})
def reset_timer_value(self, args):
self.started_time = None
@@ -263,13 +293,17 @@ class JobCard(Document):
operation_wise_completed_time = {}
for time_log in self.time_logs:
if time_log.operation not in operation_wise_completed_time:
operation_wise_completed_time.setdefault(time_log.operation,
frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
operation_wise_completed_time.setdefault(
time_log.operation,
frappe._dict(
{"status": "Pending", "completed_qty": 0.0, "completed_time": 0.0, "employee": []}
),
)
op_row = operation_wise_completed_time[time_log.operation]
op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
if self.status == 'On Hold':
op_row.status = 'Pause'
if self.status == "On Hold":
op_row.status = "Pause"
op_row.employee.append(time_log.employee)
if time_log.time_in_mins:
@@ -279,7 +313,7 @@ class JobCard(Document):
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
if operation_deatils:
if row.status != 'Complete':
if row.status != "Complete":
row.status = operation_deatils.status
row.completed_time = operation_deatils.completed_time
@@ -289,43 +323,52 @@ class JobCard(Document):
if operation_deatils.completed_qty:
row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
else:
row.status = 'Pending'
row.status = "Pending"
row.completed_time = 0.0
row.completed_qty = 0.0
def update_time_logs(self, row):
self.append("time_logs", {
"from_time": row.planned_start_time,
"to_time": row.planned_end_time,
"completed_qty": 0,
"time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time),
})
self.append(
"time_logs",
{
"from_time": row.planned_start_time,
"to_time": row.planned_end_time,
"completed_qty": 0,
"time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time),
},
)
@frappe.whitelist()
def get_required_items(self):
if not self.get('work_order'):
if not self.get("work_order"):
return
doc = frappe.get_doc('Work Order', self.get('work_order'))
if doc.transfer_material_against == 'Work Order' or doc.skip_transfer:
doc = frappe.get_doc("Work Order", self.get("work_order"))
if doc.transfer_material_against == "Work Order" or doc.skip_transfer:
return
for d in doc.required_items:
if not d.operation:
frappe.throw(_("Row {0} : Operation is required against the raw material item {1}")
.format(d.idx, d.item_code))
frappe.throw(
_("Row {0} : Operation is required against the raw material item {1}").format(
d.idx, d.item_code
)
)
if self.get('operation') == d.operation:
self.append('items', {
"item_code": d.item_code,
"source_warehouse": d.source_warehouse,
"uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
"item_name": d.item_name,
"description": d.description,
"required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
"rate": d.rate,
"amount": d.amount
})
if self.get("operation") == d.operation:
self.append(
"items",
{
"item_code": d.item_code,
"source_warehouse": d.source_warehouse,
"uom": frappe.db.get_value("Item", d.item_code, "stock_uom"),
"item_name": d.item_name,
"description": d.description,
"required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
"rate": d.rate,
"amount": d.amount,
},
)
def on_submit(self):
self.validate_transfer_qty()
@@ -339,31 +382,52 @@ class JobCard(Document):
def validate_transfer_qty(self):
if self.items and self.transferred_qty < self.for_quantity:
frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
.format(self.name))
frappe.throw(
_(
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
).format(self.name)
)
def validate_job_card(self):
if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
.format(get_link_to_form('Work Order', self.work_order)))
if (
self.work_order
and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped"
):
frappe.throw(
_("Transaction not allowed against stopped Work Order {0}").format(
get_link_to_form("Work Order", self.work_order)
)
)
if not self.time_logs:
frappe.throw(_("Time logs are required for {0} {1}")
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
frappe.throw(
_("Time logs are required for {0} {1}").format(
bold("Job Card"), get_link_to_form("Job Card", self.name)
)
)
if self.for_quantity and self.total_completed_qty != self.for_quantity:
total_completed_qty = bold(_("Total Completed Qty"))
qty_to_manufacture = bold(_("Qty to Manufacture"))
frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})")
.format(total_completed_qty, bold(self.total_completed_qty), qty_to_manufacture,bold(self.for_quantity)))
frappe.throw(
_("The {0} ({1}) must be equal to {2} ({3})").format(
total_completed_qty,
bold(self.total_completed_qty),
qty_to_manufacture,
bold(self.for_quantity),
)
)
def update_work_order(self):
if not self.work_order:
return
if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
'add_corrective_operation_cost_in_finished_good_valuation')):
if self.is_corrective_job_card and not cint(
frappe.db.get_single_value(
"Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation"
)
):
return
for_quantity, time_in_mins = 0, 0
@@ -375,7 +439,7 @@ class JobCard(Document):
for_quantity = flt(data[0].completed_qty)
time_in_mins = flt(data[0].time_in_mins)
wo = frappe.get_doc('Work Order', self.work_order)
wo = frappe.get_doc("Work Order", self.work_order)
if self.is_corrective_job_card:
self.update_corrective_in_work_order(wo)
@@ -386,8 +450,11 @@ class JobCard(Document):
def update_corrective_in_work_order(self, wo):
wo.corrective_operation_cost = 0.0
for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
for row in frappe.get_all(
"Job Card",
fields=["total_time_in_mins", "hour_rate"],
filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order},
):
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
wo.calculate_operating_cost()
@@ -395,27 +462,37 @@ class JobCard(Document):
wo.save()
def validate_produced_quantity(self, for_quantity, wo):
if self.docstatus < 2: return
if self.docstatus < 2:
return
if wo.produced_qty > for_quantity:
first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.")
.format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)))
first_part_msg = _(
"The {0} {1} is used to calculate the valuation cost for the finished good {2}."
).format(
frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)
)
second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.")
.format(frappe.bold(get_link_to_form("Work Order", self.work_order))))
second_part_msg = _(
"Kindly cancel the Manufacturing Entries first against the work order {0}."
).format(frappe.bold(get_link_to_form("Work Order", self.work_order)))
frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg),
JobCardCancelError, title = _("Error"))
frappe.throw(
_("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error")
)
def update_work_order_data(self, for_quantity, time_in_mins, wo):
time_data = frappe.db.sql("""
time_data = frappe.db.sql(
"""
SELECT
min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
""", (self.work_order, self.operation_id), as_dict=1)
""",
(self.work_order, self.operation_id),
as_dict=1,
)
for data in wo.operations:
if data.get("name") == self.operation_id:
@@ -434,92 +511,118 @@ class JobCard(Document):
wo.save()
def get_current_operation_data(self):
return frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
"is_corrective_job_card": 0})
return frappe.get_all(
"Job Card",
fields=["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters={
"docstatus": 1,
"work_order": self.work_order,
"operation_id": self.operation_id,
"is_corrective_job_card": 0,
},
)
def set_transferred_qty_in_job_card(self, ste_doc):
for row in ste_doc.items:
if not row.job_card_item: continue
if not row.job_card_item:
continue
qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
qty = frappe.db.sql(
""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
se.purpose = 'Material Transfer for Manufacture'
""", (row.job_card_item))[0][0]
""",
(row.job_card_item),
)[0][0]
frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty))
def set_transferred_qty(self, update_status=False):
"Set total FG Qty for which RM was transferred."
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
doc = frappe.get_doc('Work Order', self.get('work_order'))
if doc.transfer_material_against == 'Work Order' or doc.skip_transfer:
doc = frappe.get_doc("Work Order", self.get("work_order"))
if doc.transfer_material_against == "Work Order" or doc.skip_transfer:
return
if self.items:
# sum of 'For Quantity' of Stock Entries against JC
self.transferred_qty = frappe.db.get_value('Stock Entry', {
'job_card': self.name,
'work_order': self.work_order,
'docstatus': 1,
'purpose': 'Material Transfer for Manufacture'
}, 'sum(fg_completed_qty)') or 0
self.transferred_qty = (
frappe.db.get_value(
"Stock Entry",
{
"job_card": self.name,
"work_order": self.work_order,
"docstatus": 1,
"purpose": "Material Transfer for Manufacture",
},
"sum(fg_completed_qty)",
)
or 0
)
self.db_set("transferred_qty", self.transferred_qty)
qty = 0
if self.work_order:
doc = frappe.get_doc('Work Order', self.work_order)
if doc.transfer_material_against == 'Job Card' and not doc.skip_transfer:
doc = frappe.get_doc("Work Order", self.work_order)
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
completed = True
for d in doc.operations:
if d.status != 'Completed':
if d.status != "Completed":
completed = False
break
if completed:
job_cards = frappe.get_all('Job Card', filters = {'work_order': self.work_order,
'docstatus': ('!=', 2)}, fields = 'sum(transferred_qty) as qty', group_by='operation_id')
job_cards = frappe.get_all(
"Job Card",
filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
fields="sum(transferred_qty) as qty",
group_by="operation_id",
)
if job_cards:
qty = min(d.qty for d in job_cards)
doc.db_set('material_transferred_for_manufacturing', qty)
doc.db_set("material_transferred_for_manufacturing", qty)
self.set_status(update_status)
def set_status(self, update_status=False):
if self.status == "On Hold": return
if self.status == "On Hold":
return
self.status = {
0: "Open",
1: "Submitted",
2: "Cancelled"
}[self.docstatus or 0]
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
if self.for_quantity <= self.transferred_qty:
self.status = "Material Transferred"
if self.time_logs:
self.status = 'Work In Progress'
self.status = "Work In Progress"
if (self.docstatus == 1 and
(self.for_quantity <= self.total_completed_qty or not self.items)):
self.status = 'Completed'
if self.status != 'Completed':
if self.for_quantity <= self.transferred_qty:
self.status = 'Material Transferred'
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
self.status = "Completed"
if update_status:
self.db_set('status', self.status)
self.db_set("status", self.status)
def validate_operation_id(self):
if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and
frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id):
if (
self.get("operation_id")
and self.get("operation_row_number")
and self.operation
and self.work_order
and frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name")
!= self.operation_id
):
work_order = bold(get_link_to_form("Work Order", self.work_order))
frappe.throw(_("Operation {0} does not belong to the work order {1}")
.format(bold(self.operation), work_order), OperationMismatchError)
frappe.throw(
_("Operation {0} does not belong to the work order {1}").format(
bold(self.operation), work_order
),
OperationMismatchError,
)
def validate_sequence_id(self):
if self.is_corrective_job_card:
@@ -535,18 +638,25 @@ class JobCard(Document):
current_operation_qty += flt(self.total_completed_qty)
data = frappe.get_all("Work Order Operation",
fields = ["operation", "status", "completed_qty"],
filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ('<', self.sequence_id)},
order_by = "sequence_id, idx")
data = frappe.get_all(
"Work Order Operation",
fields=["operation", "status", "completed_qty"],
filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)},
order_by="sequence_id, idx",
)
message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(bold(self.name),
bold(get_link_to_form("Work Order", self.work_order)))
message = "Job Card {0}: As per the sequence of the operations in the work order {1}".format(
bold(self.name), bold(get_link_to_form("Work Order", self.work_order))
)
for row in data:
if row.status != "Completed" and row.completed_qty < current_operation_qty:
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
frappe.throw(
_("{0}, complete the operation {1} before the operation {2}.").format(
message, bold(row.operation), bold(self.operation)
),
OperationSequenceError,
)
def validate_work_order(self):
if self.is_work_order_closed():
@@ -554,13 +664,14 @@ class JobCard(Document):
def is_work_order_closed(self):
if self.work_order:
status = frappe.get_value('Work Order', self.work_order)
status = frappe.get_value("Work Order", self.work_order)
if status == "Closed":
return True
return False
@frappe.whitelist()
def make_time_log(args):
if isinstance(args, str):
@@ -571,16 +682,17 @@ def make_time_log(args):
doc.validate_sequence_id()
doc.add_time_log(args)
@frappe.whitelist()
def get_operation_details(work_order, operation):
if work_order and operation:
return frappe.get_all("Work Order Operation", fields = ["name", "idx"],
filters = {
"parent": work_order,
"operation": operation
}
return frappe.get_all(
"Work Order Operation",
fields=["name", "idx"],
filters={"parent": work_order, "operation": operation},
)
@frappe.whitelist()
def get_operations(doctype, txt, searchfield, start, page_len, filters):
if not filters.get("work_order"):
@@ -590,12 +702,16 @@ def get_operations(doctype, txt, searchfield, start, page_len, filters):
if txt:
args["operation"] = ("like", "%{0}%".format(txt))
return frappe.get_all("Work Order Operation",
filters = args,
fields = ["distinct operation as operation"],
limit_start = start,
limit_page_length = page_len,
order_by="idx asc", as_list=1)
return frappe.get_all(
"Work Order Operation",
filters=args,
fields=["distinct operation as operation"],
limit_start=start,
limit_page_length=page_len,
order_by="idx asc",
as_list=1,
)
@frappe.whitelist()
def make_material_request(source_name, target_doc=None):
@@ -605,26 +721,29 @@ def make_material_request(source_name, target_doc=None):
def set_missing_values(source, target):
target.material_request_type = "Material Transfer"
doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": {
"doctype": "Material Request",
"field_map": {
"name": "job_card",
doclist = get_mapped_doc(
"Job Card",
source_name,
{
"Job Card": {
"doctype": "Material Request",
"field_map": {
"name": "job_card",
},
},
"Job Card Item": {
"doctype": "Material Request Item",
"field_map": {"required_qty": "qty", "uom": "stock_uom", "name": "job_card_item"},
"postprocess": update_item,
},
},
"Job Card Item": {
"doctype": "Material Request Item",
"field_map": {
"required_qty": "qty",
"uom": "stock_uom",
"name": "job_card_item"
},
"postprocess": update_item,
}
}, target_doc, set_missing_values)
target_doc,
set_missing_values,
)
return doclist
@frappe.whitelist()
def make_stock_entry(source_name, target_doc=None):
def update_item(source, target, source_parent):
@@ -642,7 +761,7 @@ def make_stock_entry(source_name, target_doc=None):
target.from_bom = 1
# avoid negative 'For Quantity'
pending_fg_qty = flt(source.get('for_quantity', 0)) - flt(source.get('transferred_qty', 0))
pending_fg_qty = flt(source.get("for_quantity", 0)) - flt(source.get("transferred_qty", 0))
target.fg_completed_qty = pending_fg_qty if pending_fg_qty > 0 else 0
target.set_transfer_qty()
@@ -650,36 +769,45 @@ def make_stock_entry(source_name, target_doc=None):
target.set_missing_values()
target.set_stock_entry_type()
wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item")
wo_allows_alternate_item = frappe.db.get_value(
"Work Order", target.work_order, "allow_alternative_item"
)
for item in target.items:
item.allow_alternative_item = int(wo_allows_alternate_item and
frappe.get_cached_value("Item", item.item_code, "allow_alternative_item"))
item.allow_alternative_item = int(
wo_allows_alternate_item
and frappe.get_cached_value("Item", item.item_code, "allow_alternative_item")
)
doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": {
"doctype": "Stock Entry",
"field_map": {
"name": "job_card",
"for_quantity": "fg_completed_qty"
doclist = get_mapped_doc(
"Job Card",
source_name,
{
"Job Card": {
"doctype": "Stock Entry",
"field_map": {"name": "job_card", "for_quantity": "fg_completed_qty"},
},
"Job Card Item": {
"doctype": "Stock Entry Detail",
"field_map": {
"source_warehouse": "s_warehouse",
"required_qty": "qty",
"name": "job_card_item",
},
"postprocess": update_item,
"condition": lambda doc: doc.required_qty > 0,
},
},
"Job Card Item": {
"doctype": "Stock Entry Detail",
"field_map": {
"source_warehouse": "s_warehouse",
"required_qty": "qty",
"name": "job_card_item"
},
"postprocess": update_item,
"condition": lambda doc: doc.required_qty > 0
}
}, target_doc, set_missing_values)
target_doc,
set_missing_values,
)
return doclist
def time_diff_in_minutes(string_ed_date, string_st_date):
return time_diff(string_ed_date, string_st_date).total_seconds() / 60
@frappe.whitelist()
def get_job_details(start, end, filters=None):
events = []
@@ -687,41 +815,49 @@ def get_job_details(start, end, filters=None):
event_color = {
"Completed": "#cdf5a6",
"Material Transferred": "#ffdd9e",
"Work In Progress": "#D3D3D3"
"Work In Progress": "#D3D3D3",
}
from frappe.desk.reportview import get_filters_cond
conditions = get_filters_cond("Job Card", filters, [])
job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
job_cards = frappe.db.sql(
""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
`tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
min(`tabJob Card Time Log`.from_time) as from_time,
max(`tabJob Card Time Log`.to_time) as to_time
FROM `tabJob Card` , `tabJob Card Time Log`
WHERE
`tabJob Card`.name = `tabJob Card Time Log`.parent {0}
group by `tabJob Card`.name""".format(conditions), as_dict=1)
group by `tabJob Card`.name""".format(
conditions
),
as_dict=1,
)
for d in job_cards:
subject_data = []
for field in ["name", "work_order", "remarks"]:
if not d.get(field): continue
subject_data = []
for field in ["name", "work_order", "remarks"]:
if not d.get(field):
continue
subject_data.append(d.get(field))
subject_data.append(d.get(field))
color = event_color.get(d.status)
job_card_data = {
'from_time': d.from_time,
'to_time': d.to_time,
'name': d.name,
'subject': '\n'.join(subject_data),
'color': color if color else "#89bcde"
}
color = event_color.get(d.status)
job_card_data = {
"from_time": d.from_time,
"to_time": d.to_time,
"name": d.name,
"subject": "\n".join(subject_data),
"color": color if color else "#89bcde",
}
events.append(job_card_data)
events.append(job_card_data)
return events
@frappe.whitelist()
def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
def set_missing_values(source, target):
@@ -729,20 +865,26 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
target.operation = operation
target.for_operation = for_operation
target.set('time_logs', [])
target.set('employee', [])
target.set('items', [])
target.set("time_logs", [])
target.set("employee", [])
target.set("items", [])
target.set_sub_operations()
target.get_required_items()
target.validate_time_logs()
doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": {
"doctype": "Job Card",
"field_map": {
"name": "for_job_card",
},
}
}, target_doc, set_missing_values)
doclist = get_mapped_doc(
"Job Card",
source_name,
{
"Job Card": {
"doctype": "Job Card",
"field_map": {
"name": "for_job_card",
},
}
},
target_doc,
set_missing_values,
)
return doclist

View File

@@ -3,18 +3,10 @@ from frappe import _
def get_data():
return {
'fieldname': 'job_card',
'non_standard_fieldnames': {
'Quality Inspection': 'reference_name'
},
'transactions': [
{
'label': _('Transactions'),
'items': ['Material Request', 'Stock Entry']
},
{
'label': _('Reference'),
'items': ['Quality Inspection']
}
]
"fieldname": "job_card",
"non_standard_fieldnames": {"Quality Inspection": "reference_name"},
"transactions": [
{"label": _("Transactions"), "items": ["Material Request", "Stock Entry"]},
{"label": _("Reference"), "items": ["Quality Inspection"]},
],
}

View File

@@ -1,4 +1,5 @@
frappe.listview_settings['Job Card'] = {
has_indicator_for_draft: true,
get_indicator: function(doc) {
if (doc.status === "Work In Progress") {
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];

View File

@@ -20,13 +20,11 @@ class TestJobCard(FrappeTestCase):
transfer_material_against, source_warehouse = None, None
tests_that_skip_setup = (
"test_job_card_material_transfer_correctness",
)
tests_that_skip_setup = ("test_job_card_material_transfer_correctness",)
tests_that_transfer_against_jc = (
"test_job_card_multiple_materials_transfer",
"test_job_card_excess_material_transfer",
"test_job_card_partial_material_transfer"
"test_job_card_partial_material_transfer",
)
if self._testMethodName in tests_that_skip_setup:
@@ -40,7 +38,7 @@ class TestJobCard(FrappeTestCase):
item="_Test FG Item 2",
qty=2,
transfer_material_against=transfer_material_against,
source_warehouse=source_warehouse
source_warehouse=source_warehouse,
)
def tearDown(self):
@@ -48,8 +46,9 @@ class TestJobCard(FrappeTestCase):
def test_job_card(self):
job_cards = frappe.get_all('Job Card',
filters = {'work_order': self.work_order.name}, fields = ["operation_id", "name"])
job_cards = frappe.get_all(
"Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"]
)
if job_cards:
job_card = job_cards[0]
@@ -63,30 +62,38 @@ class TestJobCard(FrappeTestCase):
frappe.delete_doc("Job Card", d.name)
def test_job_card_with_different_work_station(self):
job_cards = frappe.get_all('Job Card',
filters = {'work_order': self.work_order.name},
fields = ["operation_id", "workstation", "name", "for_quantity"])
job_cards = frappe.get_all(
"Job Card",
filters={"work_order": self.work_order.name},
fields=["operation_id", "workstation", "name", "for_quantity"],
)
job_card = job_cards[0]
if job_card:
workstation = frappe.db.get_value("Workstation",
{"name": ("not in", [job_card.workstation])}, "name")
workstation = frappe.db.get_value(
"Workstation", {"name": ("not in", [job_card.workstation])}, "name"
)
if not workstation or job_card.workstation == workstation:
workstation = make_workstation(workstation_name=random_string(5)).name
doc = frappe.get_doc("Job Card", job_card.name)
doc.workstation = workstation
doc.append("time_logs", {
"from_time": "2009-01-01 12:06:25",
"to_time": "2009-01-01 12:37:25",
"time_in_mins": "31.00002",
"completed_qty": job_card.for_quantity
})
doc.append(
"time_logs",
{
"from_time": "2009-01-01 12:06:25",
"to_time": "2009-01-01 12:37:25",
"time_in_mins": "31.00002",
"completed_qty": job_card.for_quantity,
},
)
doc.submit()
completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
completed_qty = frappe.db.get_value(
"Work Order Operation", job_card.operation_id, "completed_qty"
)
self.assertEqual(completed_qty, job_card.for_quantity)
doc.cancel()
@@ -97,51 +104,49 @@ class TestJobCard(FrappeTestCase):
def test_job_card_overlap(self):
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
jc1_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
jc2_name = frappe.db.get_value("Job Card", {'work_order': wo2.name})
jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name})
jc1 = frappe.get_doc("Job Card", jc1_name)
jc2 = frappe.get_doc("Job Card", jc2_name)
employee = "_T-Employee-00001" # from test records
employee = "_T-Employee-00001" # from test records
jc1.append("time_logs", {
"from_time": "2021-01-01 00:00:00",
"to_time": "2021-01-01 08:00:00",
"completed_qty": 1,
"employee": employee,
})
jc1.append(
"time_logs",
{
"from_time": "2021-01-01 00:00:00",
"to_time": "2021-01-01 08:00:00",
"completed_qty": 1,
"employee": employee,
},
)
jc1.save()
# add a new entry in same time slice
jc2.append("time_logs", {
"from_time": "2021-01-01 00:01:00",
"to_time": "2021-01-01 06:00:00",
"completed_qty": 1,
"employee": employee,
})
jc2.append(
"time_logs",
{
"from_time": "2021-01-01 00:01:00",
"to_time": "2021-01-01 06:00:00",
"completed_qty": 1,
"employee": employee,
},
)
self.assertRaises(OverlapError, jc2.save)
def test_job_card_multiple_materials_transfer(self):
"Test transferring RMs separately against Job Card with multiple RMs."
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
make_stock_entry(
item_code="_Test Item",
target="Stores - _TC",
qty=10,
basic_rate=100
)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured",
target="Stores - _TC",
qty=6,
basic_rate=100
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100
)
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
del transfer_entry_1.items[1] # transfer only 1 of 2 RMs
del transfer_entry_1.items[1] # transfer only 1 of 2 RMs
transfer_entry_1.insert()
transfer_entry_1.submit()
@@ -162,13 +167,14 @@ class TestJobCard(FrappeTestCase):
def test_job_card_excess_material_transfer(self):
"Test transferring more than required RM against Job Card."
make_stock_entry(item_code="_Test Item", target="Stores - _TC",
qty=25, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
target="Stores - _TC", qty=15, basic_rate=100)
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
)
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
self.assertEqual(job_card.status, "Open")
# fully transfer both RMs
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
@@ -192,11 +198,10 @@ class TestJobCard(FrappeTestCase):
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
job_card.append("time_logs", {
"from_time": "2021-01-01 00:01:00",
"to_time": "2021-01-01 06:00:00",
"completed_qty": 2
})
job_card.append(
"time_logs",
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
)
job_card.save()
job_card.submit()
@@ -206,12 +211,12 @@ class TestJobCard(FrappeTestCase):
def test_job_card_partial_material_transfer(self):
"Test partial material transfer against Job Card"
make_stock_entry(item_code="_Test Item", target="Stores - _TC",
qty=25, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop Manufactured",
target="Stores - _TC", qty=15, basic_rate=100)
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
make_stock_entry(
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
)
job_card_name = frappe.db.get_value("Job Card", {'work_order': self.work_order.name})
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
job_card = frappe.get_doc("Job Card", job_card_name)
# partially transfer
@@ -241,15 +246,14 @@ class TestJobCard(FrappeTestCase):
def test_job_card_material_transfer_correctness(self):
"""
1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
2. Test impact of changing 'For Qty' in such a Stock Entry
1. Test if only current Job Card Items are pulled in a Stock Entry against a Job Card
2. Test impact of changing 'For Qty' in such a Stock Entry
"""
create_bom_with_multiple_operations()
work_order = make_wo_with_transfer_against_jc()
job_card_name = frappe.db.get_value(
"Job Card",
{"work_order": work_order.name,"operation": "Test Operation A"}
"Job Card", {"work_order": work_order.name, "operation": "Test Operation A"}
)
job_card = frappe.get_doc("Job Card", job_card_name)
@@ -275,6 +279,7 @@ class TestJobCard(FrappeTestCase):
# rollback via tearDown method
def create_bom_with_multiple_operations():
"Create a BOM with multiple operations and Material Transfer against Job Card"
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
@@ -286,19 +291,22 @@ def create_bom_with_multiple_operations():
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate_rent": 300,
"time_in_mins": 60
"time_in_mins": 60,
}
make_workstation(row)
make_operation(row)
bom_doc.append("operations", {
"operation": "Test Operation A",
"description": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate": 300,
"time_in_mins": 60,
"operating_cost": 100
})
bom_doc.append(
"operations",
{
"operation": "Test Operation A",
"description": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate": 300,
"time_in_mins": 60,
"operating_cost": 100,
},
)
bom_doc.transfer_material_against = "Job Card"
bom_doc.save()
@@ -306,6 +314,7 @@ def create_bom_with_multiple_operations():
return bom_doc
def make_wo_with_transfer_against_jc():
"Create a WO with multiple operations and Material Transfer against Job Card"
@@ -314,7 +323,7 @@ def make_wo_with_transfer_against_jc():
qty=4,
transfer_material_against="Job Card",
source_warehouse="Stores - _TC",
do_not_submit=True
do_not_submit=True,
)
work_order.required_items[0].operation = "Test Operation A"
work_order.required_items[1].operation = "_Test Operation 1"
@@ -322,8 +331,9 @@ def make_wo_with_transfer_against_jc():
return work_order
def make_bom_for_jc_tests():
test_records = frappe.get_test_records('BOM')
test_records = frappe.get_test_records("BOM")
bom = frappe.copy_doc(test_records[2])
bom.set_rate_of_sub_assembly_item_based_on_bom = 0
bom.rm_cost_as_per = "Valuation Rate"

View File

@@ -11,14 +11,19 @@ from frappe.utils import cint
class ManufacturingSettings(Document):
pass
def get_mins_between_operations():
return relativedelta(minutes=cint(frappe.db.get_single_value("Manufacturing Settings",
"mins_between_operations")) or 10)
return relativedelta(
minutes=cint(frappe.db.get_single_value("Manufacturing Settings", "mins_between_operations"))
or 10
)
@frappe.whitelist()
def is_material_consumption_enabled():
if not hasattr(frappe.local, 'material_consumption'):
frappe.local.material_consumption = cint(frappe.db.get_single_value('Manufacturing Settings',
'material_consumption'))
if not hasattr(frappe.local, "material_consumption"):
frappe.local.material_consumption = cint(
frappe.db.get_single_value("Manufacturing Settings", "material_consumption")
)
return frappe.local.material_consumption

View File

@@ -19,12 +19,14 @@ class Operation(Document):
operation_list = []
for row in self.sub_operations:
if row.operation in operation_list:
frappe.throw(_("The operation {0} can not add multiple times")
.format(frappe.bold(row.operation)))
frappe.throw(
_("The operation {0} can not add multiple times").format(frappe.bold(row.operation))
)
if self.name == row.operation:
frappe.throw(_("The operation {0} can not be the sub operation")
.format(frappe.bold(row.operation)))
frappe.throw(
_("The operation {0} can not be the sub operation").format(frappe.bold(row.operation))
)
operation_list.append(row.operation)

View File

@@ -3,11 +3,6 @@ from frappe import _
def get_data():
return {
'fieldname': 'operation',
'transactions': [
{
'label': _('Manufacture'),
'items': ['BOM', 'Work Order', 'Job Card']
}
]
"fieldname": "operation",
"transactions": [{"label": _("Manufacture"), "items": ["BOM", "Work Order", "Job Card"]}],
}

View File

@@ -5,11 +5,13 @@ import unittest
import frappe
test_records = frappe.get_test_records('Operation')
test_records = frappe.get_test_records("Operation")
class TestOperation(unittest.TestCase):
pass
def make_operation(*args, **kwargs):
args = args if args else kwargs
if isinstance(args, tuple):
@@ -18,11 +20,9 @@ def make_operation(*args, **kwargs):
args = frappe._dict(args)
if not frappe.db.exists("Operation", args.operation):
doc = frappe.get_doc({
"doctype": "Operation",
"name": args.operation,
"workstation": args.workstation
})
doc = frappe.get_doc(
{"doctype": "Operation", "name": args.operation, "workstation": args.workstation}
)
doc.insert()
return doc

View File

@@ -2,6 +2,13 @@
// For license information, please see license.txt
frappe.ui.form.on('Production Plan', {
before_save: function(frm) {
// preserve temporary names on production plan item to re-link sub-assembly items
frm.doc.po_items.forEach(item => {
item.temporary_name = item.name;
});
},
setup: function(frm) {
frm.custom_make_buttons = {
'Work Order': 'Work Order / Subcontract PO',

View File

@@ -190,7 +190,7 @@
"label": "Select Items to Manufacture"
},
{
"depends_on": "get_items_from",
"depends_on": "eval:doc.get_items_from && doc.docstatus == 0",
"fieldname": "get_items",
"fieldtype": "Button",
"label": "Get Finished Goods for Manufacture"
@@ -198,6 +198,7 @@
{
"fieldname": "po_items",
"fieldtype": "Table",
"label": "Assembly Items",
"no_copy": 1,
"options": "Production Plan Item",
"reqd": 1
@@ -357,6 +358,7 @@
"options": "Production Plan Sub Assembly Item"
},
{
"depends_on": "eval:doc.po_items && doc.po_items.length && doc.docstatus == 0",
"fieldname": "get_sub_assembly_items",
"fieldtype": "Button",
"label": "Get Sub Assembly Items"
@@ -382,7 +384,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-02-23 17:16:10.629378",
"modified": "2022-03-25 09:15:25.017664",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
@@ -404,5 +406,6 @@
}
],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@@ -3,15 +3,9 @@ from frappe import _
def get_data():
return {
'fieldname': 'production_plan',
'transactions': [
{
'label': _('Transactions'),
'items': ['Work Order', 'Material Request']
},
{
'label': _('Subcontract'),
'items': ['Purchase Order']
},
]
"fieldname": "production_plan",
"transactions": [
{"label": _("Transactions"), "items": ["Work Order", "Material Request"]},
{"label": _("Subcontract"), "items": ["Purchase Order"]},
],
}

View File

@@ -21,80 +21,88 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
class TestProductionPlan(FrappeTestCase):
def setUp(self):
for item in ['Test Production Item 1', 'Subassembly Item 1',
'Raw Material Item 1', 'Raw Material Item 2']:
for item in [
"Test Production Item 1",
"Subassembly Item 1",
"Raw Material Item 1",
"Raw Material Item 2",
]:
create_item(item, valuation_rate=100)
sr = frappe.db.get_value('Stock Reconciliation Item',
{'item_code': item, 'docstatus': 1}, 'parent')
sr = frappe.db.get_value(
"Stock Reconciliation Item", {"item_code": item, "docstatus": 1}, "parent"
)
if sr:
sr_doc = frappe.get_doc('Stock Reconciliation', sr)
sr_doc = frappe.get_doc("Stock Reconciliation", sr)
sr_doc.cancel()
create_item('Test Non Stock Raw Material', is_stock_item=0)
for item, raw_materials in {'Subassembly Item 1': ['Raw Material Item 1', 'Raw Material Item 2'],
'Test Production Item 1': ['Raw Material Item 1', 'Subassembly Item 1',
'Test Non Stock Raw Material']}.items():
if not frappe.db.get_value('BOM', {'item': item}):
make_bom(item = item, raw_materials = raw_materials)
create_item("Test Non Stock Raw Material", is_stock_item=0)
for item, raw_materials in {
"Subassembly Item 1": ["Raw Material Item 1", "Raw Material Item 2"],
"Test Production Item 1": [
"Raw Material Item 1",
"Subassembly Item 1",
"Test Non Stock Raw Material",
],
}.items():
if not frappe.db.get_value("BOM", {"item": item}):
make_bom(item=item, raw_materials=raw_materials)
def tearDown(self) -> None:
frappe.db.rollback()
def test_production_plan_mr_creation(self):
"Test if MRs are created for unavailable raw materials."
pln = create_production_plan(item_code='Test Production Item 1')
pln = create_production_plan(item_code="Test Production Item 1")
self.assertTrue(len(pln.mr_items), 2)
pln.make_material_request()
pln.reload()
self.assertTrue(pln.status, 'Material Requested')
self.assertTrue(pln.status, "Material Requested")
material_requests = frappe.get_all(
'Material Request Item',
fields = ['distinct parent'],
filters = {'production_plan': pln.name},
as_list=1
"Material Request Item",
fields=["distinct parent"],
filters={"production_plan": pln.name},
as_list=1,
)
self.assertTrue(len(material_requests), 2)
pln.make_work_order()
work_orders = frappe.get_all('Work Order', fields = ['name'],
filters = {'production_plan': pln.name}, as_list=1)
work_orders = frappe.get_all(
"Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1
)
self.assertTrue(len(work_orders), len(pln.po_items))
for name in material_requests:
mr = frappe.get_doc('Material Request', name[0])
mr = frappe.get_doc("Material Request", name[0])
if mr.docstatus != 0:
mr.cancel()
for name in work_orders:
mr = frappe.delete_doc('Work Order', name[0])
mr = frappe.delete_doc("Work Order", name[0])
pln = frappe.get_doc('Production Plan', pln.name)
pln = frappe.get_doc("Production Plan", pln.name)
pln.cancel()
def test_production_plan_start_date(self):
"Test if Work Order has same Planned Start Date as Prod Plan."
planned_date = add_to_date(date=None, days=3)
plan = create_production_plan(
item_code='Test Production Item 1',
planned_start_date=planned_date
item_code="Test Production Item 1", planned_start_date=planned_date
)
plan.make_work_order()
work_orders = frappe.get_all(
'Work Order',
fields = ['name', 'planned_start_date'],
filters = {'production_plan': plan.name}
"Work Order", fields=["name", "planned_start_date"], filters={"production_plan": plan.name}
)
self.assertEqual(work_orders[0].planned_start_date, planned_date)
for wo in work_orders:
frappe.delete_doc('Work Order', wo.name)
frappe.delete_doc("Work Order", wo.name)
plan.reload()
plan.cancel()
@@ -104,15 +112,14 @@ class TestProductionPlan(FrappeTestCase):
- Enable 'ignore_existing_ordered_qty'.
- Test if MR Planning table pulls Raw Material Qty even if it is in stock.
"""
sr1 = create_stock_reconciliation(item_code="Raw Material Item 1",
target="_Test Warehouse - _TC", qty=1, rate=110)
sr2 = create_stock_reconciliation(item_code="Raw Material Item 2",
target="_Test Warehouse - _TC", qty=1, rate=120)
pln = create_production_plan(
item_code='Test Production Item 1',
ignore_existing_ordered_qty=1
sr1 = create_stock_reconciliation(
item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=110
)
sr2 = create_stock_reconciliation(
item_code="Raw Material Item 2", target="_Test Warehouse - _TC", qty=1, rate=120
)
pln = create_production_plan(item_code="Test Production Item 1", ignore_existing_ordered_qty=1)
self.assertTrue(len(pln.mr_items))
self.assertTrue(flt(pln.mr_items[0].quantity), 1.0)
@@ -122,19 +129,13 @@ class TestProductionPlan(FrappeTestCase):
def test_production_plan_with_non_stock_item(self):
"Test if MR Planning table includes Non Stock RM."
pln = create_production_plan(
item_code='Test Production Item 1',
include_non_stock_items=1
)
pln = create_production_plan(item_code="Test Production Item 1", include_non_stock_items=1)
self.assertTrue(len(pln.mr_items), 3)
pln.cancel()
def test_production_plan_without_multi_level(self):
"Test MR Planning table for non exploded BOM."
pln = create_production_plan(
item_code='Test Production Item 1',
use_multi_level_bom=0
)
pln = create_production_plan(item_code="Test Production Item 1", use_multi_level_bom=0)
self.assertTrue(len(pln.mr_items), 2)
pln.cancel()
@@ -144,15 +145,15 @@ class TestProductionPlan(FrappeTestCase):
- Test if MR Planning table avoids pulling Raw Material Qty as it is in stock for
non exploded BOM.
"""
sr1 = create_stock_reconciliation(item_code="Raw Material Item 1",
target="_Test Warehouse - _TC", qty=1, rate=130)
sr2 = create_stock_reconciliation(item_code="Subassembly Item 1",
target="_Test Warehouse - _TC", qty=1, rate=140)
sr1 = create_stock_reconciliation(
item_code="Raw Material Item 1", target="_Test Warehouse - _TC", qty=1, rate=130
)
sr2 = create_stock_reconciliation(
item_code="Subassembly Item 1", target="_Test Warehouse - _TC", qty=1, rate=140
)
pln = create_production_plan(
item_code='Test Production Item 1',
use_multi_level_bom=0,
ignore_existing_ordered_qty=0
item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=0
)
self.assertFalse(len(pln.mr_items))
@@ -162,73 +163,86 @@ class TestProductionPlan(FrappeTestCase):
def test_production_plan_sales_orders(self):
"Test if previously fulfilled SO (with WO) is pulled into Prod Plan."
item = 'Test Production Item 1'
item = "Test Production Item 1"
so = make_sales_order(item_code=item, qty=1)
sales_order = so.name
sales_order_item = so.items[0].name
pln = frappe.new_doc('Production Plan')
pln = frappe.new_doc("Production Plan")
pln.company = so.company
pln.get_items_from = 'Sales Order'
pln.get_items_from = "Sales Order"
pln.append('sales_orders', {
'sales_order': so.name,
'sales_order_date': so.transaction_date,
'customer': so.customer,
'grand_total': so.grand_total
})
pln.append(
"sales_orders",
{
"sales_order": so.name,
"sales_order_date": so.transaction_date,
"customer": so.customer,
"grand_total": so.grand_total,
},
)
pln.get_so_items()
pln.submit()
pln.make_work_order()
work_order = frappe.db.get_value('Work Order', {'sales_order': sales_order,
'production_plan': pln.name, 'sales_order_item': sales_order_item}, 'name')
work_order = frappe.db.get_value(
"Work Order",
{"sales_order": sales_order, "production_plan": pln.name, "sales_order_item": sales_order_item},
"name",
)
wo_doc = frappe.get_doc('Work Order', work_order)
wo_doc.update({
'wip_warehouse': 'Work In Progress - _TC',
'fg_warehouse': 'Finished Goods - _TC'
})
wo_doc = frappe.get_doc("Work Order", work_order)
wo_doc.update(
{"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"}
)
wo_doc.submit()
so_wo_qty = frappe.db.get_value('Sales Order Item', sales_order_item, 'work_order_qty')
so_wo_qty = frappe.db.get_value("Sales Order Item", sales_order_item, "work_order_qty")
self.assertTrue(so_wo_qty, 5)
pln = frappe.new_doc('Production Plan')
pln.update({
'from_date': so.transaction_date,
'to_date': so.transaction_date,
'customer': so.customer,
'item_code': item,
'sales_order_status': so.status
})
pln = frappe.new_doc("Production Plan")
pln.update(
{
"from_date": so.transaction_date,
"to_date": so.transaction_date,
"customer": so.customer,
"item_code": item,
"sales_order_status": so.status,
}
)
sales_orders = get_sales_orders(pln) or {}
sales_orders = [d.get('name') for d in sales_orders if d.get('name') == sales_order]
sales_orders = [d.get("name") for d in sales_orders if d.get("name") == sales_order]
self.assertEqual(sales_orders, [])
def test_production_plan_combine_items(self):
"Test combining FG items in Production Plan."
item = 'Test Production Item 1'
item = "Test Production Item 1"
so1 = make_sales_order(item_code=item, qty=1)
pln = frappe.new_doc('Production Plan')
pln = frappe.new_doc("Production Plan")
pln.company = so1.company
pln.get_items_from = 'Sales Order'
pln.append('sales_orders', {
'sales_order': so1.name,
'sales_order_date': so1.transaction_date,
'customer': so1.customer,
'grand_total': so1.grand_total
})
pln.get_items_from = "Sales Order"
pln.append(
"sales_orders",
{
"sales_order": so1.name,
"sales_order_date": so1.transaction_date,
"customer": so1.customer,
"grand_total": so1.grand_total,
},
)
so2 = make_sales_order(item_code=item, qty=2)
pln.append('sales_orders', {
'sales_order': so2.name,
'sales_order_date': so2.transaction_date,
'customer': so2.customer,
'grand_total': so2.grand_total
})
pln.append(
"sales_orders",
{
"sales_order": so2.name,
"sales_order_date": so2.transaction_date,
"customer": so2.customer,
"grand_total": so2.grand_total,
},
)
pln.combine_items = 1
pln.get_items()
pln.submit()
@@ -236,26 +250,31 @@ class TestProductionPlan(FrappeTestCase):
self.assertTrue(pln.po_items[0].planned_qty, 3)
pln.make_work_order()
work_order = frappe.db.get_value('Work Order', {
'production_plan_item': pln.po_items[0].name,
'production_plan': pln.name
}, 'name')
work_order = frappe.db.get_value(
"Work Order",
{"production_plan_item": pln.po_items[0].name, "production_plan": pln.name},
"name",
)
wo_doc = frappe.get_doc('Work Order', work_order)
wo_doc.update({
'wip_warehouse': 'Work In Progress - _TC',
})
wo_doc = frappe.get_doc("Work Order", work_order)
wo_doc.update(
{
"wip_warehouse": "Work In Progress - _TC",
}
)
wo_doc.submit()
so_items = []
for plan_reference in pln.prod_plan_references:
so_items.append(plan_reference.sales_order_item)
so_wo_qty = frappe.db.get_value('Sales Order Item', plan_reference.sales_order_item, 'work_order_qty')
so_wo_qty = frappe.db.get_value(
"Sales Order Item", plan_reference.sales_order_item, "work_order_qty"
)
self.assertEqual(so_wo_qty, plan_reference.qty)
wo_doc.cancel()
for so_item in so_items:
so_wo_qty = frappe.db.get_value('Sales Order Item', so_item, 'work_order_qty')
so_wo_qty = frappe.db.get_value("Sales Order Item", so_item, "work_order_qty")
self.assertEqual(so_wo_qty, 0.0)
pln.reload()
@@ -269,12 +288,8 @@ class TestProductionPlan(FrappeTestCase):
"""
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
bom_tree_1 = {
"Red-Car": {"Wheel": {"Rubber": {}}}
}
bom_tree_2 = {
"Green-Car": {"Wheel": {"Rubber": {}}}
}
bom_tree_1 = {"Red-Car": {"Wheel": {"Rubber": {}}}}
bom_tree_2 = {"Green-Car": {"Wheel": {"Rubber": {}}}}
parent_bom_1 = create_nested_bom(bom_tree_1, prefix="")
parent_bom_2 = create_nested_bom(bom_tree_2, prefix="")
@@ -284,20 +299,23 @@ class TestProductionPlan(FrappeTestCase):
frappe.db.set_value("BOM Item", parent_bom_2.items[0].name, "bom_no", subassembly_bom)
plan = create_production_plan(item_code="Red-Car", use_multi_level_bom=1, do_not_save=True)
plan.append("po_items", { # Add Green-Car to Prod Plan
'use_multi_level_bom': 1,
'item_code': "Green-Car",
'bom_no': frappe.db.get_value('Item', "Green-Car", 'default_bom'),
'planned_qty': 1,
'planned_start_date': now_datetime()
})
plan.append(
"po_items",
{ # Add Green-Car to Prod Plan
"use_multi_level_bom": 1,
"item_code": "Green-Car",
"bom_no": frappe.db.get_value("Item", "Green-Car", "default_bom"),
"planned_qty": 1,
"planned_start_date": now_datetime(),
},
)
plan.get_sub_assembly_items()
self.assertTrue(len(plan.sub_assembly_items), 2)
plan.combine_sub_items = 1
plan.get_sub_assembly_items()
self.assertTrue(len(plan.sub_assembly_items), 1) # check if sub-assembly items merged
self.assertTrue(len(plan.sub_assembly_items), 1) # check if sub-assembly items merged
self.assertEqual(plan.sub_assembly_items[0].qty, 2.0)
self.assertEqual(plan.sub_assembly_items[0].stock_qty, 2.0)
@@ -307,25 +325,29 @@ class TestProductionPlan(FrappeTestCase):
self.assertTrue(len(plan.sub_assembly_items), 2)
def test_pp_to_mr_customer_provided(self):
" Test Material Request from Production Plan for Customer Provided Item."
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
create_item('Production Item CUST')
"Test Material Request from Production Plan for Customer Provided Item."
create_item(
"CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0
)
create_item("Production Item CUST")
for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items():
if not frappe.db.get_value('BOM', {'item': item}):
make_bom(item = item, raw_materials = raw_materials)
production_plan = create_production_plan(item_code = 'Production Item CUST')
for item, raw_materials in {
"Production Item CUST": ["Raw Material Item 1", "CUST-0987"]
}.items():
if not frappe.db.get_value("BOM", {"item": item}):
make_bom(item=item, raw_materials=raw_materials)
production_plan = create_production_plan(item_code="Production Item CUST")
production_plan.make_material_request()
material_request = frappe.db.get_value(
'Material Request Item',
{'production_plan': production_plan.name, 'item_code': 'CUST-0987'},
'parent'
"Material Request Item",
{"production_plan": production_plan.name, "item_code": "CUST-0987"},
"parent",
)
mr = frappe.get_doc('Material Request', material_request)
mr = frappe.get_doc("Material Request", material_request)
self.assertTrue(mr.material_request_type, 'Customer Provided')
self.assertTrue(mr.customer, '_Test Customer')
self.assertTrue(mr.material_request_type, "Customer Provided")
self.assertTrue(mr.customer, "_Test Customer")
def test_production_plan_with_multi_level_bom(self):
"""
@@ -339,33 +361,34 @@ class TestProductionPlan(FrappeTestCase):
create_item(item_code, is_stock_item=1)
# created bom upto 3 level
if not frappe.db.get_value('BOM', {'item': "Test BOM 3"}):
make_bom(item = "Test BOM 3", raw_materials = ["Test RM BOM 1"], rm_qty=3)
if not frappe.db.get_value("BOM", {"item": "Test BOM 3"}):
make_bom(item="Test BOM 3", raw_materials=["Test RM BOM 1"], rm_qty=3)
if not frappe.db.get_value('BOM', {'item': "Test BOM 2"}):
make_bom(item = "Test BOM 2", raw_materials = ["Test BOM 3"], rm_qty=3)
if not frappe.db.get_value("BOM", {"item": "Test BOM 2"}):
make_bom(item="Test BOM 2", raw_materials=["Test BOM 3"], rm_qty=3)
if not frappe.db.get_value('BOM', {'item': "Test BOM 1"}):
make_bom(item = "Test BOM 1", raw_materials = ["Test BOM 2"], rm_qty=2)
if not frappe.db.get_value("BOM", {"item": "Test BOM 1"}):
make_bom(item="Test BOM 1", raw_materials=["Test BOM 2"], rm_qty=2)
item_code = "Test BOM 1"
pln = frappe.new_doc('Production Plan')
pln = frappe.new_doc("Production Plan")
pln.company = "_Test Company"
pln.append("po_items", {
"item_code": item_code,
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
"planned_qty": 3
})
pln.append(
"po_items",
{
"item_code": item_code,
"bom_no": frappe.db.get_value("BOM", {"item": "Test BOM 1"}),
"planned_qty": 3,
},
)
pln.get_sub_assembly_items('In House')
pln.get_sub_assembly_items("In House")
pln.submit()
pln.make_work_order()
#last level sub-assembly work order produce qty
# last level sub-assembly work order produce qty
to_produce_qty = frappe.db.get_value(
"Work Order",
{"production_plan": pln.name, "production_item": "Test BOM 3"},
"qty"
"Work Order", {"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty"
)
self.assertEqual(to_produce_qty, 18.0)
@@ -374,70 +397,72 @@ class TestProductionPlan(FrappeTestCase):
def test_get_warehouse_list_group(self):
"Check if required child warehouses are returned."
warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]'
warehouse_json = '[{"warehouse":"_Test Warehouse Group - _TC"}]'
warehouses = set(get_warehouse_list(warehouse_json))
expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"}
missing_warehouse = expected_warehouses - warehouses
self.assertTrue(len(missing_warehouse) == 0,
msg=f"Following warehouses were expected {', '.join(missing_warehouse)}")
self.assertTrue(
len(missing_warehouse) == 0,
msg=f"Following warehouses were expected {', '.join(missing_warehouse)}",
)
def test_get_warehouse_list_single(self):
"Check if same warehouse is returned in absence of child warehouses."
warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]'
warehouse_json = '[{"warehouse":"_Test Scrap Warehouse - _TC"}]'
warehouses = set(get_warehouse_list(warehouse_json))
expected_warehouses = {"_Test Scrap Warehouse - _TC", }
expected_warehouses = {
"_Test Scrap Warehouse - _TC",
}
self.assertEqual(warehouses, expected_warehouses)
def test_get_sales_order_with_variant(self):
"Check if Template BOM is fetched in absence of Variant BOM."
rm_item = create_item('PIV_RM', valuation_rate = 100)
if not frappe.db.exists('Item', {"item_code": 'PIV'}):
item = create_item('PIV', valuation_rate = 100)
rm_item = create_item("PIV_RM", valuation_rate=100)
if not frappe.db.exists("Item", {"item_code": "PIV"}):
item = create_item("PIV", valuation_rate=100)
variant_settings = {
"attributes": [
{
"attribute": "Colour"
},
{"attribute": "Colour"},
],
"has_variants": 1
"has_variants": 1,
}
item.update(variant_settings)
item.save()
parent_bom = make_bom(item = 'PIV', raw_materials = [rm_item.item_code])
if not frappe.db.exists('BOM', {"item": 'PIV'}):
parent_bom = make_bom(item = 'PIV', raw_materials = [rm_item.item_code])
parent_bom = make_bom(item="PIV", raw_materials=[rm_item.item_code])
if not frappe.db.exists("BOM", {"item": "PIV"}):
parent_bom = make_bom(item="PIV", raw_materials=[rm_item.item_code])
else:
parent_bom = frappe.get_doc('BOM', {"item": 'PIV'})
parent_bom = frappe.get_doc("BOM", {"item": "PIV"})
if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}):
if not frappe.db.exists("Item", {"item_code": "PIV-RED"}):
variant = create_variant("PIV", {"Colour": "Red"})
variant.save()
variant_bom = make_bom(item = variant.item_code, raw_materials = [rm_item.item_code])
variant_bom = make_bom(item=variant.item_code, raw_materials=[rm_item.item_code])
else:
variant = frappe.get_doc('Item', 'PIV-RED')
if not frappe.db.exists('BOM', {"item": 'PIV-RED'}):
variant_bom = make_bom(item = variant.item_code, raw_materials = [rm_item.item_code])
variant = frappe.get_doc("Item", "PIV-RED")
if not frappe.db.exists("BOM", {"item": "PIV-RED"}):
variant_bom = make_bom(item=variant.item_code, raw_materials=[rm_item.item_code])
"""Testing when item variant has a BOM"""
so = make_sales_order(item_code="PIV-RED", qty=5)
pln = frappe.new_doc('Production Plan')
pln = frappe.new_doc("Production Plan")
pln.company = so.company
pln.get_items_from = 'Sales Order'
pln.item_code = 'PIV-RED'
pln.get_items_from = "Sales Order"
pln.item_code = "PIV-RED"
pln.get_open_sales_orders()
self.assertEqual(pln.sales_orders[0].sales_order, so.name)
pln.get_so_items()
self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
self.assertEqual(pln.po_items[0].item_code, "PIV-RED")
self.assertEqual(pln.po_items[0].bom_no, variant_bom.name)
so.cancel()
frappe.delete_doc('Sales Order', so.name)
frappe.delete_doc("Sales Order", so.name)
variant_bom.cancel()
frappe.delete_doc('BOM', variant_bom.name)
frappe.delete_doc("BOM", variant_bom.name)
"""Testing when item variant doesn't have a BOM"""
so = make_sales_order(item_code="PIV-RED", qty=5)
@@ -445,7 +470,7 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(pln.sales_orders[0].sales_order, so.name)
pln.po_items = []
pln.get_so_items()
self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
self.assertEqual(pln.po_items[0].item_code, "PIV-RED")
self.assertEqual(pln.po_items[0].bom_no, parent_bom.name)
frappe.db.rollback()
@@ -457,27 +482,35 @@ class TestProductionPlan(FrappeTestCase):
prefix = "_TestLevel_"
boms = {
"Assembly": {
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
"SubAssembly1": {
"ChildPart1": {},
"ChildPart2": {},
},
"ChildPart6": {},
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
},
"MegaDeepAssy": {
"SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},},
# ^ assert that this is
# first item in subassy table
}
"SecretSubassy": {
"SecretPart": {"VerySecret": {"SuperSecret": {"Classified": {}}}},
},
# ^ assert that this is
# first item in subassy table
},
}
create_nested_bom(boms, prefix=prefix)
items = [prefix + item_code for item_code in boms.keys()]
plan = create_production_plan(item_code=items[0], do_not_save=True)
plan.append("po_items", {
'use_multi_level_bom': 1,
'item_code': items[1],
'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'),
'planned_qty': 1,
'planned_start_date': now_datetime()
})
plan.append(
"po_items",
{
"use_multi_level_bom": 1,
"item_code": items[1],
"bom_no": frappe.db.get_value("Item", items[1], "default_bom"),
"planned_qty": 1,
"planned_start_date": now_datetime(),
},
)
plan.get_sub_assembly_items()
bom_level_order = [d.bom_level for d in plan.sub_assembly_items]
@@ -487,6 +520,7 @@ class TestProductionPlan(FrappeTestCase):
def test_multiple_work_order_for_production_plan_item(self):
"Test producing Prod Plan (making WO) in parts."
def create_work_order(item, pln, qty):
# Get Production Items
items_data = pln.get_production_items()
@@ -497,14 +531,13 @@ class TestProductionPlan(FrappeTestCase):
# Create and Submit Work Order for each item in items_data
for key, item in items_data.items():
if pln.sub_assembly_items:
item['use_multi_level_bom'] = 0
item["use_multi_level_bom"] = 0
wo_name = pln.create_work_order(item)
wo_doc = frappe.get_doc("Work Order", wo_name)
wo_doc.update({
'wip_warehouse': 'Work In Progress - _TC',
'fg_warehouse': 'Finished Goods - _TC'
})
wo_doc.update(
{"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"}
)
wo_doc.submit()
wo_list.append(wo_name)
@@ -554,34 +587,30 @@ class TestProductionPlan(FrappeTestCase):
make_stock_entry as make_se_from_wo,
)
make_stock_entry(item_code="Raw Material Item 1",
target="Work In Progress - _TC",
qty=2, basic_rate=100
make_stock_entry(
item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100
)
make_stock_entry(item_code="Raw Material Item 2",
target="Work In Progress - _TC",
qty=2, basic_rate=100
make_stock_entry(
item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100
)
item = 'Test Production Item 1'
item = "Test Production Item 1"
so = make_sales_order(item_code=item, qty=1)
pln = create_production_plan(
company=so.company,
get_items_from="Sales Order",
sales_order=so,
skip_getting_mr_items=True
company=so.company, get_items_from="Sales Order", sales_order=so, skip_getting_mr_items=True
)
self.assertEqual(pln.po_items[0].pending_qty, 1)
wo = make_wo_order_test_record(
item_code=item, qty=1,
item_code=item,
qty=1,
company=so.company,
wip_warehouse='Work In Progress - _TC',
fg_warehouse='Finished Goods - _TC',
wip_warehouse="Work In Progress - _TC",
fg_warehouse="Finished Goods - _TC",
skip_transfer=1,
use_multi_level_bom=1,
do_not_submit=True
do_not_submit=True,
)
wo.production_plan = pln.name
wo.production_plan_item = pln.po_items[0].name
@@ -604,29 +633,25 @@ class TestProductionPlan(FrappeTestCase):
make_stock_entry as make_se_from_wo,
)
make_stock_entry(item_code="Raw Material Item 1",
target="Work In Progress - _TC",
qty=2, basic_rate=100
make_stock_entry(
item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100
)
make_stock_entry(item_code="Raw Material Item 2",
target="Work In Progress - _TC",
qty=2, basic_rate=100
make_stock_entry(
item_code="Raw Material Item 2", target="Work In Progress - _TC", qty=2, basic_rate=100
)
pln = create_production_plan(
item_code='Test Production Item 1',
skip_getting_mr_items=True
)
pln = create_production_plan(item_code="Test Production Item 1", skip_getting_mr_items=True)
self.assertEqual(pln.po_items[0].pending_qty, 1)
wo = make_wo_order_test_record(
item_code='Test Production Item 1', qty=1,
item_code="Test Production Item 1",
qty=1,
company=pln.company,
wip_warehouse='Work In Progress - _TC',
fg_warehouse='Finished Goods - _TC',
wip_warehouse="Work In Progress - _TC",
fg_warehouse="Finished Goods - _TC",
skip_transfer=1,
use_multi_level_bom=1,
do_not_submit=True
do_not_submit=True,
)
wo.production_plan = pln.name
wo.production_plan_item = pln.po_items[0].name
@@ -644,17 +669,57 @@ class TestProductionPlan(FrappeTestCase):
def test_qty_based_status(self):
pp = frappe.new_doc("Production Plan")
pp.po_items = [
frappe._dict(planned_qty=5, produce_qty=4)
]
pp.po_items = [frappe._dict(planned_qty=5, produce_qty=4)]
self.assertFalse(pp.all_items_completed())
pp.po_items = [
frappe._dict(planned_qty=5, produce_qty=10),
frappe._dict(planned_qty=5, produce_qty=4)
frappe._dict(planned_qty=5, produce_qty=4),
]
self.assertFalse(pp.all_items_completed())
def test_production_plan_planned_qty(self):
pln = create_production_plan(item_code="_Test FG Item", planned_qty=0.55)
pln.make_work_order()
work_order = frappe.db.get_value("Work Order", {"production_plan": pln.name}, "name")
wo_doc = frappe.get_doc("Work Order", work_order)
wo_doc.update(
{"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"}
)
wo_doc.submit()
self.assertEqual(wo_doc.qty, 0.55)
def test_temporary_name_relinking(self):
pp = frappe.new_doc("Production Plan")
# this can not be unittested so mocking data that would be expected
# from client side.
for _ in range(10):
po_item = pp.append(
"po_items",
{
"name": frappe.generate_hash(length=10),
"temporary_name": frappe.generate_hash(length=10),
},
)
pp.append("sub_assembly_items", {"production_plan_item": po_item.temporary_name})
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
# bad links should be erased
pp.append("sub_assembly_items", {"production_plan_item": frappe.generate_hash(length=16)})
pp._rename_temporary_references()
self.assertIsNone(pp.sub_assembly_items[-1].production_plan_item)
pp.sub_assembly_items.pop()
# reattempting on same doc shouldn't change anything
pp._rename_temporary_references()
for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items):
self.assertEqual(po_item.name, subassy_item.production_plan_item)
def create_production_plan(**args):
"""
@@ -664,40 +729,48 @@ def create_production_plan(**args):
"""
args = frappe._dict(args)
pln = frappe.get_doc({
'doctype': 'Production Plan',
'company': args.company or '_Test Company',
'customer': args.customer or '_Test Customer',
'posting_date': nowdate(),
'include_non_stock_items': args.include_non_stock_items or 0,
'include_subcontracted_items': args.include_subcontracted_items or 0,
'ignore_existing_ordered_qty': args.ignore_existing_ordered_qty or 0,
'get_items_from': 'Sales Order'
})
pln = frappe.get_doc(
{
"doctype": "Production Plan",
"company": args.company or "_Test Company",
"customer": args.customer or "_Test Customer",
"posting_date": nowdate(),
"include_non_stock_items": args.include_non_stock_items or 0,
"include_subcontracted_items": args.include_subcontracted_items or 0,
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
"get_items_from": "Sales Order",
}
)
if not args.get("sales_order"):
pln.append('po_items', {
'use_multi_level_bom': args.use_multi_level_bom or 1,
'item_code': args.item_code,
'bom_no': frappe.db.get_value('Item', args.item_code, 'default_bom'),
'planned_qty': args.planned_qty or 1,
'planned_start_date': args.planned_start_date or now_datetime()
})
pln.append(
"po_items",
{
"use_multi_level_bom": args.use_multi_level_bom or 1,
"item_code": args.item_code,
"bom_no": frappe.db.get_value("Item", args.item_code, "default_bom"),
"planned_qty": args.planned_qty or 1,
"planned_start_date": args.planned_start_date or now_datetime(),
},
)
if args.get("get_items_from") == "Sales Order" and args.get("sales_order"):
so = args.get("sales_order")
pln.append('sales_orders', {
'sales_order': so.name,
'sales_order_date': so.transaction_date,
'customer': so.customer,
'grand_total': so.grand_total
})
pln.append(
"sales_orders",
{
"sales_order": so.name,
"sales_order_date": so.transaction_date,
"customer": so.customer,
"grand_total": so.grand_total,
},
)
pln.get_items()
if not args.get("skip_getting_mr_items"):
mr_items = get_items_for_material_requests(pln.as_dict())
for d in mr_items:
pln.append('mr_items', d)
pln.append("mr_items", d)
if not args.do_not_save:
pln.insert()
@@ -706,31 +779,37 @@ def create_production_plan(**args):
return pln
def make_bom(**args):
args = frappe._dict(args)
bom = frappe.get_doc({
'doctype': 'BOM',
'is_default': 1,
'item': args.item,
'currency': args.currency or 'USD',
'quantity': args.quantity or 1,
'company': args.company or '_Test Company',
'routing': args.routing,
'with_operations': args.with_operations or 0
})
bom = frappe.get_doc(
{
"doctype": "BOM",
"is_default": 1,
"item": args.item,
"currency": args.currency or "USD",
"quantity": args.quantity or 1,
"company": args.company or "_Test Company",
"routing": args.routing,
"with_operations": args.with_operations or 0,
}
)
for item in args.raw_materials:
item_doc = frappe.get_doc('Item', item)
item_doc = frappe.get_doc("Item", item)
bom.append('items', {
'item_code': item,
'qty': args.rm_qty or 1.0,
'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom,
'rate': item_doc.valuation_rate or args.rate,
'source_warehouse': args.source_warehouse
})
bom.append(
"items",
{
"item_code": item,
"qty": args.rm_qty or 1.0,
"uom": item_doc.stock_uom,
"stock_uom": item_doc.stock_uom,
"rate": item_doc.valuation_rate or args.rate,
"source_warehouse": args.source_warehouse,
},
)
if not args.do_not_save:
bom.insert(ignore_permissions=True)

View File

@@ -27,7 +27,8 @@
"material_request",
"material_request_item",
"product_bundle_item",
"item_reference"
"item_reference",
"temporary_name"
],
"fields": [
{
@@ -204,17 +205,25 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Item Reference"
},
{
"fieldname": "temporary_name",
"fieldtype": "Data",
"hidden": 1,
"label": "temporary name"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-06-28 18:31:06.822168",
"modified": "2022-03-24 04:54:09.940224",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@@ -19,9 +19,11 @@ class Routing(Document):
def calculate_operating_cost(self):
for operation in self.operations:
if not operation.hour_rate:
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60,
operation.precision("operating_cost"))
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, "hour_rate")
operation.operating_cost = flt(
flt(operation.hour_rate) * flt(operation.time_in_mins) / 60,
operation.precision("operating_cost"),
)
def set_routing_id(self):
sequence_id = 0
@@ -29,7 +31,10 @@ class Routing(Document):
if not row.sequence_id:
row.sequence_id = sequence_id + 1
elif sequence_id and row.sequence_id and cint(sequence_id) > cint(row.sequence_id):
frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}")
.format(row.idx, row.sequence_id, sequence_id))
frappe.throw(
_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}").format(
row.idx, row.sequence_id, sequence_id
)
)
sequence_id = row.sequence_id

View File

@@ -1,9 +1,2 @@
def get_data():
return {
'fieldname': 'routing',
'transactions': [
{
'items': ['BOM']
}
]
}
return {"fieldname": "routing", "transactions": [{"items": ["BOM"]}]}

View File

@@ -16,24 +16,27 @@ class TestRouting(FrappeTestCase):
@classmethod
def tearDownClass(cls):
frappe.db.sql('delete from tabBOM where item=%s', cls.item_code)
frappe.db.sql("delete from tabBOM where item=%s", cls.item_code)
def test_sequence_id(self):
operations = [{"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30},
{"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20}]
operations = [
{"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30},
{"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20},
]
make_test_records("UOM")
setup_operations(operations)
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
bom_doc = setup_bom(item_code=self.item_code, routing=routing_doc.name)
wo_doc = make_wo_order_test_record(production_item = self.item_code, bom_no=bom_doc.name)
wo_doc = make_wo_order_test_record(production_item=self.item_code, bom_no=bom_doc.name)
for row in routing_doc.operations:
self.assertEqual(row.sequence_id, row.idx)
for data in frappe.get_all("Job Card",
filters={"work_order": wo_doc.name}, order_by="sequence_id desc"):
for data in frappe.get_all(
"Job Card", filters={"work_order": wo_doc.name}, order_by="sequence_id desc"
):
job_card_doc = frappe.get_doc("Job Card", data.name)
job_card_doc.time_logs[0].completed_qty = 10
if job_card_doc.sequence_id != 1:
@@ -52,33 +55,25 @@ class TestRouting(FrappeTestCase):
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate_rent": 300,
"hour_rate_labour": 750 ,
"time_in_mins": 30
"hour_rate_labour": 750,
"time_in_mins": 30,
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation B",
"hour_rate_labour": 200,
"hour_rate_rent": 1000,
"time_in_mins": 20
}
"time_in_mins": 20,
},
]
test_routing_operations = [
{
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"time_in_mins": 30
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation A",
"time_in_mins": 20
}
{"operation": "Test Operation A", "workstation": "_Test Workstation A", "time_in_mins": 30},
{"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 20},
]
setup_operations(operations)
routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR')
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
self.assertEqual(routing_doc.operations[0].time_in_mins, 30)
self.assertEqual(routing_doc.operations[1].time_in_mins, 20)
routing_doc.operations[0].time_in_mins = 90
@@ -93,10 +88,12 @@ class TestRouting(FrappeTestCase):
def setup_operations(rows):
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
for row in rows:
make_workstation(row)
make_operation(row)
def create_routing(**args):
args = frappe._dict(args)
@@ -108,7 +105,7 @@ def create_routing(**args):
doc.insert()
except frappe.DuplicateEntryError:
doc = frappe.get_doc("Routing", args.routing_name)
doc.delete_key('operations')
doc.delete_key("operations")
for operation in args.operations:
doc.append("operations", operation)
@@ -116,28 +113,35 @@ def create_routing(**args):
return doc
def setup_bom(**args):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
args = frappe._dict(args)
if not frappe.db.exists('Item', args.item_code):
make_item(args.item_code, {
'is_stock_item': 1
})
if not frappe.db.exists("Item", args.item_code):
make_item(args.item_code, {"is_stock_item": 1})
if not args.raw_materials:
if not frappe.db.exists('Item', "Test Extra Item N-1"):
make_item("Test Extra Item N-1", {
'is_stock_item': 1,
})
if not frappe.db.exists("Item", "Test Extra Item N-1"):
make_item(
"Test Extra Item N-1",
{
"is_stock_item": 1,
},
)
args.raw_materials = ['Test Extra Item N-1']
args.raw_materials = ["Test Extra Item N-1"]
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
name = frappe.db.get_value("BOM", {"item": args.item_code}, "name")
if not name:
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
routing = args.routing, with_operations=1, currency = args.currency)
bom_doc = make_bom(
item=args.item_code,
raw_materials=args.get("raw_materials"),
routing=args.routing,
with_operations=1,
currency=args.currency,
)
else:
bom_doc = frappe.get_doc("BOM", name)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,18 +3,10 @@ from frappe import _
def get_data():
return {
'fieldname': 'work_order',
'non_standard_fieldnames': {
'Batch': 'reference_name'
},
'transactions': [
{
'label': _('Transactions'),
'items': ['Stock Entry', 'Job Card', 'Pick List']
},
{
'label': _('Reference'),
'items': ['Serial No', 'Batch']
}
]
"fieldname": "work_order",
"non_standard_fieldnames": {"Batch": "reference_name"},
"transactions": [
{"label": _("Transactions"), "items": ["Stock Entry", "Job Card", "Pick List"]},
{"label": _("Reference"), "items": ["Serial No", "Batch"]},
],
}

View File

@@ -9,5 +9,6 @@ from frappe.model.document import Document
class WorkOrderItem(Document):
pass
def on_doctype_update():
frappe.db.add_index("Work Order Item", ["item_code", "source_warehouse"])

View File

@@ -13,19 +13,42 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
)
test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation')
make_test_records('Workstation')
test_records = frappe.get_test_records("Workstation")
make_test_records("Workstation")
class TestWorkstation(FrappeTestCase):
def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
self.assertRaises(NotInWorkingHoursError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00")
self.assertRaises(NotInWorkingHoursError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00")
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
check_if_within_operating_hours(
"_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00"
)
check_if_within_operating_hours(
"_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00"
)
self.assertRaises(
NotInWorkingHoursError,
check_if_within_operating_hours,
"_Test Workstation 1",
"Operation 1",
"2013-02-02 05:00:00",
"2013-02-02 20:00:00",
)
self.assertRaises(
NotInWorkingHoursError,
check_if_within_operating_hours,
"_Test Workstation 1",
"Operation 1",
"2013-02-02 05:00:00",
"2013-02-02 20:00:00",
)
self.assertRaises(
WorkstationHolidayError,
check_if_within_operating_hours,
"_Test Workstation 1",
"Operation 1",
"2013-02-01 10:00:00",
"2013-02-02 20:00:00",
)
def test_update_bom_operation_rate(self):
operations = [
@@ -33,14 +56,14 @@ class TestWorkstation(FrappeTestCase):
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"hour_rate_rent": 300,
"time_in_mins": 60
"time_in_mins": 60,
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation B",
"hour_rate_rent": 1000,
"time_in_mins": 60
}
"time_in_mins": 60,
},
]
for row in operations:
@@ -48,21 +71,13 @@ class TestWorkstation(FrappeTestCase):
make_operation(row)
test_routing_operations = [
{
"operation": "Test Operation A",
"workstation": "_Test Workstation A",
"time_in_mins": 60
},
{
"operation": "Test Operation B",
"workstation": "_Test Workstation A",
"time_in_mins": 60
}
{"operation": "Test Operation A", "workstation": "_Test Workstation A", "time_in_mins": 60},
{"operation": "Test Operation B", "workstation": "_Test Workstation A", "time_in_mins": 60},
]
routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations)
routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
w1 = frappe.get_doc("Workstation", "_Test Workstation A")
#resets values
# resets values
w1.hour_rate_rent = 300
w1.hour_rate_labour = 0
w1.save()
@@ -72,13 +87,14 @@ class TestWorkstation(FrappeTestCase):
self.assertEqual(bom_doc.operations[0].hour_rate, 300)
w1.hour_rate_rent = 250
w1.save()
#updating after setting new rates in workstations
# updating after setting new rates in workstations
bom_doc.update_cost()
bom_doc.reload()
self.assertEqual(w1.hour_rate, 250)
self.assertEqual(bom_doc.operations[0].hour_rate, 250)
self.assertEqual(bom_doc.operations[1].hour_rate, 250)
def make_workstation(*args, **kwargs):
args = args if args else kwargs
if isinstance(args, tuple):
@@ -88,10 +104,7 @@ def make_workstation(*args, **kwargs):
workstation_name = args.workstation_name or args.workstation
if not frappe.db.exists("Workstation", workstation_name):
doc = frappe.get_doc({
"doctype": "Workstation",
"workstation_name": workstation_name
})
doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name})
doc.hour_rate_rent = args.get("hour_rate_rent")
doc.hour_rate_labour = args.get("hour_rate_labour")
doc.insert()

View File

@@ -19,14 +19,26 @@ from frappe.utils import (
from erpnext.support.doctype.issue.issue import get_holidays
class WorkstationHolidayError(frappe.ValidationError): pass
class NotInWorkingHoursError(frappe.ValidationError): pass
class OverlapError(frappe.ValidationError): pass
class WorkstationHolidayError(frappe.ValidationError):
pass
class NotInWorkingHoursError(frappe.ValidationError):
pass
class OverlapError(frappe.ValidationError):
pass
class Workstation(Document):
def validate(self):
self.hour_rate = (flt(self.hour_rate_labour) + flt(self.hour_rate_electricity) +
flt(self.hour_rate_consumable) + flt(self.hour_rate_rent))
self.hour_rate = (
flt(self.hour_rate_labour)
+ flt(self.hour_rate_electricity)
+ flt(self.hour_rate_consumable)
+ flt(self.hour_rate_rent)
)
def on_update(self):
self.validate_overlap_for_operation_timings()
@@ -35,29 +47,41 @@ class Workstation(Document):
def validate_overlap_for_operation_timings(self):
"""Check if there is no overlap in setting Workstation Operating Hours"""
for d in self.get("working_hours"):
existing = frappe.db.sql_list("""select idx from `tabWorkstation Working Hour`
existing = frappe.db.sql_list(
"""select idx from `tabWorkstation Working Hour`
where parent = %s and name != %s
and (
(start_time between %s and %s) or
(end_time between %s and %s) or
(%s between start_time and end_time))
""", (self.name, d.name, d.start_time, d.end_time, d.start_time, d.end_time, d.start_time))
""",
(self.name, d.name, d.start_time, d.end_time, d.start_time, d.end_time, d.start_time),
)
if existing:
frappe.throw(_("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError)
frappe.throw(
_("Row #{0}: Timings conflicts with row {1}").format(d.idx, comma_and(existing)), OverlapError
)
def update_bom_operation(self):
bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation`
where workstation = %s and parenttype = 'routing' """, self.name)
bom_list = frappe.db.sql(
"""select DISTINCT parent from `tabBOM Operation`
where workstation = %s and parenttype = 'routing' """,
self.name,
)
for bom_no in bom_list:
frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s
frappe.db.sql(
"""update `tabBOM Operation` set hour_rate = %s
where parent = %s and workstation = %s""",
(self.hour_rate, bom_no[0], self.name))
(self.hour_rate, bom_no[0], self.name),
)
def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False):
if not skip_holiday_list_check and (not self.holiday_list or
cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"))):
if not skip_holiday_list_check and (
not self.holiday_list
or cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"))
):
return schedule_date
if schedule_date in tuple(get_holidays(self.holiday_list)):
@@ -66,18 +90,25 @@ class Workstation(Document):
return schedule_date
@frappe.whitelist()
def get_default_holiday_list():
return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list")
return frappe.get_cached_value(
"Company", frappe.defaults.get_user_default("Company"), "default_holiday_list"
)
def check_if_within_operating_hours(workstation, operation, from_datetime, to_datetime):
if from_datetime and to_datetime:
if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")):
if not cint(
frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")
):
check_workstation_for_holiday(workstation, from_datetime, to_datetime)
if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")):
is_within_operating_hours(workstation, operation, from_datetime, to_datetime)
def is_within_operating_hours(workstation, operation, from_datetime, to_datetime):
operation_length = time_diff_in_seconds(to_datetime, from_datetime)
workstation = frappe.get_doc("Workstation", workstation)
@@ -87,21 +118,35 @@ def is_within_operating_hours(workstation, operation, from_datetime, to_datetime
for working_hour in workstation.working_hours:
if working_hour.start_time and working_hour.end_time:
slot_length = (to_timedelta(working_hour.end_time or "") - to_timedelta(working_hour.start_time or "")).total_seconds()
slot_length = (
to_timedelta(working_hour.end_time or "") - to_timedelta(working_hour.start_time or "")
).total_seconds()
if slot_length >= operation_length:
return
frappe.throw(_("Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations").format(operation, workstation.name), NotInWorkingHoursError)
frappe.throw(
_(
"Operation {0} longer than any available working hours in workstation {1}, break down the operation into multiple operations"
).format(operation, workstation.name),
NotInWorkingHoursError,
)
def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list")
if holiday_list and from_datetime and to_datetime:
applicable_holidays = []
for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s
for d in frappe.db.sql(
"""select holiday_date from `tabHoliday` where parent = %s
and holiday_date between %s and %s """,
(holiday_list, getdate(from_datetime), getdate(to_datetime))):
applicable_holidays.append(formatdate(d[0]))
(holiday_list, getdate(from_datetime), getdate(to_datetime)),
):
applicable_holidays.append(formatdate(d[0]))
if applicable_holidays:
frappe.throw(_("Workstation is closed on the following dates as per Holiday List: {0}")
.format(holiday_list) + "\n" + "\n".join(applicable_holidays), WorkstationHolidayError)
frappe.throw(
_("Workstation is closed on the following dates as per Holiday List: {0}").format(holiday_list)
+ "\n"
+ "\n".join(applicable_holidays),
WorkstationHolidayError,
)

View File

@@ -3,17 +3,22 @@ from frappe import _
def get_data():
return {
'fieldname': 'workstation',
'transactions': [
"fieldname": "workstation",
"transactions": [
{"label": _("Master"), "items": ["BOM", "Routing", "Operation"]},
{
'label': _('Master'),
'items': ['BOM', 'Routing', 'Operation']
"label": _("Transaction"),
"items": [
"Work Order",
"Job Card",
],
},
{
'label': _('Transaction'),
'items': ['Work Order', 'Job Card',]
}
],
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
'Work Order', 'Job Card',]
"disable_create_buttons": [
"BOM",
"Routing",
"Operation",
"Work Order",
"Job Card",
],
}

View File

@@ -11,30 +11,37 @@ def execute(filters=None):
get_data(filters, data)
return columns, data
def get_data(filters, data):
get_exploded_items(filters.bom, data)
def get_exploded_items(bom, data, indent=0, qty=1):
exploded_items = frappe.get_all("BOM Item",
exploded_items = frappe.get_all(
"BOM Item",
filters={"parent": bom},
fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom'])
fields=["qty", "bom_no", "qty", "scrap", "item_code", "item_name", "description", "uom"],
)
for item in exploded_items:
print(item.bom_no, indent)
item["indent"] = indent
data.append({
'item_code': item.item_code,
'item_name': item.item_name,
'indent': indent,
'bom_level': indent,
'bom': item.bom_no,
'qty': item.qty * qty,
'uom': item.uom,
'description': item.description,
'scrap': item.scrap
})
data.append(
{
"item_code": item.item_code,
"item_name": item.item_name,
"indent": indent,
"bom_level": indent,
"bom": item.bom_no,
"qty": item.qty * qty,
"uom": item.uom,
"description": item.description,
"scrap": item.scrap,
}
)
if item.bom_no:
get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty)
get_exploded_items(item.bom_no, data, indent=indent + 1, qty=item.qty)
def get_columns():
return [
@@ -43,49 +50,13 @@ def get_columns():
"fieldtype": "Link",
"fieldname": "item_code",
"width": 300,
"options": "Item"
},
{
"label": "Item Name",
"fieldtype": "data",
"fieldname": "item_name",
"width": 100
},
{
"label": "BOM",
"fieldtype": "Link",
"fieldname": "bom",
"width": 150,
"options": "BOM"
},
{
"label": "Qty",
"fieldtype": "data",
"fieldname": "qty",
"width": 100
},
{
"label": "UOM",
"fieldtype": "data",
"fieldname": "uom",
"width": 100
},
{
"label": "BOM Level",
"fieldtype": "Int",
"fieldname": "bom_level",
"width": 100
},
{
"label": "Standard Description",
"fieldtype": "data",
"fieldname": "description",
"width": 150
},
{
"label": "Scrap",
"fieldtype": "data",
"fieldname": "scrap",
"width": 100
"options": "Item",
},
{"label": "Item Name", "fieldtype": "data", "fieldname": "item_name", "width": 100},
{"label": "BOM", "fieldtype": "Link", "fieldname": "bom", "width": 150, "options": "BOM"},
{"label": "Qty", "fieldtype": "data", "fieldname": "qty", "width": 100},
{"label": "UOM", "fieldtype": "data", "fieldname": "uom", "width": 100},
{"label": "BOM Level", "fieldtype": "Int", "fieldname": "bom_level", "width": 100},
{"label": "Standard Description", "fieldtype": "data", "fieldname": "description", "width": 150},
{"label": "Scrap", "fieldtype": "data", "fieldname": "scrap", "width": 100},
]

View File

@@ -11,6 +11,7 @@ def execute(filters=None):
columns = get_columns(filters)
return columns, data
def get_data(filters):
bom_wise_data = {}
bom_data, report_data = [], []
@@ -24,11 +25,9 @@ def get_data(filters):
bom_data.append(d.name)
row.update(d)
else:
row.update({
"operation": d.operation,
"workstation": d.workstation,
"time_in_mins": d.time_in_mins
})
row.update(
{"operation": d.operation, "workstation": d.workstation, "time_in_mins": d.time_in_mins}
)
# maintain BOM wise data for grouping such as:
# {"BOM A": [{Row1}, {Row2}], "BOM B": ...}
@@ -43,20 +42,25 @@ def get_data(filters):
return report_data
def get_filtered_data(filters):
bom = frappe.qb.DocType("BOM")
bom_ops = frappe.qb.DocType("BOM Operation")
bom_ops_query = (
frappe.qb.from_(bom)
.join(bom_ops).on(bom.name == bom_ops.parent)
.join(bom_ops)
.on(bom.name == bom_ops.parent)
.select(
bom.name, bom.item, bom.item_name, bom.uom,
bom_ops.operation, bom_ops.workstation, bom_ops.time_in_mins
).where(
(bom.docstatus == 1)
& (bom.is_active == 1)
bom.name,
bom.item,
bom.item_name,
bom.uom,
bom_ops.operation,
bom_ops.workstation,
bom_ops.time_in_mins,
)
.where((bom.docstatus == 1) & (bom.is_active == 1))
)
if filters.get("item_code"):
@@ -66,18 +70,20 @@ def get_filtered_data(filters):
bom_ops_query = bom_ops_query.where(bom.name.isin(filters.get("bom_id")))
if filters.get("workstation"):
bom_ops_query = bom_ops_query.where(
bom_ops.workstation == filters.get("workstation")
)
bom_ops_query = bom_ops_query.where(bom_ops.workstation == filters.get("workstation"))
bom_operation_data = bom_ops_query.run(as_dict=True)
return bom_operation_data
def get_bom_count(bom_data):
data = frappe.get_all("BOM Item",
data = frappe.get_all(
"BOM Item",
fields=["count(name) as count", "bom_no"],
filters= {"bom_no": ("in", bom_data)}, group_by = "bom_no")
filters={"bom_no": ("in", bom_data)},
group_by="bom_no",
)
bom_count = {}
for d in data:
@@ -85,58 +91,42 @@ def get_bom_count(bom_data):
return bom_count
def get_args():
return frappe._dict({
"name": "",
"item": "",
"item_name": "",
"uom": ""
})
return frappe._dict({"name": "", "item": "", "item_name": "", "uom": ""})
def get_columns(filters):
return [{
"label": _("BOM ID"),
"options": "BOM",
"fieldname": "name",
"fieldtype": "Link",
"width": 220
}, {
"label": _("Item Code"),
"options": "Item",
"fieldname": "item",
"fieldtype": "Link",
"width": 150
}, {
"label": _("Item Name"),
"fieldname": "item_name",
"fieldtype": "Data",
"width": 110
}, {
"label": _("UOM"),
"options": "UOM",
"fieldname": "uom",
"fieldtype": "Link",
"width": 100
}, {
"label": _("Operation"),
"options": "Operation",
"fieldname": "operation",
"fieldtype": "Link",
"width": 140
}, {
"label": _("Workstation"),
"options": "Workstation",
"fieldname": "workstation",
"fieldtype": "Link",
"width": 110
}, {
"label": _("Time (In Mins)"),
"fieldname": "time_in_mins",
"fieldtype": "Float",
"width": 120
}, {
"label": _("Sub-assembly BOM Count"),
"fieldname": "used_as_subassembly_items",
"fieldtype": "Int",
"width": 200
}]
return [
{"label": _("BOM ID"), "options": "BOM", "fieldname": "name", "fieldtype": "Link", "width": 220},
{
"label": _("Item Code"),
"options": "Item",
"fieldname": "item",
"fieldtype": "Link",
"width": 150,
},
{"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 110},
{"label": _("UOM"), "options": "UOM", "fieldname": "uom", "fieldtype": "Link", "width": 100},
{
"label": _("Operation"),
"options": "Operation",
"fieldname": "operation",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Workstation"),
"options": "Workstation",
"fieldname": "workstation",
"fieldtype": "Link",
"width": 110,
},
{"label": _("Time (In Mins)"), "fieldname": "time_in_mins", "fieldtype": "Float", "width": 120},
{
"label": _("Sub-assembly BOM Count"),
"fieldname": "used_as_subassembly_items",
"fieldtype": "Int",
"width": 200,
},
]

View File

@@ -23,14 +23,24 @@ def execute(filters=None):
summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details))
return columns, summ_data
def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
to_build = row.to_build if row.to_build > 0 else 0
diff_qty = to_build - reqd_qty
return [row.item_code, row.description,
comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer', []), add_quotes=False),
comma_and(manufacture_details.get(row.item_code, {}).get('manufacturer_part', []), add_quotes=False),
row.actual_qty, str(to_build),
reqd_qty, diff_qty, last_pur_price]
return [
row.item_code,
row.description,
comma_and(manufacture_details.get(row.item_code, {}).get("manufacturer", []), add_quotes=False),
comma_and(
manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False
),
row.actual_qty,
str(to_build),
reqd_qty,
diff_qty,
last_pur_price,
]
def get_columns():
"""return columns"""
@@ -41,12 +51,13 @@ def get_columns():
_("Manufacturer Part Number") + "::250",
_("Qty") + ":Float:50",
_("Stock Qty") + ":Float:100",
_("Reqd Qty")+ ":Float:100",
_("Diff Qty")+ ":Float:100",
_("Last Purchase Price")+ ":Float:100",
_("Reqd Qty") + ":Float:100",
_("Diff Qty") + ":Float:100",
_("Last Purchase Price") + ":Float:100",
]
return columns
def get_bom_stock(filters):
conditions = ""
bom = filters.get("bom")
@@ -59,18 +70,23 @@ def get_bom_stock(filters):
qty_field = "stock_qty"
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
if warehouse_details:
conditions += " and exists (select name from `tabWarehouse` wh \
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" % (warehouse_details.lft,
warehouse_details.rgt)
conditions += (
" and exists (select name from `tabWarehouse` wh \
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
% (warehouse_details.lft, warehouse_details.rgt)
)
else:
conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
else:
conditions += ""
return frappe.db.sql("""
return frappe.db.sql(
"""
SELECT
bom_item.item_code,
bom_item.description,
@@ -86,14 +102,21 @@ def get_bom_stock(filters):
WHERE
bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
GROUP BY bom_item.item_code""".format(
qty_field=qty_field, table=table, conditions=conditions, bom=bom
),
as_dict=1,
)
def get_manufacturer_records():
details = frappe.get_all('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "item_code"])
details = frappe.get_all(
"Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"]
)
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get('item_code'), {})
dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
dic = manufacture_details.setdefault(detail.get("item_code"), {})
dic.setdefault("manufacturer", []).append(detail.get("manufacturer"))
dic.setdefault("manufacturer_part", []).append(detail.get("manufacturer_part_no"))
return manufacture_details

View File

@@ -7,7 +7,8 @@ from frappe import _
def execute(filters=None):
if not filters: filters = {}
if not filters:
filters = {}
columns = get_columns()
@@ -15,6 +16,7 @@ def execute(filters=None):
return columns, data
def get_columns():
"""return columns"""
columns = [
@@ -29,6 +31,7 @@ def get_columns():
return columns
def get_bom_stock(filters):
conditions = ""
bom = filters.get("bom")
@@ -37,25 +40,30 @@ def get_bom_stock(filters):
qty_field = "stock_qty"
qty_to_produce = filters.get("qty_to_produce", 1)
if int(qty_to_produce) <= 0:
if int(qty_to_produce) <= 0:
frappe.throw(_("Quantity to Produce can not be less than Zero"))
if filters.get("show_exploded_view"):
table = "`tabBOM Explosion Item`"
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
if warehouse_details:
conditions += " and exists (select name from `tabWarehouse` wh \
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)" % (warehouse_details.lft,
warehouse_details.rgt)
conditions += (
" and exists (select name from `tabWarehouse` wh \
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
% (warehouse_details.lft, warehouse_details.rgt)
)
else:
conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
else:
conditions += ""
return frappe.db.sql("""
return frappe.db.sql(
"""
SELECT
bom_item.item_code,
bom_item.description ,
@@ -74,9 +82,10 @@ def get_bom_stock(filters):
bom_item.parent = {bom} and bom_item.parenttype='BOM'
GROUP BY bom_item.item_code""".format(
qty_field=qty_field,
table=table,
conditions=conditions,
bom=frappe.db.escape(bom),
qty_to_produce=qty_to_produce or 1)
)
qty_field=qty_field,
table=table,
conditions=conditions,
bom=frappe.db.escape(bom),
qty_to_produce=qty_to_produce or 1,
)
)

View File

@@ -12,98 +12,99 @@ def execute(filters=None):
data = get_data(filters)
return columns, data
def get_columns(filters):
columns = [{
columns = [
{
"label": _("Work Order"),
"fieldname": "work_order",
"fieldtype": "Link",
"options": "Work Order",
"width": 120
}]
if not filters.get('bom_no'):
columns.extend([
{
"label": _("BOM No"),
"fieldname": "bom_no",
"fieldtype": "Link",
"options": "BOM",
"width": 180
}
])
columns.extend([
{
"label": _("Finished Good"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 120
},
{
"label": _("Ordered Qty"),
"fieldname": "qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("Produced Qty"),
"fieldname": "produced_qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("Raw Material"),
"fieldname": "raw_material_code",
"fieldtype": "Link",
"options": "Item",
"width": 120
},
{
"label": _("Required Qty"),
"fieldname": "required_qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("Consumed Qty"),
"fieldname": "consumed_qty",
"fieldtype": "Float",
"width": 120
"width": 120,
}
])
]
if not filters.get("bom_no"):
columns.extend(
[
{
"label": _("BOM No"),
"fieldname": "bom_no",
"fieldtype": "Link",
"options": "BOM",
"width": 180,
}
]
)
columns.extend(
[
{
"label": _("Finished Good"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 120,
},
{"label": _("Ordered Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 120},
{"label": _("Produced Qty"), "fieldname": "produced_qty", "fieldtype": "Float", "width": 120},
{
"label": _("Raw Material"),
"fieldname": "raw_material_code",
"fieldtype": "Link",
"options": "Item",
"width": 120,
},
{"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 120},
{"label": _("Consumed Qty"), "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120},
]
)
return columns
def get_data(filters):
cond = "1=1"
if filters.get('bom_no') and not filters.get('work_order'):
cond += " and bom_no = '%s'" % filters.get('bom_no')
if filters.get("bom_no") and not filters.get("work_order"):
cond += " and bom_no = '%s'" % filters.get("bom_no")
if filters.get('work_order'):
cond += " and name = '%s'" % filters.get('work_order')
if filters.get("work_order"):
cond += " and name = '%s'" % filters.get("work_order")
results = []
for d in frappe.db.sql(""" select name as work_order, qty, produced_qty, production_item, bom_no
from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(cond), as_dict=1):
for d in frappe.db.sql(
""" select name as work_order, qty, produced_qty, production_item, bom_no
from `tabWork Order` where produced_qty > qty and docstatus = 1 and {0}""".format(
cond
),
as_dict=1,
):
results.append(d)
for data in frappe.get_all('Work Order Item', fields=["item_code as raw_material_code",
"required_qty", "consumed_qty"], filters={'parent': d.work_order, 'parenttype': 'Work Order'}):
for data in frappe.get_all(
"Work Order Item",
fields=["item_code as raw_material_code", "required_qty", "consumed_qty"],
filters={"parent": d.work_order, "parenttype": "Work Order"},
):
results.append(data)
return results
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_work_orders(doctype, txt, searchfield, start, page_len, filters):
cond = "1=1"
if filters.get('bom_no'):
cond += " and bom_no = '%s'" % filters.get('bom_no')
if filters.get("bom_no"):
cond += " and bom_no = '%s'" % filters.get("bom_no")
return frappe.db.sql("""select name from `tabWork Order`
return frappe.db.sql(
"""select name from `tabWork Order`
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1
order by name limit {1}, {2}""".format(cond, start, page_len),{
'name': "%%%s%%" % txt
}, as_list=1)
order by name limit {1}, {2}""".format(
cond, start, page_len
),
{"name": "%%%s%%" % txt},
as_list=1,
)

View File

@@ -11,58 +11,77 @@ def execute(filters=None):
def get_data(report_filters):
data = []
operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
operations = frappe.get_all("Operation", filters={"is_corrective_operation": 1})
if operations:
if report_filters.get('operation'):
operations = [report_filters.get('operation')]
if report_filters.get("operation"):
operations = [report_filters.get("operation")]
else:
operations = [d.name for d in operations]
job_card = frappe.qb.DocType("Job Card")
operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost')
item_code = (job_card.production_item).as_('item_code')
operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_(
"operating_cost"
)
item_code = (job_card.production_item).as_("item_code")
query = (frappe.qb
.from_(job_card)
.select(job_card.name, job_card.work_order, item_code, job_card.item_name,
job_card.operation, job_card.serial_no, job_card.batch_no,
job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate,
operating_cost)
.where(
(job_card.docstatus == 1)
& (job_card.is_corrective_job_card == 1))
.groupby(job_card.name)
)
query = (
frappe.qb.from_(job_card)
.select(
job_card.name,
job_card.work_order,
item_code,
job_card.item_name,
job_card.operation,
job_card.serial_no,
job_card.batch_no,
job_card.workstation,
job_card.total_time_in_mins,
job_card.hour_rate,
operating_cost,
)
.where((job_card.docstatus == 1) & (job_card.is_corrective_job_card == 1))
.groupby(job_card.name)
)
query = append_filters(query, report_filters, operations, job_card)
data = query.run(as_dict=True)
return data
def append_filters(query, report_filters, operations, job_card):
"""Append optional filters to query builder. """
for field in ("name", "work_order", "operation", "workstation",
"company", "serial_no", "batch_no", "production_item"):
def append_filters(query, report_filters, operations, job_card):
"""Append optional filters to query builder."""
for field in (
"name",
"work_order",
"operation",
"workstation",
"company",
"serial_no",
"batch_no",
"production_item",
):
if report_filters.get(field):
if field == 'serial_no':
query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field))))
elif field == 'operation':
if field == "serial_no":
query = query.where(job_card[field].like("%{}%".format(report_filters.get(field))))
elif field == "operation":
query = query.where(job_card[field].isin(operations))
else:
query = query.where(job_card[field] == report_filters.get(field))
if report_filters.get('from_date') or report_filters.get('to_date'):
if report_filters.get("from_date") or report_filters.get("to_date"):
job_card_time_log = frappe.qb.DocType("Job Card Time Log")
query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent)
if report_filters.get('from_date'):
query = query.where(job_card_time_log.from_time >= report_filters.get('from_date'))
if report_filters.get('to_date'):
query = query.where(job_card_time_log.to_time <= report_filters.get('to_date'))
if report_filters.get("from_date"):
query = query.where(job_card_time_log.from_time >= report_filters.get("from_date"))
if report_filters.get("to_date"):
query = query.where(job_card_time_log.to_time <= report_filters.get("to_date"))
return query
def get_columns(filters):
return [
{
@@ -70,64 +89,49 @@ def get_columns(filters):
"fieldtype": "Link",
"fieldname": "name",
"options": "Job Card",
"width": "120"
"width": "120",
},
{
"label": _("Work Order"),
"fieldtype": "Link",
"fieldname": "work_order",
"options": "Work Order",
"width": "100"
"width": "100",
},
{
"label": _("Item Code"),
"fieldtype": "Link",
"fieldname": "item_code",
"options": "Item",
"width": "100"
},
{
"label": _("Item Name"),
"fieldtype": "Data",
"fieldname": "item_name",
"width": "100"
"width": "100",
},
{"label": _("Item Name"), "fieldtype": "Data", "fieldname": "item_name", "width": "100"},
{
"label": _("Operation"),
"fieldtype": "Link",
"fieldname": "operation",
"options": "Operation",
"width": "100"
},
{
"label": _("Serial No"),
"fieldtype": "Data",
"fieldname": "serial_no",
"width": "100"
},
{
"label": _("Batch No"),
"fieldtype": "Data",
"fieldname": "batch_no",
"width": "100"
"width": "100",
},
{"label": _("Serial No"), "fieldtype": "Data", "fieldname": "serial_no", "width": "100"},
{"label": _("Batch No"), "fieldtype": "Data", "fieldname": "batch_no", "width": "100"},
{
"label": _("Workstation"),
"fieldtype": "Link",
"fieldname": "workstation",
"options": "Workstation",
"width": "100"
"width": "100",
},
{
"label": _("Operating Cost"),
"fieldtype": "Currency",
"fieldname": "operating_cost",
"width": "150"
"width": "150",
},
{
"label": _("Total Time (in Mins)"),
"fieldtype": "Float",
"fieldname": "total_time_in_mins",
"width": "150"
}
"width": "150",
},
]

View File

@@ -14,10 +14,20 @@ def execute(filters=None):
chart_data = get_chart_data(data, filters)
return columns, data, None, chart_data
def get_data(filters):
query_filters = {}
fields = ["name", "workstation", "operator", "from_time", "to_time", "downtime", "stop_reason", "remarks"]
fields = [
"name",
"workstation",
"operator",
"from_time",
"to_time",
"downtime",
"stop_reason",
"remarks",
]
query_filters["from_time"] = (">=", filters.get("from_date"))
query_filters["to_time"] = ("<=", filters.get("to_date"))
@@ -25,13 +35,14 @@ def get_data(filters):
if filters.get("workstation"):
query_filters["workstation"] = filters.get("workstation")
data = frappe.get_all("Downtime Entry", fields= fields, filters=query_filters) or []
data = frappe.get_all("Downtime Entry", fields=fields, filters=query_filters) or []
for d in data:
if d.downtime:
d.downtime = d.downtime / 60
return data
def get_chart_data(data, columns):
labels = sorted(list(set([d.workstation for d in data])))
@@ -47,17 +58,13 @@ def get_chart_data(data, columns):
datasets.append(workstation_wise_data.get(label, 0))
chart = {
"data": {
"labels": labels,
"datasets": [
{"name": "Machine Downtime", "values": datasets}
]
},
"type": "bar"
"data": {"labels": labels, "datasets": [{"name": "Machine Downtime", "values": datasets}]},
"type": "bar",
}
return chart
def get_columns(filters):
return [
{
@@ -65,50 +72,25 @@ def get_columns(filters):
"fieldname": "name",
"fieldtype": "Link",
"options": "Downtime Entry",
"width": 100
"width": 100,
},
{
"label": _("Machine"),
"fieldname": "workstation",
"fieldtype": "Link",
"options": "Workstation",
"width": 100
"width": 100,
},
{
"label": _("Operator"),
"fieldname": "operator",
"fieldtype": "Link",
"options": "Employee",
"width": 130
"width": 130,
},
{
"label": _("From Time"),
"fieldname": "from_time",
"fieldtype": "Datetime",
"width": 160
},
{
"label": _("To Time"),
"fieldname": "to_time",
"fieldtype": "Datetime",
"width": 160
},
{
"label": _("Downtime (In Hours)"),
"fieldname": "downtime",
"fieldtype": "Float",
"width": 150
},
{
"label": _("Stop Reason"),
"fieldname": "stop_reason",
"fieldtype": "Data",
"width": 220
},
{
"label": _("Remarks"),
"fieldname": "remarks",
"fieldtype": "Text",
"width": 100
}
{"label": _("From Time"), "fieldname": "from_time", "fieldtype": "Datetime", "width": 160},
{"label": _("To Time"), "fieldname": "to_time", "fieldtype": "Datetime", "width": 160},
{"label": _("Downtime (In Hours)"), "fieldname": "downtime", "fieldtype": "Float", "width": 150},
{"label": _("Stop Reason"), "fieldname": "stop_reason", "fieldtype": "Data", "width": 220},
{"label": _("Remarks"), "fieldname": "remarks", "fieldtype": "Text", "width": 100},
]

View File

@@ -14,6 +14,7 @@ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
def execute(filters=None):
return ForecastingReport(filters).execute_report()
class ExponentialSmoothingForecast(object):
def forecast_future_data(self):
for key, value in self.period_wise_data.items():
@@ -26,24 +27,22 @@ class ExponentialSmoothingForecast(object):
elif forecast_data:
previous_period_data = forecast_data[-1]
value[forecast_key] = (previous_period_data[1] +
flt(self.filters.smoothing_constant) * (
flt(previous_period_data[0]) - flt(previous_period_data[1])
)
value[forecast_key] = previous_period_data[1] + flt(self.filters.smoothing_constant) * (
flt(previous_period_data[0]) - flt(previous_period_data[1])
)
if value.get(forecast_key):
# will be use to forecaset next period
forecast_data.append([value.get(period.key), value.get(forecast_key)])
class ForecastingReport(ExponentialSmoothingForecast):
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
self.data = []
self.doctype = self.filters.based_on_document
self.child_doctype = self.doctype + " Item"
self.based_on_field = ("qty"
if self.filters.based_on_field == "Qty" else "amount")
self.based_on_field = "qty" if self.filters.based_on_field == "Qty" else "amount"
self.fieldtype = "Float" if self.based_on_field == "qty" else "Currency"
self.company_currency = erpnext.get_company_currency(self.filters.company)
@@ -63,8 +62,15 @@ class ForecastingReport(ExponentialSmoothingForecast):
self.period_wise_data = {}
from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1)
self.period_list = get_period_list(from_date, self.filters.to_date,
from_date, self.filters.to_date, "Date Range", self.filters.periodicity, ignore_fiscal_year=True)
self.period_list = get_period_list(
from_date,
self.filters.to_date,
from_date,
self.filters.to_date,
"Date Range",
self.filters.periodicity,
ignore_fiscal_year=True,
)
order_data = self.get_data_for_forecast() or []
@@ -76,8 +82,10 @@ class ForecastingReport(ExponentialSmoothingForecast):
period_data = self.period_wise_data[key]
for period in self.period_list:
# check if posting date is within the period
if (entry.posting_date >= period.from_date and entry.posting_date <= period.to_date):
period_data[period.key] = period_data.get(period.key, 0.0) + flt(entry.get(self.based_on_field))
if entry.posting_date >= period.from_date and entry.posting_date <= period.to_date:
period_data[period.key] = period_data.get(period.key, 0.0) + flt(
entry.get(self.based_on_field)
)
for key, value in self.period_wise_data.items():
list_of_period_value = [value.get(p.key, 0) for p in self.period_list]
@@ -90,12 +98,12 @@ class ForecastingReport(ExponentialSmoothingForecast):
def get_data_for_forecast(self):
cond = ""
if self.filters.item_code:
cond = " AND soi.item_code = %s" %(frappe.db.escape(self.filters.item_code))
cond = " AND soi.item_code = %s" % (frappe.db.escape(self.filters.item_code))
warehouses = []
if self.filters.warehouse:
warehouses = get_child_warehouses(self.filters.warehouse)
cond += " AND soi.warehouse in ({})".format(','.join(['%s'] * len(warehouses)))
cond += " AND soi.warehouse in ({})".format(",".join(["%s"] * len(warehouses)))
input_data = [self.filters.from_date, self.filters.company]
if warehouses:
@@ -103,7 +111,8 @@ class ForecastingReport(ExponentialSmoothingForecast):
date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
return frappe.db.sql("""
return frappe.db.sql(
"""
SELECT
so.{date_field} as posting_date, soi.item_code, soi.warehouse,
soi.item_name, soi.stock_qty as qty, soi.base_amount as amount
@@ -112,23 +121,27 @@ class ForecastingReport(ExponentialSmoothingForecast):
WHERE
so.docstatus = 1 AND so.name = soi.parent AND
so.{date_field} < %s AND so.company = %s {cond}
""".format(doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond),
tuple(input_data), as_dict=1)
""".format(
doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond
),
tuple(input_data),
as_dict=1,
)
def prepare_final_data(self):
self.data = []
if not self.period_wise_data: return
if not self.period_wise_data:
return
for key in self.period_wise_data:
self.data.append(self.period_wise_data.get(key))
def add_total(self):
if not self.data: return
if not self.data:
return
total_row = {
"item_code": _(frappe.bold("Total Quantity"))
}
total_row = {"item_code": _(frappe.bold("Total Quantity"))}
for value in self.data:
for period in self.period_list:
@@ -145,43 +158,52 @@ class ForecastingReport(ExponentialSmoothingForecast):
self.data.append(total_row)
def get_columns(self):
columns = [{
"label": _("Item Code"),
"options": "Item",
"fieldname": "item_code",
"fieldtype": "Link",
"width": 130
}, {
"label": _("Warehouse"),
"options": "Warehouse",
"fieldname": "warehouse",
"fieldtype": "Link",
"width": 130
}]
columns = [
{
"label": _("Item Code"),
"options": "Item",
"fieldname": "item_code",
"fieldtype": "Link",
"width": 130,
},
{
"label": _("Warehouse"),
"options": "Warehouse",
"fieldname": "warehouse",
"fieldtype": "Link",
"width": 130,
},
]
width = 180 if self.filters.periodicity in ['Yearly', "Half-Yearly", "Quarterly"] else 100
width = 180 if self.filters.periodicity in ["Yearly", "Half-Yearly", "Quarterly"] else 100
for period in self.period_list:
if (self.filters.periodicity in ['Yearly', "Half-Yearly", "Quarterly"]
or period.from_date >= getdate(self.filters.from_date)):
if self.filters.periodicity in [
"Yearly",
"Half-Yearly",
"Quarterly",
] or period.from_date >= getdate(self.filters.from_date):
forecast_key = period.key
label = _(period.label)
if period.from_date >= getdate(self.filters.from_date):
forecast_key = 'forecast_' + period.key
forecast_key = "forecast_" + period.key
label = _(period.label) + " " + _("(Forecast)")
columns.append({
"label": label,
"fieldname": forecast_key,
"fieldtype": self.fieldtype,
"width": width,
"default": 0.0
})
columns.append(
{
"label": label,
"fieldname": forecast_key,
"fieldtype": self.fieldtype,
"width": width,
"default": 0.0,
}
)
return columns
def get_chart_data(self):
if not self.data: return
if not self.data:
return
labels = []
self.total_demand = []
@@ -206,40 +228,35 @@ class ForecastingReport(ExponentialSmoothingForecast):
"data": {
"labels": labels,
"datasets": [
{
"name": "Demand",
"values": self.total_demand
},
{
"name": "Forecast",
"values": self.total_forecast
}
]
{"name": "Demand", "values": self.total_demand},
{"name": "Forecast", "values": self.total_forecast},
],
},
"type": "line"
"type": "line",
}
def get_summary_data(self):
if not self.data: return
if not self.data:
return
return [
{
"value": sum(self.total_demand),
"label": _("Total Demand (Past Data)"),
"currency": self.company_currency,
"datatype": self.fieldtype
"datatype": self.fieldtype,
},
{
"value": sum(self.total_history_forecast),
"label": _("Total Forecast (Past Data)"),
"currency": self.company_currency,
"datatype": self.fieldtype
"datatype": self.fieldtype,
},
{
"value": sum(self.total_future_forecast),
"indicator": "Green",
"label": _("Total Forecast (Future Data)"),
"currency": self.company_currency,
"datatype": self.fieldtype
}
"datatype": self.fieldtype,
},
]

View File

@@ -16,23 +16,34 @@ def execute(filters=None):
chart_data = get_chart_data(data, filters)
return columns, data, None, chart_data
def get_data(filters):
query_filters = {
"docstatus": ("<", 2),
"posting_date": ("between", [filters.from_date, filters.to_date])
"posting_date": ("between", [filters.from_date, filters.to_date]),
}
fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date",
"total_completed_qty", "workstation", "operation", "total_time_in_mins"]
fields = [
"name",
"status",
"work_order",
"production_item",
"item_name",
"posting_date",
"total_completed_qty",
"workstation",
"operation",
"total_time_in_mins",
]
for field in ["work_order", "workstation", "operation", "company"]:
if filters.get(field):
query_filters[field] = ("in", filters.get(field))
data = frappe.get_all("Job Card",
fields= fields, filters=query_filters)
data = frappe.get_all("Job Card", fields=fields, filters=query_filters)
if not data: return []
if not data:
return []
job_cards = [d.name for d in data]
@@ -42,9 +53,12 @@ def get_data(filters):
}
job_card_time_details = {}
for job_card_data in frappe.get_all("Job Card Time Log",
for job_card_data in frappe.get_all(
"Job Card Time Log",
fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"],
filters=job_card_time_filter, group_by="parent"):
filters=job_card_time_filter,
group_by="parent",
):
job_card_time_details[job_card_data.parent] = job_card_data
res = []
@@ -60,6 +74,7 @@ def get_data(filters):
return res
def get_chart_data(job_card_details, filters):
labels, periodic_data = prepare_chart_data(job_card_details, filters)
@@ -73,23 +88,15 @@ def get_chart_data(job_card_details, filters):
datasets.append({"name": "Open", "values": open_job_cards})
datasets.append({"name": "Completed", "values": completed})
chart = {
"data": {
'labels': labels,
'datasets': datasets
},
"type": "bar"
}
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}
return chart
def prepare_chart_data(job_card_details, filters):
labels = []
periodic_data = {
"Open": {},
"Completed": {}
}
periodic_data = {"Open": {}, "Completed": {}}
filters.range = "Monthly"
@@ -110,6 +117,7 @@ def prepare_chart_data(job_card_details, filters):
return labels, periodic_data
def get_columns(filters):
columns = [
{
@@ -117,84 +125,62 @@ def get_columns(filters):
"fieldname": "name",
"fieldtype": "Link",
"options": "Job Card",
"width": 100
},
{
"label": _("Posting Date"),
"fieldname": "posting_date",
"fieldtype": "Date",
"width": 100
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
]
if not filters.get("status"):
columns.append(
{
"label": _("Status"),
"fieldname": "status",
"width": 100
},
{"label": _("Status"), "fieldname": "status", "width": 100},
)
columns.extend([
{
"label": _("Work Order"),
"fieldname": "work_order",
"fieldtype": "Link",
"options": "Work Order",
"width": 100
},
{
"label": _("Production Item"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 110
},
{
"label": _("Item Name"),
"fieldname": "item_name",
"fieldtype": "Data",
"width": 100
},
{
"label": _("Workstation"),
"fieldname": "workstation",
"fieldtype": "Link",
"options": "Workstation",
"width": 110
},
{
"label": _("Operation"),
"fieldname": "operation",
"fieldtype": "Link",
"options": "Operation",
"width": 110
},
{
"label": _("Total Completed Qty"),
"fieldname": "total_completed_qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("From Time"),
"fieldname": "from_time",
"fieldtype": "Datetime",
"width": 120
},
{
"label": _("To Time"),
"fieldname": "to_time",
"fieldtype": "Datetime",
"width": 120
},
{
"label": _("Time Required (In Mins)"),
"fieldname": "total_time_in_mins",
"fieldtype": "Float",
"width": 100
}
])
columns.extend(
[
{
"label": _("Work Order"),
"fieldname": "work_order",
"fieldtype": "Link",
"options": "Work Order",
"width": 100,
},
{
"label": _("Production Item"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 110,
},
{"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
{
"label": _("Workstation"),
"fieldname": "workstation",
"fieldtype": "Link",
"options": "Workstation",
"width": 110,
},
{
"label": _("Operation"),
"fieldname": "operation",
"fieldtype": "Link",
"options": "Operation",
"width": 110,
},
{
"label": _("Total Completed Qty"),
"fieldname": "total_completed_qty",
"fieldtype": "Float",
"width": 120,
},
{"label": _("From Time"), "fieldname": "from_time", "fieldtype": "Datetime", "width": 120},
{"label": _("To Time"), "fieldname": "to_time", "fieldtype": "Datetime", "width": 120},
{
"label": _("Time Required (In Mins)"),
"fieldname": "total_time_in_mins",
"fieldtype": "Float",
"width": 100,
},
]
)
return columns

View File

@@ -12,87 +12,71 @@ Data = List[Row]
Columns = List[Dict[str, str]]
QueryArgs = Dict[str, str]
def execute(filters: Filters) -> Tuple[Columns, Data]:
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters: Filters) -> Data:
query_args = get_query_args(filters)
data = run_query(query_args)
update_data_with_total_pl_value(data)
return data
def get_columns() -> Columns:
return [
{
'label': _('Work Order'),
'fieldname': 'name',
'fieldtype': 'Link',
'options': 'Work Order',
'width': '200'
"label": _("Work Order"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Work Order",
"width": "200",
},
{
'label': _('Item'),
'fieldname': 'production_item',
'fieldtype': 'Link',
'options': 'Item',
'width': '100'
"label": _("Item"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": "100",
},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": "100"},
{
'label': _('Status'),
'fieldname': 'status',
'fieldtype': 'Data',
'width': '100'
"label": _("Manufactured Qty"),
"fieldname": "produced_qty",
"fieldtype": "Float",
"width": "150",
},
{"label": _("Loss Qty"), "fieldname": "process_loss_qty", "fieldtype": "Float", "width": "150"},
{
'label': _('Manufactured Qty'),
'fieldname': 'produced_qty',
'fieldtype': 'Float',
'width': '150'
"label": _("Actual Manufactured Qty"),
"fieldname": "actual_produced_qty",
"fieldtype": "Float",
"width": "150",
},
{"label": _("Loss Value"), "fieldname": "total_pl_value", "fieldtype": "Float", "width": "150"},
{"label": _("FG Value"), "fieldname": "total_fg_value", "fieldtype": "Float", "width": "150"},
{
'label': _('Loss Qty'),
'fieldname': 'process_loss_qty',
'fieldtype': 'Float',
'width': '150'
"label": _("Raw Material Value"),
"fieldname": "total_rm_value",
"fieldtype": "Float",
"width": "150",
},
{
'label': _('Actual Manufactured Qty'),
'fieldname': 'actual_produced_qty',
'fieldtype': 'Float',
'width': '150'
},
{
'label': _('Loss Value'),
'fieldname': 'total_pl_value',
'fieldtype': 'Float',
'width': '150'
},
{
'label': _('FG Value'),
'fieldname': 'total_fg_value',
'fieldtype': 'Float',
'width': '150'
},
{
'label': _('Raw Material Value'),
'fieldname': 'total_rm_value',
'fieldtype': 'Float',
'width': '150'
}
]
def get_query_args(filters: Filters) -> QueryArgs:
query_args = {}
query_args.update(filters)
query_args.update(
get_filter_conditions(filters)
)
query_args.update(get_filter_conditions(filters))
return query_args
def run_query(query_args: QueryArgs) -> Data:
return frappe.db.sql("""
return frappe.db.sql(
"""
SELECT
wo.name, wo.status, wo.production_item, wo.qty,
wo.produced_qty, wo.process_loss_qty,
@@ -111,23 +95,26 @@ def run_query(query_args: QueryArgs) -> Data:
{work_order_filter}
GROUP BY
se.work_order
""".format(**query_args), query_args, as_dict=1)
""".format(
**query_args
),
query_args,
as_dict=1,
)
def update_data_with_total_pl_value(data: Data) -> None:
for row in data:
value_per_unit_fg = row['total_fg_value'] / row['actual_produced_qty']
row['total_pl_value'] = row['process_loss_qty'] * value_per_unit_fg
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
def get_filter_conditions(filters: Filters) -> QueryArgs:
filter_conditions = dict(item_filter="", work_order_filter="")
if "item" in filters:
production_item = filters.get("item")
filter_conditions.update(
{"item_filter": f"AND wo.production_item='{production_item}'"}
)
filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"})
if "work_order" in filters:
work_order_name = filters.get("work_order")
filter_conditions.update(
{"work_order_filter": f"AND wo.name='{work_order_name}'"}
)
filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"})
return filter_conditions

View File

@@ -12,16 +12,11 @@ from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get
def execute(filters=None):
columns = get_columns(filters)
data, chart = get_data(filters, columns)
return columns, data, None , chart
return columns, data, None, chart
def get_columns(filters):
columns =[
{
"label": _("Status"),
"fieldname": "Status",
"fieldtype": "Data",
"width": 140
}]
columns = [{"label": _("Status"), "fieldname": "Status", "fieldtype": "Data", "width": 140}]
ranges = get_period_date_ranges(filters)
@@ -29,22 +24,20 @@ def get_columns(filters):
period = get_period(end_date, filters)
columns.append({
"label": _(period),
"fieldname": scrub(period),
"fieldtype": "Float",
"width": 120
})
columns.append(
{"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120}
)
return columns
def get_periodic_data(filters, entry):
periodic_data = {
"All Work Orders": {},
"Not Started": {},
"Overdue": {},
"Pending": {},
"Completed": {}
"Completed": {},
}
ranges = get_period_date_ranges(filters)
@@ -52,34 +45,37 @@ def get_periodic_data(filters, entry):
for from_date, end_date in ranges:
period = get_period(end_date, filters)
for d in entry:
if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date) :
if getdate(d.creation) <= getdate(from_date) or getdate(d.creation) <= getdate(end_date):
periodic_data = update_periodic_data(periodic_data, "All Work Orders", period)
if d.status == 'Completed':
if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate(from_date):
if d.status == "Completed":
if getdate(d.actual_end_date) < getdate(from_date) or getdate(d.modified) < getdate(
from_date
):
periodic_data = update_periodic_data(periodic_data, "Completed", period)
elif getdate(d.actual_start_date) < getdate(from_date) :
elif getdate(d.actual_start_date) < getdate(from_date):
periodic_data = update_periodic_data(periodic_data, "Pending", period)
elif getdate(d.planned_start_date) < getdate(from_date) :
elif getdate(d.planned_start_date) < getdate(from_date):
periodic_data = update_periodic_data(periodic_data, "Overdue", period)
else:
periodic_data = update_periodic_data(periodic_data, "Not Started", period)
elif d.status == 'In Process':
if getdate(d.actual_start_date) < getdate(from_date) :
elif d.status == "In Process":
if getdate(d.actual_start_date) < getdate(from_date):
periodic_data = update_periodic_data(periodic_data, "Pending", period)
elif getdate(d.planned_start_date) < getdate(from_date) :
elif getdate(d.planned_start_date) < getdate(from_date):
periodic_data = update_periodic_data(periodic_data, "Overdue", period)
else:
periodic_data = update_periodic_data(periodic_data, "Not Started", period)
elif d.status == 'Not Started':
if getdate(d.planned_start_date) < getdate(from_date) :
elif d.status == "Not Started":
if getdate(d.planned_start_date) < getdate(from_date):
periodic_data = update_periodic_data(periodic_data, "Overdue", period)
else:
periodic_data = update_periodic_data(periodic_data, "Not Started", period)
return periodic_data
def update_periodic_data(periodic_data, status, period):
if periodic_data.get(status).get(period):
periodic_data[status][period] += 1
@@ -88,22 +84,33 @@ def update_periodic_data(periodic_data, status, period):
return periodic_data
def get_data(filters, columns):
data = []
entry = frappe.get_all("Work Order",
fields=["creation", "modified", "actual_start_date", "actual_end_date", "planned_start_date", "planned_end_date", "status"],
filters={"docstatus": 1, "company": filters["company"] })
entry = frappe.get_all(
"Work Order",
fields=[
"creation",
"modified",
"actual_start_date",
"actual_end_date",
"planned_start_date",
"planned_end_date",
"status",
],
filters={"docstatus": 1, "company": filters["company"]},
)
periodic_data = get_periodic_data(filters,entry)
periodic_data = get_periodic_data(filters, entry)
labels = ["All Work Orders", "Not Started", "Overdue", "Pending", "Completed"]
chart_data = get_chart_data(periodic_data,columns)
chart_data = get_chart_data(periodic_data, columns)
ranges = get_period_date_ranges(filters)
for label in labels:
work = {}
work["Status"] = label
for dummy,end_date in ranges:
for dummy, end_date in ranges:
period = get_period(end_date, filters)
if periodic_data.get(label).get(period):
work[scrub(period)] = periodic_data.get(label).get(period)
@@ -113,10 +120,11 @@ def get_data(filters, columns):
return data, chart_data
def get_chart_data(periodic_data, columns):
labels = [d.get("label") for d in columns[1:]]
all_data, not_start, overdue, pending, completed = [], [], [] , [], []
all_data, not_start, overdue, pending, completed = [], [], [], [], []
datasets = []
for d in labels:
@@ -126,18 +134,13 @@ def get_chart_data(periodic_data, columns):
pending.append(periodic_data.get("Pending").get(d))
completed.append(periodic_data.get("Completed").get(d))
datasets.append({'name':'All Work Orders', 'values': all_data})
datasets.append({'name':'Not Started', 'values': not_start})
datasets.append({'name':'Overdue', 'values': overdue})
datasets.append({'name':'Pending', 'values': pending})
datasets.append({'name':'Completed', 'values': completed})
datasets.append({"name": "All Work Orders", "values": all_data})
datasets.append({"name": "Not Started", "values": not_start})
datasets.append({"name": "Overdue", "values": overdue})
datasets.append({"name": "Pending", "values": pending})
datasets.append({"name": "Completed", "values": completed})
chart = {
"data": {
'labels': labels,
'datasets': datasets
}
}
chart = {"data": {"labels": labels, "datasets": datasets}}
chart["type"] = "line"
return chart

View File

@@ -13,6 +13,7 @@ def execute(filters=None):
return columns, data
def get_data(filters):
data = []
@@ -23,6 +24,7 @@ def get_data(filters):
return data
def get_production_plan_item_details(filters, data, order_details):
itemwise_indent = {}
@@ -30,77 +32,85 @@ def get_production_plan_item_details(filters, data, order_details):
for row in production_plan_doc.po_items:
work_order = frappe.get_value(
"Work Order",
{
"production_plan_item": row.name,
"bom_no": row.bom_no,
"production_item": row.item_code
},
"name"
{"production_plan_item": row.name, "bom_no": row.bom_no, "production_item": row.item_code},
"name",
)
if row.item_code not in itemwise_indent:
itemwise_indent.setdefault(row.item_code, {})
data.append({
"indent": 0,
"item_code": row.item_code,
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
"qty": row.planned_qty,
"document_type": "Work Order",
"document_name": work_order or "",
"bom_level": 0,
"produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
})
data.append(
{
"indent": 0,
"item_code": row.item_code,
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
"qty": row.planned_qty,
"document_type": "Work Order",
"document_name": work_order or "",
"bom_level": 0,
"produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
"pending_qty": flt(row.planned_qty)
- flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)),
}
)
get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
get_production_plan_sub_assembly_item_details(
filters, row, production_plan_doc, data, order_details
)
def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details):
def get_production_plan_sub_assembly_item_details(
filters, row, production_plan_doc, data, order_details
):
for item in production_plan_doc.sub_assembly_items:
if row.name == item.production_plan_item:
subcontracted_item = (item.type_of_manufacturing == 'Subcontract')
subcontracted_item = item.type_of_manufacturing == "Subcontract"
if subcontracted_item:
docname = frappe.get_value(
"Purchase Order Item",
{
"production_plan_sub_assembly_item": item.name,
"docstatus": ("<", 2)
},
"parent"
{"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)},
"parent",
)
else:
docname = frappe.get_value(
"Work Order",
{
"production_plan_sub_assembly_item": item.name,
"docstatus": ("<", 2)
},
"name"
"Work Order", {"production_plan_sub_assembly_item": item.name, "docstatus": ("<", 2)}, "name"
)
data.append({
"indent": 1,
"item_code": item.production_item,
"item_name": item.item_name,
"qty": item.qty,
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
"document_name": docname or "",
"bom_level": item.bom_level,
"produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0),
"pending_qty": flt(item.qty) - flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0))
})
data.append(
{
"indent": 1,
"item_code": item.production_item,
"item_name": item.item_name,
"qty": item.qty,
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
"document_name": docname or "",
"bom_level": item.bom_level,
"produced_qty": order_details.get((docname, item.production_item), {}).get("produced_qty", 0),
"pending_qty": flt(item.qty)
- flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)),
}
)
def get_work_order_details(filters, order_details):
for row in frappe.get_all("Work Order", filters = {"production_plan": filters.get("production_plan")},
fields=["name", "produced_qty", "production_plan", "production_item"]):
for row in frappe.get_all(
"Work Order",
filters={"production_plan": filters.get("production_plan")},
fields=["name", "produced_qty", "production_plan", "production_item"],
):
order_details.setdefault((row.name, row.production_item), row)
def get_purchase_order_details(filters, order_details):
for row in frappe.get_all("Purchase Order Item", filters = {"production_plan": filters.get("production_plan")},
fields=["parent", "received_qty as produced_qty", "item_code"]):
for row in frappe.get_all(
"Purchase Order Item",
filters={"production_plan": filters.get("production_plan")},
fields=["parent", "received_qty as produced_qty", "item_code"],
):
order_details.setdefault((row.parent, row.item_code), row)
def get_column(filters):
return [
{
@@ -108,49 +118,24 @@ def get_column(filters):
"fieldtype": "Link",
"fieldname": "item_code",
"width": 300,
"options": "Item"
},
{
"label": "Item Name",
"fieldtype": "data",
"fieldname": "item_name",
"width": 100
"options": "Item",
},
{"label": "Item Name", "fieldtype": "data", "fieldname": "item_name", "width": 100},
{
"label": "Document Type",
"fieldtype": "Link",
"fieldname": "document_type",
"width": 150,
"options": "DocType"
"options": "DocType",
},
{
"label": "Document Name",
"fieldtype": "Dynamic Link",
"fieldname": "document_name",
"width": 150
"width": 150,
},
{
"label": "BOM Level",
"fieldtype": "Int",
"fieldname": "bom_level",
"width": 100
},
{
"label": "Order Qty",
"fieldtype": "Float",
"fieldname": "qty",
"width": 120
},
{
"label": "Received Qty",
"fieldtype": "Float",
"fieldname": "produced_qty",
"width": 160
},
{
"label": "Pending Qty",
"fieldtype": "Float",
"fieldname": "pending_qty",
"width": 110
}
{"label": "BOM Level", "fieldtype": "Int", "fieldname": "bom_level", "width": 100},
{"label": "Order Qty", "fieldtype": "Float", "fieldname": "qty", "width": 120},
{"label": "Received Qty", "fieldtype": "Float", "fieldname": "produced_qty", "width": 160},
{"label": "Pending Qty", "fieldtype": "Float", "fieldname": "pending_qty", "width": 110},
]

View File

@@ -15,38 +15,36 @@ mapper = {
stock_qty as qty_to_manufacture, `tabSales Order Item`.parent as name, bom_no, warehouse,
`tabSales Order Item`.delivery_date, `tabSales Order`.base_grand_total """,
"filters": """`tabSales Order Item`.docstatus = 1 and stock_qty > produced_qty
and `tabSales Order`.per_delivered < 100.0"""
and `tabSales Order`.per_delivered < 100.0""",
},
"Material Request": {
"fields": """ item_code as production_item, item_name as production_item_name, stock_uom,
stock_qty as qty_to_manufacture, `tabMaterial Request Item`.parent as name, bom_no, warehouse,
`tabMaterial Request Item`.schedule_date """,
"filters": """`tabMaterial Request`.docstatus = 1 and `tabMaterial Request`.per_ordered < 100
and `tabMaterial Request`.material_request_type = 'Manufacture' """
and `tabMaterial Request`.material_request_type = 'Manufacture' """,
},
"Work Order": {
"fields": """ production_item, item_name as production_item_name, planned_start_date,
stock_uom, qty as qty_to_manufacture, name, bom_no, fg_warehouse as warehouse """,
"filters": "docstatus = 1 and status not in ('Completed', 'Stopped')"
"filters": "docstatus = 1 and status not in ('Completed', 'Stopped')",
},
}
order_mapper = {
"Sales Order": {
"Delivery Date": "`tabSales Order Item`.delivery_date asc",
"Total Amount": "`tabSales Order`.base_grand_total desc"
"Total Amount": "`tabSales Order`.base_grand_total desc",
},
"Material Request": {
"Required Date": "`tabMaterial Request Item`.schedule_date asc"
},
"Work Order": {
"Planned Start Date": "planned_start_date asc"
}
"Material Request": {"Required Date": "`tabMaterial Request Item`.schedule_date asc"},
"Work Order": {"Planned Start Date": "planned_start_date asc"},
}
def execute(filters=None):
return ProductionPlanReport(filters).execute_report()
class ProductionPlanReport(object):
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
@@ -65,46 +63,64 @@ class ProductionPlanReport(object):
return self.columns, self.data
def get_open_orders(self):
doctype = ("`tabWork Order`" if self.filters.based_on == "Work Order"
else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on))
doctype = (
"`tabWork Order`"
if self.filters.based_on == "Work Order"
else "`tab{doc}`, `tab{doc} Item`".format(doc=self.filters.based_on)
)
filters = mapper.get(self.filters.based_on)["filters"]
filters = self.prepare_other_conditions(filters, self.filters.based_on)
order_by = " ORDER BY %s" % (order_mapper[self.filters.based_on][self.filters.order_by])
self.orders = frappe.db.sql(""" SELECT {fields} from {doctype}
self.orders = frappe.db.sql(
""" SELECT {fields} from {doctype}
WHERE {filters} {order_by}""".format(
doctype = doctype,
filters = filters,
order_by = order_by,
fields = mapper.get(self.filters.based_on)["fields"]
), tuple(self.filters.docnames), as_dict=1)
doctype=doctype,
filters=filters,
order_by=order_by,
fields=mapper.get(self.filters.based_on)["fields"],
),
tuple(self.filters.docnames),
as_dict=1,
)
def prepare_other_conditions(self, filters, doctype):
if self.filters.docnames:
field = "name" if doctype == "Work Order" else "`tab{} Item`.parent".format(doctype)
filters += " and %s in (%s)" % (field, ','.join(['%s'] * len(self.filters.docnames)))
filters += " and %s in (%s)" % (field, ",".join(["%s"] * len(self.filters.docnames)))
if doctype != "Work Order":
filters += " and `tab{doc}`.name = `tab{doc} Item`.parent".format(doc=doctype)
if self.filters.company:
filters += " and `tab%s`.company = %s" %(doctype, frappe.db.escape(self.filters.company))
filters += " and `tab%s`.company = %s" % (doctype, frappe.db.escape(self.filters.company))
return filters
def get_raw_materials(self):
if not self.orders: return
if not self.orders:
return
self.warehouses = [d.warehouse for d in self.orders]
self.item_codes = [d.production_item for d in self.orders]
if self.filters.based_on == "Work Order":
work_orders = [d.name for d in self.orders]
raw_materials = frappe.get_all("Work Order Item",
fields=["parent", "item_code", "item_name as raw_material_name",
"source_warehouse as warehouse", "required_qty"],
filters = {"docstatus": 1, "parent": ("in", work_orders), "source_warehouse": ("!=", "")}) or []
raw_materials = (
frappe.get_all(
"Work Order Item",
fields=[
"parent",
"item_code",
"item_name as raw_material_name",
"source_warehouse as warehouse",
"required_qty",
],
filters={"docstatus": 1, "parent": ("in", work_orders), "source_warehouse": ("!=", "")},
)
or []
)
self.warehouses.extend([d.source_warehouse for d in raw_materials])
else:
@@ -118,21 +134,32 @@ class ProductionPlanReport(object):
bom_nos.append(bom_no)
bom_doctype = ("BOM Explosion Item"
if self.filters.include_subassembly_raw_materials else "BOM Item")
bom_doctype = (
"BOM Explosion Item" if self.filters.include_subassembly_raw_materials else "BOM Item"
)
qty_field = ("qty_consumed_per_unit"
if self.filters.include_subassembly_raw_materials else "(bom_item.qty / bom.quantity)")
qty_field = (
"qty_consumed_per_unit"
if self.filters.include_subassembly_raw_materials
else "(bom_item.qty / bom.quantity)"
)
raw_materials = frappe.db.sql(""" SELECT bom_item.parent, bom_item.item_code,
raw_materials = frappe.db.sql(
""" SELECT bom_item.parent, bom_item.item_code,
bom_item.item_name as raw_material_name, {0} as required_qty_per_unit
FROM
`tabBOM` as bom, `tab{1}` as bom_item
WHERE
bom_item.parent in ({2}) and bom_item.parent = bom.name and bom.docstatus = 1
""".format(qty_field, bom_doctype, ','.join(["%s"] * len(bom_nos))), tuple(bom_nos), as_dict=1)
""".format(
qty_field, bom_doctype, ",".join(["%s"] * len(bom_nos))
),
tuple(bom_nos),
as_dict=1,
)
if not raw_materials: return
if not raw_materials:
return
self.item_codes.extend([d.item_code for d in raw_materials])
@@ -144,15 +171,20 @@ class ProductionPlanReport(object):
rows.append(d)
def get_item_details(self):
if not (self.orders and self.item_codes): return
if not (self.orders and self.item_codes):
return
self.item_details = {}
for d in frappe.get_all("Item Default", fields = ["parent", "default_warehouse"],
filters = {"company": self.filters.company, "parent": ("in", self.item_codes)}):
for d in frappe.get_all(
"Item Default",
fields=["parent", "default_warehouse"],
filters={"company": self.filters.company, "parent": ("in", self.item_codes)},
):
self.item_details[d.parent] = d
def get_bin_details(self):
if not (self.orders and self.raw_materials_dict): return
if not (self.orders and self.raw_materials_dict):
return
self.bin_details = {}
self.mrp_warehouses = []
@@ -160,48 +192,55 @@ class ProductionPlanReport(object):
self.mrp_warehouses.extend(get_child_warehouses(self.filters.raw_material_warehouse))
self.warehouses.extend(self.mrp_warehouses)
for d in frappe.get_all("Bin",
for d in frappe.get_all(
"Bin",
fields=["warehouse", "item_code", "actual_qty", "ordered_qty", "projected_qty"],
filters = {"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)}):
filters={"item_code": ("in", self.item_codes), "warehouse": ("in", self.warehouses)},
):
key = (d.item_code, d.warehouse)
if key not in self.bin_details:
self.bin_details.setdefault(key, d)
def get_purchase_details(self):
if not (self.orders and self.raw_materials_dict): return
if not (self.orders and self.raw_materials_dict):
return
self.purchase_details = {}
purchased_items = frappe.get_all("Purchase Order Item",
purchased_items = frappe.get_all(
"Purchase Order Item",
fields=["item_code", "min(schedule_date) as arrival_date", "qty as arrival_qty", "warehouse"],
filters={
"item_code": ("in", self.item_codes),
"warehouse": ("in", self.warehouses),
"docstatus": 1,
},
group_by = "item_code, warehouse")
group_by="item_code, warehouse",
)
for d in purchased_items:
key = (d.item_code, d.warehouse)
if key not in self.purchase_details:
self.purchase_details.setdefault(key, d)
def prepare_data(self):
if not self.orders: return
if not self.orders:
return
for d in self.orders:
key = d.name if self.filters.based_on == "Work Order" else d.bom_no
if not self.raw_materials_dict.get(key): continue
if not self.raw_materials_dict.get(key):
continue
bin_data = self.bin_details.get((d.production_item, d.warehouse)) or {}
d.update({
"for_warehouse": d.warehouse,
"available_qty": 0
})
d.update({"for_warehouse": d.warehouse, "available_qty": 0})
if bin_data and bin_data.get("actual_qty") > 0 and d.qty_to_manufacture:
d.available_qty = (bin_data.get("actual_qty")
if (d.qty_to_manufacture > bin_data.get("actual_qty")) else d.qty_to_manufacture)
d.available_qty = (
bin_data.get("actual_qty")
if (d.qty_to_manufacture > bin_data.get("actual_qty"))
else d.qty_to_manufacture
)
bin_data["actual_qty"] -= d.available_qty
@@ -232,8 +271,9 @@ class ProductionPlanReport(object):
d.remaining_qty = d.required_qty
self.pick_materials_from_warehouses(d, data, warehouses)
if (d.remaining_qty and self.filters.raw_material_warehouse
and d.remaining_qty != d.required_qty):
if (
d.remaining_qty and self.filters.raw_material_warehouse and d.remaining_qty != d.required_qty
):
row = self.get_args()
d.warehouse = self.filters.raw_material_warehouse
d.required_qty = d.remaining_qty
@@ -243,7 +283,8 @@ class ProductionPlanReport(object):
def pick_materials_from_warehouses(self, args, order_data, warehouses):
for index, warehouse in enumerate(warehouses):
if not args.remaining_qty: return
if not args.remaining_qty:
return
row = self.get_args()
@@ -255,14 +296,18 @@ class ProductionPlanReport(object):
args.allotted_qty = 0
if bin_data and bin_data.get("actual_qty") > 0:
args.allotted_qty = (bin_data.get("actual_qty")
if (args.required_qty > bin_data.get("actual_qty")) else args.required_qty)
args.allotted_qty = (
bin_data.get("actual_qty")
if (args.required_qty > bin_data.get("actual_qty"))
else args.required_qty
)
args.remaining_qty -= args.allotted_qty
bin_data["actual_qty"] -= args.allotted_qty
if ((self.mrp_warehouses and (args.allotted_qty or index == len(warehouses) - 1))
or not self.mrp_warehouses):
if (
self.mrp_warehouses and (args.allotted_qty or index == len(warehouses) - 1)
) or not self.mrp_warehouses:
if not self.index:
row.update(order_data)
self.index += 1
@@ -275,52 +320,45 @@ class ProductionPlanReport(object):
self.data.append(row)
def get_args(self):
return frappe._dict({
"work_order": "",
"sales_order": "",
"production_item": "",
"production_item_name": "",
"qty_to_manufacture": "",
"produced_qty": ""
})
return frappe._dict(
{
"work_order": "",
"sales_order": "",
"production_item": "",
"production_item_name": "",
"qty_to_manufacture": "",
"produced_qty": "",
}
)
def get_columns(self):
based_on = self.filters.based_on
self.columns = [{
"label": _("ID"),
"options": based_on,
"fieldname": "name",
"fieldtype": "Link",
"width": 100
}, {
"label": _("Item Code"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 120
}, {
"label": _("Item Name"),
"fieldname": "production_item_name",
"fieldtype": "Data",
"width": 130
}, {
"label": _("Warehouse"),
"options": "Warehouse",
"fieldname": "for_warehouse",
"fieldtype": "Link",
"width": 100
}, {
"label": _("Order Qty"),
"fieldname": "qty_to_manufacture",
"fieldtype": "Float",
"width": 80
}, {
"label": _("Available"),
"fieldname": "available_qty",
"fieldtype": "Float",
"width": 80
}]
self.columns = [
{"label": _("ID"), "options": based_on, "fieldname": "name", "fieldtype": "Link", "width": 100},
{
"label": _("Item Code"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 120,
},
{
"label": _("Item Name"),
"fieldname": "production_item_name",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Warehouse"),
"options": "Warehouse",
"fieldname": "for_warehouse",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Order Qty"), "fieldname": "qty_to_manufacture", "fieldtype": "Float", "width": 80},
{"label": _("Available"), "fieldname": "available_qty", "fieldtype": "Float", "width": 80},
]
fieldname, fieldtype = "delivery_date", "Date"
if self.filters.based_on == "Sales Order" and self.filters.order_by == "Total Amount":
@@ -330,48 +368,50 @@ class ProductionPlanReport(object):
elif self.filters.based_on == "Work Order":
fieldname = "planned_start_date"
self.columns.append({
"label": _(self.filters.order_by),
"fieldname": fieldname,
"fieldtype": fieldtype,
"width": 100
})
self.columns.append(
{
"label": _(self.filters.order_by),
"fieldname": fieldname,
"fieldtype": fieldtype,
"width": 100,
}
)
self.columns.extend([{
"label": _("Raw Material Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 120
}, {
"label": _("Raw Material Name"),
"fieldname": "raw_material_name",
"fieldtype": "Data",
"width": 130
}, {
"label": _("Warehouse"),
"options": "Warehouse",
"fieldname": "warehouse",
"fieldtype": "Link",
"width": 110
}, {
"label": _("Required Qty"),
"fieldname": "required_qty",
"fieldtype": "Float",
"width": 100
}, {
"label": _("Allotted Qty"),
"fieldname": "allotted_qty",
"fieldtype": "Float",
"width": 100
}, {
"label": _("Expected Arrival Date"),
"fieldname": "arrival_date",
"fieldtype": "Date",
"width": 160
}, {
"label": _("Arrival Quantity"),
"fieldname": "arrival_qty",
"fieldtype": "Float",
"width": 140
}])
self.columns.extend(
[
{
"label": _("Raw Material Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 120,
},
{
"label": _("Raw Material Name"),
"fieldname": "raw_material_name",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Warehouse"),
"options": "Warehouse",
"fieldname": "warehouse",
"fieldtype": "Link",
"width": 110,
},
{"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 100},
{"label": _("Allotted Qty"), "fieldname": "allotted_qty", "fieldtype": "Float", "width": 100},
{
"label": _("Expected Arrival Date"),
"fieldname": "arrival_date",
"fieldtype": "Date",
"width": 160,
},
{
"label": _("Arrival Quantity"),
"fieldname": "arrival_qty",
"fieldtype": "Float",
"width": 140,
},
]
)

View File

@@ -11,13 +11,24 @@ def execute(filters=None):
data = get_data(filters)
columns = get_columns(filters)
chart_data = get_chart_data(data, filters)
return columns, data , None, chart_data
return columns, data, None, chart_data
def get_data(filters):
query_filters = {"docstatus": ("<", 2)}
fields = ["name", "status", "report_date", "item_code", "item_name", "sample_size",
"inspection_type", "reference_type", "reference_name", "inspected_by"]
fields = [
"name",
"status",
"report_date",
"item_code",
"item_name",
"sample_size",
"inspection_type",
"reference_type",
"reference_name",
"inspected_by",
]
for field in ["status", "item_code", "status", "inspected_by"]:
if filters.get(field):
@@ -26,36 +37,33 @@ def get_data(filters):
query_filters["report_date"] = (">=", filters.get("from_date"))
query_filters["report_date"] = ("<=", filters.get("to_date"))
return frappe.get_all("Quality Inspection",
fields= fields, filters=query_filters, order_by="report_date asc")
return frappe.get_all(
"Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc"
)
def get_chart_data(periodic_data, columns):
labels = ["Rejected", "Accepted"]
status_wise_data = {
"Accepted": 0,
"Rejected": 0
}
status_wise_data = {"Accepted": 0, "Rejected": 0}
datasets = []
for d in periodic_data:
status_wise_data[d.status] += 1
datasets.append({'name':'Qty Wise Chart',
'values': [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")]})
datasets.append(
{
"name": "Qty Wise Chart",
"values": [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")],
}
)
chart = {
"data": {
'labels': labels,
'datasets': datasets
},
"type": "donut",
"height": 300
}
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "donut", "height": 300}
return chart
def get_columns(filters):
columns = [
{
@@ -63,71 +71,49 @@ def get_columns(filters):
"fieldname": "name",
"fieldtype": "Link",
"options": "Work Order",
"width": 100
"width": 100,
},
{
"label": _("Report Date"),
"fieldname": "report_date",
"fieldtype": "Date",
"width": 150
}
{"label": _("Report Date"), "fieldname": "report_date", "fieldtype": "Date", "width": 150},
]
if not filters.get("status"):
columns.append(
{
"label": _("Status"),
"fieldname": "status",
"width": 100
},
{"label": _("Status"), "fieldname": "status", "width": 100},
)
columns.extend([
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 130
},
{
"label": _("Item Name"),
"fieldname": "item_name",
"fieldtype": "Data",
"width": 130
},
{
"label": _("Sample Size"),
"fieldname": "sample_size",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Inspection Type"),
"fieldname": "inspection_type",
"fieldtype": "Data",
"width": 110
},
{
"label": _("Document Type"),
"fieldname": "reference_type",
"fieldtype": "Data",
"width": 90
},
{
"label": _("Document Name"),
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"options": "reference_type",
"width": 150
},
{
"label": _("Inspected By"),
"fieldname": "inspected_by",
"fieldtype": "Link",
"options": "User",
"width": 150
}
])
columns.extend(
[
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 130,
},
{"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 130},
{"label": _("Sample Size"), "fieldname": "sample_size", "fieldtype": "Float", "width": 110},
{
"label": _("Inspection Type"),
"fieldname": "inspection_type",
"fieldtype": "Data",
"width": 110,
},
{"label": _("Document Type"), "fieldname": "reference_type", "fieldtype": "Data", "width": 90},
{
"label": _("Document Name"),
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"options": "reference_type",
"width": 150,
},
{
"label": _("Inspected By"),
"fieldname": "inspected_by",
"fieldtype": "Link",
"options": "User",
"width": 150,
},
]
)
return columns

View File

@@ -12,12 +12,13 @@ def execute(filters=None):
return columns, data
def get_data(report_filters):
fields = get_fields()
filters = get_filter_condition(report_filters)
wo_items = {}
for d in frappe.get_all("Work Order", filters = filters, fields=fields):
for d in frappe.get_all("Work Order", filters=filters, fields=fields):
d.extra_consumed_qty = 0.0
if d.consumed_qty and d.consumed_qty > d.required_qty:
d.extra_consumed_qty = d.consumed_qty - d.required_qty
@@ -29,7 +30,7 @@ def get_data(report_filters):
for key, wo_data in wo_items.items():
for index, row in enumerate(wo_data):
if index != 0:
#If one work order has multiple raw materials then show parent data in the first row only
# If one work order has multiple raw materials then show parent data in the first row only
for field in ["name", "status", "production_item", "qty", "produced_qty"]:
row[field] = ""
@@ -37,17 +38,28 @@ def get_data(report_filters):
return data
def get_fields():
return ["`tabWork Order Item`.`parent`", "`tabWork Order Item`.`item_code` as raw_material_item_code",
"`tabWork Order Item`.`item_name` as raw_material_name", "`tabWork Order Item`.`required_qty`",
"`tabWork Order Item`.`transferred_qty`", "`tabWork Order Item`.`consumed_qty`", "`tabWork Order`.`status`",
"`tabWork Order`.`name`", "`tabWork Order`.`production_item`", "`tabWork Order`.`qty`",
"`tabWork Order`.`produced_qty`"]
return [
"`tabWork Order Item`.`parent`",
"`tabWork Order Item`.`item_code` as raw_material_item_code",
"`tabWork Order Item`.`item_name` as raw_material_name",
"`tabWork Order Item`.`required_qty`",
"`tabWork Order Item`.`transferred_qty`",
"`tabWork Order Item`.`consumed_qty`",
"`tabWork Order`.`status`",
"`tabWork Order`.`name`",
"`tabWork Order`.`production_item`",
"`tabWork Order`.`qty`",
"`tabWork Order`.`produced_qty`",
]
def get_filter_condition(report_filters):
filters = {
"docstatus": 1, "status": ("in", ["In Process", "Completed", "Stopped"]),
"creation": ("between", [report_filters.from_date, report_filters.to_date])
"docstatus": 1,
"status": ("in", ["In Process", "Completed", "Stopped"]),
"creation": ("between", [report_filters.from_date, report_filters.to_date]),
}
for field in ["name", "production_item", "company", "status"]:
@@ -58,6 +70,7 @@ def get_filter_condition(report_filters):
return filters
def get_columns():
return [
{
@@ -65,67 +78,38 @@ def get_columns():
"fieldname": "name",
"fieldtype": "Link",
"options": "Work Order",
"width": 80
},
{
"label": _("Status"),
"fieldname": "status",
"fieldtype": "Data",
"width": 80
"width": 80,
},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80},
{
"label": _("Production Item"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 130
},
{
"label": _("Qty to Produce"),
"fieldname": "qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("Produced Qty"),
"fieldname": "produced_qty",
"fieldtype": "Float",
"width": 110
"width": 130,
},
{"label": _("Qty to Produce"), "fieldname": "qty", "fieldtype": "Float", "width": 120},
{"label": _("Produced Qty"), "fieldname": "produced_qty", "fieldtype": "Float", "width": 110},
{
"label": _("Raw Material Item"),
"fieldname": "raw_material_item_code",
"fieldtype": "Link",
"options": "Item",
"width": 150
},
{
"label": _("Item Name"),
"fieldname": "raw_material_name",
"width": 130
},
{
"label": _("Required Qty"),
"fieldname": "required_qty",
"fieldtype": "Float",
"width": 100
"width": 150,
},
{"label": _("Item Name"), "fieldname": "raw_material_name", "width": 130},
{"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 100},
{
"label": _("Transferred Qty"),
"fieldname": "transferred_qty",
"fieldtype": "Float",
"width": 100
},
{
"label": _("Consumed Qty"),
"fieldname": "consumed_qty",
"fieldtype": "Float",
"width": 100
"width": 100,
},
{"label": _("Consumed Qty"), "fieldname": "consumed_qty", "fieldtype": "Float", "width": 100},
{
"label": _("Extra Consumed Qty"),
"fieldname": "extra_consumed_qty",
"fieldtype": "Float",
"width": 100
}
"width": 100,
},
]

View File

@@ -12,17 +12,20 @@ def execute(filters=None):
columns = get_columns()
return columns, data
def get_item_list(wo_list, filters):
out = []
#Add a row for each item/qty
# Add a row for each item/qty
for wo_details in wo_list:
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
for wo_item_details in frappe.db.get_values("Work Order Item",
{"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1):
for wo_item_details in frappe.db.get_values(
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
):
item_list = frappe.db.sql("""SELECT
item_list = frappe.db.sql(
"""SELECT
bom_item.item_code as item_code,
ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty
FROM
@@ -36,8 +39,14 @@ def get_item_list(wo_list, filters):
and bom.name = %(bom)s
GROUP BY
bom_item.item_code""",
{"bom": wo_details.bom_no, "warehouse": wo_item_details.source_warehouse,
"filterhouse": filters.warehouse, "item_code": wo_item_details.item_code}, as_dict=1)
{
"bom": wo_details.bom_no,
"warehouse": wo_item_details.source_warehouse,
"filterhouse": filters.warehouse,
"item_code": wo_item_details.item_code,
},
as_dict=1,
)
stock_qty = 0
count = 0
@@ -54,97 +63,99 @@ def get_item_list(wo_list, filters):
else:
build = "N"
row = frappe._dict({
"work_order": wo_details.name,
"status": wo_details.status,
"req_items": cint(count),
"instock": stock_qty,
"description": desc,
"source_warehouse": wo_item_details.source_warehouse,
"item_code": wo_item_details.item_code,
"bom_no": wo_details.bom_no,
"qty": wo_details.qty,
"buildable_qty": buildable_qty,
"ready_to_build": build
})
row = frappe._dict(
{
"work_order": wo_details.name,
"status": wo_details.status,
"req_items": cint(count),
"instock": stock_qty,
"description": desc,
"source_warehouse": wo_item_details.source_warehouse,
"item_code": wo_item_details.item_code,
"bom_no": wo_details.bom_no,
"qty": wo_details.qty,
"buildable_qty": buildable_qty,
"ready_to_build": build,
}
)
out.append(row)
return out
def get_work_orders():
out = frappe.get_all("Work Order", filters={"docstatus": 1, "status": ( "!=","Completed")},
fields=["name","status", "bom_no", "qty", "produced_qty"], order_by='name')
out = frappe.get_all(
"Work Order",
filters={"docstatus": 1, "status": ("!=", "Completed")},
fields=["name", "status", "bom_no", "qty", "produced_qty"],
order_by="name",
)
return out
def get_columns():
columns = [{
"fieldname": "work_order",
"label": "Work Order",
"fieldtype": "Link",
"options": "Work Order",
"width": 110
}, {
"fieldname": "bom_no",
"label": "BOM",
"fieldtype": "Link",
"options": "BOM",
"width": 120
}, {
"fieldname": "description",
"label": "Description",
"fieldtype": "Data",
"options": "",
"width": 230
}, {
"fieldname": "item_code",
"label": "Item Code",
"fieldtype": "Link",
"options": "Item",
"width": 110
},{
"fieldname": "source_warehouse",
"label": "Source Warehouse",
"fieldtype": "Link",
"options": "Warehouse",
"width": 110
},{
"fieldname": "qty",
"label": "Qty to Build",
"fieldtype": "Data",
"options": "",
"width": 110
}, {
"fieldname": "status",
"label": "Status",
"fieldtype": "Data",
"options": "",
"width": 100
}, {
"fieldname": "req_items",
"label": "# Req'd Items",
"fieldtype": "Data",
"options": "",
"width": 105
}, {
"fieldname": "instock",
"label": "# In Stock",
"fieldtype": "Data",
"options": "",
"width": 105
}, {
"fieldname": "buildable_qty",
"label": "Buildable Qty",
"fieldtype": "Data",
"options": "",
"width": 100
}, {
"fieldname": "ready_to_build",
"label": "Build All?",
"fieldtype": "Data",
"options": "",
"width": 90
}]
columns = [
{
"fieldname": "work_order",
"label": "Work Order",
"fieldtype": "Link",
"options": "Work Order",
"width": 110,
},
{"fieldname": "bom_no", "label": "BOM", "fieldtype": "Link", "options": "BOM", "width": 120},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Data",
"options": "",
"width": 230,
},
{
"fieldname": "item_code",
"label": "Item Code",
"fieldtype": "Link",
"options": "Item",
"width": 110,
},
{
"fieldname": "source_warehouse",
"label": "Source Warehouse",
"fieldtype": "Link",
"options": "Warehouse",
"width": 110,
},
{"fieldname": "qty", "label": "Qty to Build", "fieldtype": "Data", "options": "", "width": 110},
{"fieldname": "status", "label": "Status", "fieldtype": "Data", "options": "", "width": 100},
{
"fieldname": "req_items",
"label": "# Req'd Items",
"fieldtype": "Data",
"options": "",
"width": 105,
},
{
"fieldname": "instock",
"label": "# In Stock",
"fieldtype": "Data",
"options": "",
"width": 105,
},
{
"fieldname": "buildable_qty",
"label": "Buildable Qty",
"fieldtype": "Data",
"options": "",
"width": 100,
},
{
"fieldname": "ready_to_build",
"label": "Build All?",
"fieldtype": "Data",
"options": "",
"width": 90,
},
]
return columns

View File

@@ -21,11 +21,23 @@ def execute(filters=None):
chart_data = get_chart_data(data, filters)
return columns, data, None, chart_data
def get_data(filters):
query_filters = {"docstatus": ("<", 2)}
fields = ["name", "status", "sales_order", "production_item", "qty", "produced_qty",
"planned_start_date", "planned_end_date", "actual_start_date", "actual_end_date", "lead_time"]
fields = [
"name",
"status",
"sales_order",
"production_item",
"qty",
"produced_qty",
"planned_start_date",
"planned_end_date",
"actual_start_date",
"actual_end_date",
"lead_time",
]
for field in ["sales_order", "production_item", "status", "company"]:
if filters.get(field):
@@ -34,15 +46,16 @@ def get_data(filters):
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
data = frappe.get_all("Work Order",
fields= fields, filters=query_filters, order_by="planned_start_date asc")
data = frappe.get_all(
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
)
res = []
for d in data:
start_date = d.actual_start_date or d.planned_start_date
d.age = 0
if d.status != 'Completed':
if d.status != "Completed":
d.age = date_diff(today(), start_date)
if filters.get("age") <= d.age:
@@ -50,6 +63,7 @@ def get_data(filters):
return res
def get_chart_data(data, filters):
if filters.get("charts_based_on") == "Status":
return get_chart_based_on_status(data)
@@ -58,6 +72,7 @@ def get_chart_data(data, filters):
else:
return get_chart_based_on_qty(data, filters)
def get_chart_based_on_status(data):
labels = frappe.get_meta("Work Order").get_options("status").split("\n")
if "" in labels:
@@ -71,25 +86,18 @@ def get_chart_based_on_status(data):
values = [status_wise_data[label] for label in labels]
chart = {
"data": {
'labels': labels,
'datasets': [{'name':'Qty Wise Chart', 'values': values}]
},
"data": {"labels": labels, "datasets": [{"name": "Qty Wise Chart", "values": values}]},
"type": "donut",
"height": 300
"height": 300,
}
return chart
def get_chart_based_on_age(data):
labels = ["0-30 Days", "30-60 Days", "60-90 Days", "90 Above"]
age_wise_data = {
"0-30 Days": 0,
"30-60 Days": 0,
"60-90 Days": 0,
"90 Above": 0
}
age_wise_data = {"0-30 Days": 0, "30-60 Days": 0, "60-90 Days": 0, "90 Above": 0}
for d in data:
if d.age > 0 and d.age <= 30:
@@ -101,20 +109,22 @@ def get_chart_based_on_age(data):
else:
age_wise_data["90 Above"] += 1
values = [age_wise_data["0-30 Days"], age_wise_data["30-60 Days"],
age_wise_data["60-90 Days"], age_wise_data["90 Above"]]
values = [
age_wise_data["0-30 Days"],
age_wise_data["30-60 Days"],
age_wise_data["60-90 Days"],
age_wise_data["90 Above"],
]
chart = {
"data": {
'labels': labels,
'datasets': [{'name':'Qty Wise Chart', 'values': values}]
},
"data": {"labels": labels, "datasets": [{"name": "Qty Wise Chart", "values": values}]},
"type": "donut",
"height": 300
"height": 300,
}
return chart
def get_chart_based_on_qty(data, filters):
labels, periodic_data = prepare_chart_data(data, filters)
@@ -129,25 +139,18 @@ def get_chart_based_on_qty(data, filters):
datasets.append({"name": "Completed", "values": completed})
chart = {
"data": {
'labels': labels,
'datasets': datasets
},
"data": {"labels": labels, "datasets": datasets},
"type": "bar",
"barOptions": {
"stacked": 1
}
"barOptions": {"stacked": 1},
}
return chart
def prepare_chart_data(data, filters):
labels = []
periodic_data = {
"Pending": {},
"Completed": {}
}
periodic_data = {"Pending": {}, "Completed": {}}
filters.range = "Monthly"
@@ -165,11 +168,12 @@ def prepare_chart_data(data, filters):
for d in data:
if getdate(d.planned_start_date) >= from_date and getdate(d.planned_start_date) <= end_date:
periodic_data["Pending"][period] += (flt(d.qty) - flt(d.produced_qty))
periodic_data["Pending"][period] += flt(d.qty) - flt(d.produced_qty)
periodic_data["Completed"][period] += flt(d.produced_qty)
return labels, periodic_data
def get_columns(filters):
columns = [
{
@@ -177,90 +181,77 @@ def get_columns(filters):
"fieldname": "name",
"fieldtype": "Link",
"options": "Work Order",
"width": 100
"width": 100,
},
]
if not filters.get("status"):
columns.append(
{
"label": _("Status"),
"fieldname": "status",
"width": 100
},
{"label": _("Status"), "fieldname": "status", "width": 100},
)
columns.extend([
{
"label": _("Production Item"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 130
},
{
"label": _("Produce Qty"),
"fieldname": "qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Produced Qty"),
"fieldname": "produced_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Sales Order"),
"fieldname": "sales_order",
"fieldtype": "Link",
"options": "Sales Order",
"width": 90
},
{
"label": _("Planned Start Date"),
"fieldname": "planned_start_date",
"fieldtype": "Date",
"width": 150
},
{
"label": _("Planned End Date"),
"fieldname": "planned_end_date",
"fieldtype": "Date",
"width": 150
}
])
if filters.get("status") != 'Not Started':
columns.extend([
columns.extend(
[
{
"label": _("Actual Start Date"),
"fieldname": "actual_start_date",
"label": _("Production Item"),
"fieldname": "production_item",
"fieldtype": "Link",
"options": "Item",
"width": 130,
},
{"label": _("Produce Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 110},
{"label": _("Produced Qty"), "fieldname": "produced_qty", "fieldtype": "Float", "width": 110},
{
"label": _("Sales Order"),
"fieldname": "sales_order",
"fieldtype": "Link",
"options": "Sales Order",
"width": 90,
},
{
"label": _("Planned Start Date"),
"fieldname": "planned_start_date",
"fieldtype": "Date",
"width": 100
"width": 150,
},
{
"label": _("Actual End Date"),
"fieldname": "actual_end_date",
"label": _("Planned End Date"),
"fieldname": "planned_end_date",
"fieldtype": "Date",
"width": 100
"width": 150,
},
{
"label": _("Age"),
"fieldname": "age",
"fieldtype": "Float",
"width": 110
},
])
]
)
if filters.get("status") == 'Completed':
columns.extend([
{
"label": _("Lead Time (in mins)"),
"fieldname": "lead_time",
"fieldtype": "Float",
"width": 110
},
])
if filters.get("status") != "Not Started":
columns.extend(
[
{
"label": _("Actual Start Date"),
"fieldname": "actual_start_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Actual End Date"),
"fieldname": "actual_end_date",
"fieldtype": "Date",
"width": 100,
},
{"label": _("Age"), "fieldname": "age", "fieldtype": "Float", "width": 110},
]
)
if filters.get("status") == "Completed":
columns.extend(
[
{
"label": _("Lead Time (in mins)"),
"fieldname": "lead_time",
"fieldtype": "Float",
"width": 110,
},
]
)
return columns