mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-02 03:39:11 +00:00
Merge branch 'develop' into bom-update-tool
This commit is contained in:
@@ -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",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]}],
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"]}],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,2 @@
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'routing',
|
||||
'transactions': [
|
||||
{
|
||||
'items': ['BOM']
|
||||
}
|
||||
]
|
||||
}
|
||||
return {"fieldname": "routing", "transactions": [{"items": ["BOM"]}]}
|
||||
|
||||
@@ -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
@@ -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"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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},
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user