[Enhancement] Work Order Material Consumption (#13384)

* Work Order Material Consumption

* Test cases and other minor fixes

* Test cases fixes

* Travis Fixes

* Work Order Material Consumption Request Changes

* Update work_order.js
This commit is contained in:
Doridel Cahanap 2018-04-09 07:01:28 +02:00 committed by Nabin Hait
parent e9638be032
commit ad76f9ad70
18 changed files with 2834 additions and 2453 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -0,0 +1,34 @@
#Material consumption
Material Consumption functionality allows you to have multiple consumption `Stock Entry` against a Work Order. To enable this, go to Manufacturing > Manufacturing Settings.
<img class="screenshot" alt="Item Alternative" src="{{docs_base_url}}/assets/img/manufacturing/allow-material-consumption.png">
Once enabled, a `Material Consumption` button will be available in Work Order once started.
<img class="screenshot" alt="Item Alternative" src="{{docs_base_url}}/assets/img/manufacturing/material-consumption-button.png">
When button is clicked, it will do the following:
1. It will create Stock Entry with purpose `Material Consumption for Manufacture`.
<img class="screenshot" alt="Item Alternative" src="{{docs_base_url}}/assets/img/manufacturing/material-consumption-for-manufacture.png">
2. If the "Backflush Raw Materials Based On" in the Manufacturing Settings is set to `BOM`, if will propose to consume all required qty for manufacture.
3. If the "Backflush Raw Materials Based On" in the Manufacturing Settings is set to `Material Transferred for Manufacture`, if will propose to consume all transferred qty for manufacture.
4. Once submitted, it will update `Consumed Qty` column in the Work Order.
<img class="screenshot" alt="Item Alternative" src="{{docs_base_url}}/assets/img/manufacturing/consumed-qty.png">
5. In succeeding Material Consumption, it will suggest unconsumed qty.
6. Once "Finish" button is clicked in Work Order, it will take into account consumed qty.
### Validations
* If "Allow Multiple Material Consumption" is not set in Manufacturing Settings but "Material Consumption for Manufacture" is use in Stock Entry.
<img class="screenshot" alt="Item Alternative" src="{{docs_base_url}}/assets/img/manufacturing/material-consumption-stock-entry.gif">
* Cannot cancel "Material Consumption for Manufacture" for completed Work Order.
<img class="screenshot" alt="Item Alternative" src="{{docs_base_url}}/assets/img/manufacturing/cancel-material-consumption-stock-entry.gif">

View File

@ -0,0 +1,5 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Manufacturing Settings', {
});

View File

@ -40,6 +40,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -71,6 +72,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -102,6 +104,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -134,6 +137,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -163,6 +167,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -195,6 +200,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -226,6 +232,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -255,6 +262,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -285,6 +293,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -317,6 +326,39 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Allow multiple Material Consumption against a Work Order",
"fieldname": "material_consumption",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Multiple Material Consumption",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -348,6 +390,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -377,6 +420,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -408,6 +452,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -439,6 +484,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
@ -454,7 +500,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2018-02-16 13:18:17.964103",
"modified": "2018-03-28 13:56:31.187520",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
class TestManufacturingSettings(unittest.TestCase):
pass

View File

@ -60,6 +60,7 @@ class TestWorkOrder(unittest.TestCase):
def test_over_production(self):
from erpnext.manufacturing.doctype.work_order.work_order import StockOverProductionError
wo_doc = self.check_planned_qty()
test_stock_entry.make_stock_entry(item_code="_Test Item",
@ -300,6 +301,7 @@ def make_wo_order_test_record(**args):
wo_order.company = args.company or "_Test Company"
wo_order.stock_uom = args.stock_uom or "_Test UOM"
wo_order.use_multi_level_bom=0
wo_order.skip_transfer=1
wo_order.get_items_and_operations_from_bom()
if args.source_warehouse:

View File

@ -310,21 +310,42 @@ erpnext.work_order = {
}
if(!frm.doc.skip_transfer){
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
&& frm.doc.status != 'Stopped') {
frm.has_finish_btn = true;
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
// If "Material Consumption is check in Manufacturing Settings, allow Material Consumption
frappe.model.get_value('Manufacturing Settings', {'name': 'Manufacturing Settings'}, 'material_consumption', function(d) {
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
&& frm.doc.status != 'Stopped') {
frm.has_finish_btn = true;
if(doc.material_transferred_for_manufacturing==doc.qty) {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
if (d.material_consumption == 1) {
// Only show "Material Consumption" when required_qty > consumed_qty
var counter = 0;
var tbl = frm.doc.required_items || [];
var tbl_lenght = tbl.length;
for (var i = 0, len = tbl_lenght; i < len; i++) {
if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
counter += 1;
}
}
if (counter > 0) {
var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() {
erpnext.work_order.make_consumption_se(frm, d.backflush_raw_materials_based_on);
});
consumption_btn.addClass('btn-primary');
}
}
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
if(doc.material_transferred_for_manufacturing==doc.qty) {
// all materials transferred for manufacturing, make this primary
finish_btn.addClass('btn-primary');
}
}
}
})
} else {
if ((flt(doc.produced_qty) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
frm.has_finish_btn = true;
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.work_order.make_se(frm, 'Manufacture');
});
@ -400,6 +421,30 @@ erpnext.work_order = {
}, __("Select Quantity"), __("Make"));
},
make_consumption_se: function(frm, backflush_raw_materials_based_on) {
if(!frm.doc.skip_transfer){
var max = (backflush_raw_materials_based_on === "Material Transferred for Manufacture") ?
flt(frm.doc.material_transferred_for_manufacturing) - flt(frm.doc.produced_qty) :
flt(frm.doc.qty) - flt(frm.doc.produced_qty);
// flt(frm.doc.qty) - flt(frm.doc.material_transferred_for_manufacturing);
} else {
var max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
}
frappe.call({
method:"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
args: {
"work_order_id": frm.doc.name,
"purpose": "Material Consumption for Manufacture",
"qty": max
},
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
},
stop_work_order: function(frm, status) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",

View File

@ -184,7 +184,7 @@ class WorkOrder(Document):
and purpose=%s""", (self.name, purpose))[0][0])
if qty > self.qty:
frappe.throw(_("{0} ({1}) cannot be greater than planned quanitity ({2}) in Work Order {3}").format(\
frappe.throw(_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format(\
self.meta.get_label(fieldname), qty, self.qty, self.name), StockOverProductionError)
self.db_set(fieldname, qty)
@ -449,6 +449,9 @@ class WorkOrder(Document):
# update in bin
self.update_reserved_qty_for_production()
# calculate consumed qty based on submitted stock entries
self.update_consumed_qty_for_required_items()
def update_reserved_qty_for_production(self, items=None):
'''update reserved_qty_for_production in bins'''
for d in self.required_items:
@ -515,6 +518,24 @@ class WorkOrder(Document):
d.db_set('transferred_qty', flt(transferred_qty), update_modified = False)
def update_consumed_qty_for_required_items(self):
'''update consumed qty from submitted stock entries for that item against
the work order'''
for d in self.required_items:
consumed_qty = frappe.db.sql('''select sum(qty)
from `tabStock Entry` entry, `tabStock Entry Detail` detail
where
entry.work_order = %s
and (entry.purpose = "Material Consumption for Manufacture"
or entry.purpose = "Manufacture")
and entry.docstatus = 1
and detail.parent = entry.name
and detail.item_code = %s''', (self.name, d.item_code))[0][0]
d.db_set('consumed_qty', flt(consumed_qty), update_modified = False)
@frappe.whitelist()
def get_item_details(item, project = None):
res = frappe.db.sql("""
@ -600,9 +621,10 @@ def make_stock_entry(work_order_id, purpose, qty=None):
else:
stock_entry.from_warehouse = wip_warehouse
stock_entry.to_warehouse = work_order.fg_warehouse
additional_costs = get_additional_costs(work_order, fg_qty=stock_entry.fg_completed_qty)
stock_entry.project = work_order.project
stock_entry.set("additional_costs", additional_costs)
if purpose=="Manufacture":
additional_costs = get_additional_costs(work_order, fg_qty=stock_entry.fg_completed_qty)
stock_entry.set("additional_costs", additional_costs)
stock_entry.get_items()
return stock_entry.as_dict()

View File

@ -299,22 +299,24 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_9",
"fieldtype": "Column Break",
"depends_on": "eval:!parent.skip_transfer",
"fieldname": "consumed_qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Consumed Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,

View File

@ -95,6 +95,7 @@ frappe.ui.form.on('Stock Entry', {
refresh: function(frm) {
if(!frm.doc.docstatus) {
frm.trigger('validate_purpose_consumption');
frm.add_custom_button(__('Make Material Request'), function() {
frappe.model.with_doctype('Material Request', function() {
var mr = frappe.model.get_new_doc('Material Request');
@ -168,10 +169,20 @@ frappe.ui.form.on('Stock Entry', {
},
purpose: function(frm) {
frm.trigger('validate_purpose_consumption');
frm.fields_dict.items.grid.refresh();
frm.cscript.toggle_related_fields(frm.doc);
},
validate_purpose_consumption: function(frm) {
frappe.model.get_value('Manufacturing Settings', {'name': 'Manufacturing Settings'}, 'material_consumption', function(d) {
if (d.material_consumption==0 && frm.doc.purpose=="Material Consumption for Manufacture") {
frm.set_value("purpose", 'Manufacture');
frappe.throw(__('Material Consumption is not set in Manufacturing Settings.'));
}
})
},
company: function(frm) {
if(frm.doc.company) {
var company_doc = frappe.get_doc(":Company", frm.doc.company);
@ -592,7 +603,8 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
clean_up: function() {
// Clear Work Order record from locals, because it is updated via Stock Entry
if(this.frm.doc.work_order &&
in_list(["Manufacture", "Material Transfer for Manufacture"], this.frm.doc.purpose)) {
in_list(["Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture"],
this.frm.doc.purpose)) {
frappe.model.remove_from_locals("Work Order",
this.frm.doc.work_order);
}
@ -637,16 +649,17 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
me.frm.set_value("to_warehouse", r.message["wip_warehouse"]);
if (me.frm.doc.purpose == "Manufacture") {
if(r.message["additional_costs"].length) {
$.each(r.message["additional_costs"], function(i, row) {
me.frm.add_child("additional_costs", row);
})
refresh_field("additional_costs");
if (me.frm.doc.purpose == "Manufacture" || me.frm.doc.purpose == "Material Consumption for Manufacture" ) {
if (me.frm.doc.purpose == "Manufacture") {
if (!me.frm.doc.to_warehouse) me.frm.set_value("to_warehouse", r.message["fg_warehouse"]);
if (r.message["additional_costs"].length) {
$.each(r.message["additional_costs"], function(i, row) {
me.frm.add_child("additional_costs", row);
})
refresh_field("additional_costs");
}
}
if (!me.frm.doc.from_warehouse) me.frm.set_value("from_warehouse", r.message["wip_warehouse"]);
if (!me.frm.doc.to_warehouse) me.frm.set_value("to_warehouse", r.message["fg_warehouse"]);
}
me.get_items()
}

View File

@ -41,6 +41,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -72,6 +73,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -104,6 +106,7 @@
"reqd": 1,
"search_index": 0,
"set_only_once": 1,
"translatable": 0,
"unique": 0
},
{
@ -127,7 +130,7 @@
"no_copy": 0,
"oldfieldname": "purpose",
"oldfieldtype": "Select",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nManufacture\nRepack\nSubcontract",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSubcontract",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
@ -137,6 +140,7 @@
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -169,6 +173,7 @@
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -177,7 +182,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\"], doc.purpose)",
"depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
"fieldname": "work_order",
"fieldtype": "Link",
"hidden": 0,
@ -202,6 +207,7 @@
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -234,6 +240,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -267,6 +274,7 @@
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -298,6 +306,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -331,6 +340,7 @@
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -339,7 +349,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \t\t\t\t\t\"Subcontract\", \"Material Transfer for Manufacture\"], doc.purpose)",
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \t\t\t\t\t\"Subcontract\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
"fieldname": "from_bom",
"fieldtype": "Check",
"hidden": 0,
@ -362,6 +372,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -393,6 +404,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -423,6 +435,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "50%"
},
@ -457,6 +470,7 @@
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -489,6 +503,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -520,6 +535,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -550,6 +566,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -581,6 +598,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -614,6 +632,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -642,6 +661,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -674,6 +694,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -705,6 +726,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -734,6 +756,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -766,6 +789,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -798,6 +822,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -828,6 +853,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -856,6 +882,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -888,6 +915,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -920,6 +948,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -950,6 +979,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -979,6 +1009,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1011,6 +1042,7 @@
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1043,6 +1075,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1072,6 +1105,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1103,6 +1137,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1132,6 +1167,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1163,6 +1199,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1194,6 +1231,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1225,6 +1263,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1256,6 +1295,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1287,6 +1327,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1317,6 +1358,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1350,6 +1392,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1382,6 +1425,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1415,6 +1459,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1445,6 +1490,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1474,6 +1520,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1507,6 +1554,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1539,6 +1587,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1571,6 +1620,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1601,6 +1651,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1633,6 +1684,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1664,6 +1716,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1694,6 +1747,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1725,6 +1779,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1756,6 +1811,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1785,6 +1841,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "50%"
},
@ -1817,6 +1874,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1849,6 +1907,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
@ -1880,6 +1939,7 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
@ -1894,7 +1954,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-03-13 12:27:12.884611",
"modified": "2018-03-23 10:59:55.065055",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",

View File

@ -43,6 +43,7 @@ class StockEntry(StockController):
self.validate_posting_time()
self.validate_purpose()
self.validate_item()
self.validate_qty()
self.set_transfer_qty()
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "transfer_qty")
@ -80,15 +81,26 @@ class StockEntry(StockController):
self.update_cost_in_project()
def on_cancel(self):
self.update_stock_ledger()
self.update_work_order()
if self.purchase_order and self.purpose == "Subcontract":
self.update_purchase_order_supplied_items()
if self.work_order and self.purpose == "Material Consumption for Manufacture":
self.validate_work_order_status()
else:
self.update_work_order()
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
def validate_work_order_status(self):
pro_doc = frappe.get_doc("Work Order", self.work_order)
if pro_doc.status == 'Completed':
frappe.throw(_("Cannot cancel transaction for Completed Work Order."))
def validate_purpose(self):
valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture",
"Manufacture", "Repack", "Subcontract"]
"Manufacture", "Repack", "Subcontract", "Material Consumption for Manufacture"]
if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
@ -139,11 +151,39 @@ class StockEntry(StockController):
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
frappe.MandatoryError)
def validate_qty(self):
manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]
if self.purpose in manufacture_purpose and self.work_order:
if not frappe.get_value('Work Order', self.work_order, 'skip_transfer'):
item_code = []
for item in self.items:
if cstr(item.t_warehouse) == '':
req_items = frappe.get_all('Work Order Item',
filters={'parent': self.work_order, 'item_code': item.item_code}, fields=["item_code"])
transferred_materials = frappe.db.sql("""
select
sum(qty) as qty
from `tabStock Entry` se,`tabStock Entry Detail` sed
where
se.name = sed.parent and se.docstatus=1 and
(se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture')
and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != ''
""", (item.item_code, self.work_order), as_dict=1)
stock_qty = flt(item.qty)
trans_qty = flt(transferred_materials[0].qty)
if req_items:
if stock_qty > trans_qty:
item_code.append(item.item_code)
def validate_warehouse(self):
"""perform various (sometimes conditional) validations on warehouse"""
source_mandatory = ["Material Issue", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"]
target_mandatory = ["Material Receipt", "Material Transfer", "Subcontract", "Material Transfer for Manufacture"]
source_mandatory = ["Material Issue", "Material Transfer", "Subcontract", "Material Transfer for Manufacture",
"Material Consumption for Manufacture"]
target_mandatory = ["Material Receipt", "Material Transfer", "Subcontract", "Material Transfer for Manufacture",]
validate_for_manufacture_repack = any([d.bom_no for d in self.get("items")])
@ -196,10 +236,11 @@ class StockEntry(StockController):
frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx))
def validate_work_order(self):
if self.purpose in ("Manufacture", "Material Transfer for Manufacture"):
if self.purpose in ("Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture"):
# check if work order is entered
if self.purpose=="Manufacture" and self.work_order:
if (self.purpose=="Manufacture" or self.purpose=="Material Consumption for Manufacture") \
and self.work_order:
if not self.fg_completed_qty:
frappe.throw(_("For Quantity (Manufactured Qty) is mandatory"))
self.check_if_operations_completed()
@ -234,8 +275,7 @@ class StockEntry(StockController):
where parent in (%s)
and item_code = %s
and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0]
if fg_qty_already_entered >= qty:
if fg_qty_already_entered and fg_qty_already_entered >= qty:
frappe.throw(_("Stock Entries already created for Work Order ")
+ self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError)
@ -590,8 +630,10 @@ class StockEntry(StockController):
self.set_work_order_details()
if self.bom_no:
if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack",
"Subcontract", "Material Transfer for Manufacture"]:
"Subcontract", "Material Transfer for Manufacture", "Material Consumption for Manufacture"]:
if self.work_order and self.purpose == "Material Transfer for Manufacture":
item_dict = self.get_pending_raw_materials()
if self.to_warehouse and self.pro_doc:
@ -599,10 +641,15 @@ class StockEntry(StockController):
item["to_warehouse"] = self.pro_doc.wip_warehouse
self.add_to_stock_entry_detail(item_dict)
elif self.work_order and self.purpose == "Manufacture" and \
elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \
frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "Material Transferred for Manufacture":
self.get_transfered_raw_materials()
elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \
frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "BOM" and \
frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1:
self.get_unconsumed_raw_materials()
else:
if not self.fg_completed_qty:
frappe.throw(_("Manufacturing Quantity is mandatory"))
@ -726,6 +773,45 @@ class StockEntry(StockController):
item.from_warehouse = ""
return item_dict
def get_unconsumed_raw_materials(self):
wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item',
filters={'parent': self.work_order},
fields=["item_code", "required_qty", "consumed_qty"]
)
for item in wo_items:
qty = item.required_qty
item_account_details = frappe.db.get_value("Item", item.item_code, ["item_name",
"description", "stock_uom", "expense_account", "buying_cost_center", "name", "default_warehouse"], as_dict=1)
# Take into account consumption if there are any.
if self.purpose == 'Manufacture':
req_qty_each = flt(item.required_qty / wo.qty)
if (flt(item.consumed_qty) != 0):
remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each)
exhaust_qty = req_qty_each * wo.produced_qty
if remaining_qty > exhaust_qty :
if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1:
qty =0
else:
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
else:
qty = req_qty_each * flt(self.fg_completed_qty)
if qty > 0:
self.add_to_stock_entry_detail({
item.item_code: {
"from_warehouse": wo.wip_warehouse,
"to_warehouse": "",
"qty": qty,
"item_name": item.item_name,
"description": item.description,
"stock_uom": item_account_details.stock_uom,
"expense_account": item_account_details.expense_account,
"cost_center": item_account_details.buying_cost_center,
}
})
def get_transfered_raw_materials(self):
transferred_materials = frappe.db.sql("""
select
@ -744,7 +830,8 @@ class StockEntry(StockController):
from
`tabStock Entry` se, `tabStock Entry Detail` sed
where
se.name = sed.parent and se.docstatus=1 and se.purpose='Manufacture'
se.name = sed.parent and se.docstatus=1
and (se.purpose='Manufacture' or se.purpose='Material Consumption for Manufacture')
and se.work_order= %s and ifnull(sed.s_warehouse, '') != ''
group by sed.item_code, sed.s_warehouse
""", self.work_order, as_dict=1)
@ -755,20 +842,47 @@ class StockEntry(StockController):
po_qty = frappe.db.sql("""select qty, produced_qty, material_transferred_for_manufacturing from
`tabWork Order` where name=%s""", self.work_order, as_dict=1)[0]
manufacturing_qty = flt(po_qty.qty)
produced_qty = flt(po_qty.produced_qty)
trans_qty = flt(po_qty.material_transferred_for_manufacturing)
for item in transferred_materials:
qty= item.qty
req_items = frappe.get_all('Work Order Item',
filters={'parent': self.work_order, 'item_code': item.item_code},
fields=["required_qty", "consumed_qty"]
)
req_qty = flt(req_items[0].required_qty)
req_qty_each = flt(req_qty / manufacturing_qty)
consumed_qty = flt(req_items[0].consumed_qty)
if trans_qty and manufacturing_qty >= (produced_qty + flt(self.fg_completed_qty)):
if qty >= req_qty:
qty = (req_qty/trans_qty) * flt(self.fg_completed_qty)
else:
qty = qty - consumed_qty
if self.purpose == 'Manufacture':
# If Material Consumption is booked, must pull only remaining components to finish product
if consumed_qty != 0:
remaining_qty = consumed_qty - (produced_qty * req_qty_each)
exhaust_qty = req_qty_each * produced_qty
if remaining_qty > exhaust_qty :
if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1:
qty =0
else:
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
else:
qty = req_qty_each * flt(self.fg_completed_qty)
if trans_qty and manufacturing_qty > (produced_qty + flt(self.fg_completed_qty)):
qty = (qty/trans_qty) * flt(self.fg_completed_qty)
elif backflushed_materials.get(item.item_code):
for d in backflushed_materials.get(item.item_code):
if d.get(item.warehouse):
qty-= d.get(item.warehouse)
if (qty > req_qty):
qty = req_qty
qty-= d.get(item.warehouse)
if qty > 0:
self.add_to_stock_entry_detail({

View File

@ -575,7 +575,8 @@ class TestStockEntry(unittest.TestCase):
"bom_no": bom_no,
"qty": 1.0,
"stock_uom": "_Test UOM",
"wip_warehouse": "_Test Warehouse - _TC"
"wip_warehouse": "_Test Warehouse - _TC",
"skip_transfer": 1
})
work_order.insert()
work_order.submit()
@ -680,6 +681,34 @@ class TestStockEntry(unittest.TestCase):
repack.insert()
self.assertRaises(frappe.ValidationError, repack.submit)
def test_material_consumption(self):
from erpnext.manufacturing.doctype.work_order.work_order \
import make_stock_entry as _make_stock_entry
bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
"is_default": 1, "docstatus": 1}, ["name", "operating_cost"])
work_order = frappe.new_doc("Work Order")
work_order.update({
"company": "_Test Company",
"fg_warehouse": "_Test Warehouse 1 - _TC",
"production_item": "_Test FG Item 2",
"bom_no": bom_no,
"qty": 4.0,
"stock_uom": "_Test UOM",
"wip_warehouse": "_Test Warehouse - _TC",
"additional_operating_cost": 1000
})
work_order.insert()
work_order.submit()
make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
self.assertEqual(stock_entry.get("items")[0].qty, 10)
self.assertEqual(stock_entry.get("items")[1].qty, 6)
def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None):
se = frappe.copy_doc(test_records[0])
se.get("items")[0].item_code = item_code or "_Test Serialized Item With Series"