Production Order Enhancements (#9432)

* Production Order Enhancements
  - Show required items child table
  - Source warehouse for each raw materials, in Pro Order Item and BOM Item table
  - Group warehouse allowed for source and wip warehouse
  - Patch to populate required items, to fix status and reserved qty for stopped pro order
  - Cleaned up existing codes
  - Test cases

* Set available qty in source and wip warehouse

* minor fix in bom query naming

* Minor Fixes

* Reload BOM doctypes in patch
This commit is contained in:
Nabin Hait 2017-07-05 13:55:41 +05:30 committed by Makarand Bauskar
parent 852cb64e4f
commit 949a920022
17 changed files with 986 additions and 508 deletions

View File

@ -5,12 +5,24 @@ frappe.provide("erpnext.bom");
frappe.ui.form.on("BOM", {
setup: function(frm) {
frm.add_fetch('buying_price_list', 'currency', 'currency');
frm.fields_dict["items"].grid.get_field("bom_no").get_query = function(doc, cdt, cdn){
frm.add_fetch('buying_price_list', 'currency', 'currency')
frm.set_query("bom_no", "items", function() {
return {
filters: {'currency': frm.doc.currency}
filters: {
'currency': frm.doc.currency,
'company': frm.doc.company
}
}
}
});
frm.set_query("source_warehouse", "items", function() {
return {
filters: {
'company': frm.doc.company,
}
}
});
},
onload_post_render: function(frm) {

View File

@ -408,6 +408,7 @@ class BOM(WebsiteGenerator):
self.add_to_cur_exploded_items(frappe._dict({
'item_code' : d.item_code,
'item_name' : d.item_name,
'source_warehouse': d.source_warehouse,
'description' : d.description,
'image' : d.image,
'stock_uom' : d.stock_uom,
@ -427,7 +428,8 @@ class BOM(WebsiteGenerator):
def get_child_exploded_items(self, bom_no, stock_qty):
""" Add all items from Flat BOM of child BOM"""
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
child_fb_items = frappe.db.sql("""select bom_item.item_code, bom_item.item_name, bom_item.description,
child_fb_items = frappe.db.sql("""select bom_item.item_code, bom_item.item_name,
bom_item.description, bom_item.source_warehouse,
bom_item.stock_uom, bom_item.stock_qty, bom_item.rate,
bom_item.stock_qty / ifnull(bom.quantity, 1) as qty_consumed_per_unit
from `tabBOM Explosion Item` bom_item, tabBOM bom
@ -437,9 +439,10 @@ class BOM(WebsiteGenerator):
self.add_to_cur_exploded_items(frappe._dict({
'item_code' : d['item_code'],
'item_name' : d['item_name'],
'source_warehouse' : d['source_warehouse'],
'description' : d['description'],
'stock_uom' : d['stock_uom'],
'stock_qty' : d['qty_consumed_per_unit']*stock_qty,
'stock_qty' : d['qty_consumed_per_unit'] * stock_qty,
'rate' : flt(d['rate']),
}))
@ -493,6 +496,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
item.default_warehouse,
item.expense_account as expense_account,
item.buying_cost_center as cost_center
{select_columns}
from
`tab{table}` bom_item, `tabBOM` bom, `tabItem` item
where
@ -501,18 +505,20 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
and bom_item.parent = bom.name
and item.name = bom_item.item_code
and is_stock_item = 1
{conditions}
group by item_code, stock_uom"""
{where_conditions}
group by item_code, stock_uom"""
if fetch_exploded:
query = query.format(table="BOM Explosion Item",
conditions="""and item.is_sub_contracted_item = 0""")
where_conditions="""and item.is_sub_contracted_item = 0""",
select_columns = ", bom_item.source_warehouse")
items = frappe.db.sql(query, { "qty": qty, "bom": bom }, as_dict=True)
elif fetch_scrap_items:
query = query.format(table="BOM Scrap Item", conditions="")
query = query.format(table="BOM Scrap Item", where_conditions="", select_columns="")
items = frappe.db.sql(query, { "qty": qty, "bom": bom }, as_dict=True)
else:
query = query.format(table="BOM Item", conditions="")
query = query.format(table="BOM Item", where_conditions="",
select_columns = ", bom_item.source_warehouse")
items = frappe.db.sql(query, { "qty": qty, "bom": bom }, as_dict=True)
for item in items:

View File

@ -8,7 +8,8 @@
"parentfield": "items",
"stock_qty": 1.0,
"rate": 5000.0,
"stock_uom": "_Test UOM"
"stock_uom": "_Test UOM",
"source_warehouse": "_Test Warehouse - _TC"
},
{
"amount": 2000.0,
@ -17,7 +18,8 @@
"parentfield": "items",
"stock_qty": 2.0,
"rate": 1000.0,
"stock_uom": "_Test UOM"
"stock_uom": "_Test UOM",
"source_warehouse": "_Test Warehouse - _TC"
}
],
"docstatus": 1,
@ -48,7 +50,8 @@
"parentfield": "items",
"stock_qty": 1.0,
"rate": 5000.0,
"stock_uom": "_Test UOM"
"stock_uom": "_Test UOM",
"source_warehouse": "_Test Warehouse - _TC"
},
{
"amount": 2000.0,
@ -57,7 +60,8 @@
"parentfield": "items",
"stock_qty": 2.0,
"rate": 1000.0,
"stock_uom": "_Test UOM"
"stock_uom": "_Test UOM",
"source_warehouse": "_Test Warehouse - _TC"
}
],
"docstatus": 1,
@ -86,7 +90,8 @@
"parentfield": "items",
"stock_qty": 1.0,
"rate": 5000.0,
"stock_uom": "_Test UOM"
"stock_uom": "_Test UOM",
"source_warehouse": "_Test Warehouse - _TC"
},
{
"amount": 2000.0,
@ -96,7 +101,8 @@
"parentfield": "items",
"stock_qty": 3.0,
"rate": 1000.0,
"stock_uom": "_Test UOM"
"stock_uom": "_Test UOM",
"source_warehouse": "_Test Warehouse - _TC"
}
],
"docstatus": 1,
@ -126,7 +132,8 @@
"parentfield": "items",
"stock_qty": 2.0,
"rate": 3000.0,
"stock_uom": "_Test UOM"
"stock_uom": "_Test UOM",
"source_warehouse": "_Test Warehouse - _TC"
}
],
"docstatus": 1,

View File

@ -7,9 +7,9 @@
"beta": 0,
"creation": "2013-03-07 11:42:57",
"custom": 0,
"default_print_format": "Standard",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
@ -104,6 +104,37 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "source_warehouse",
"fieldtype": "Link",
"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": "Source Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -481,7 +512,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-06-02 19:29:34.498719",
"modified": "2017-05-29 17:51:18.151002",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Explosion Item",

View File

@ -22,7 +22,7 @@
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
@ -113,7 +113,7 @@
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
@ -136,6 +136,37 @@
"unique": 0,
"width": "150px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "source_warehouse",
"fieldtype": "Link",
"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": "Source Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -729,7 +760,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-05-23 15:59:37.946963",
"modified": "2017-05-29 17:42:37.216408",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",

View File

@ -7,7 +7,63 @@ frappe.ui.form.on("Production Order", {
'Timesheet': 'Make Timesheet',
'Stock Entry': 'Make Stock Entry',
}
// Set query for warehouses
frm.set_query("wip_warehouse", function(doc) {
return {
filters: {
'company': frm.doc.company,
}
}
});
frm.set_query("source_warehouse", "required_items", function() {
return {
filters: {
'company': frm.doc.company,
}
}
});
frm.set_query("fg_warehouse", function() {
return {
filters: {
'company': frm.doc.company,
'is_group': 0
}
}
});
// Set query for BOM
frm.set_query("bom_no", function() {
if (frm.doc.production_item) {
return{
query: "erpnext.controllers.queries.bom",
filters: {item: cstr(frm.doc.production_item)}
}
} else msgprint(__("Please enter Production Item first"));
});
// Set query for FG Item
frm.set_query("production_item", function() {
return {
query: "erpnext.controllers.queries.item_query",
filters:{
'is_stock_item': 1,
}
}
});
// Set query for FG Item
frm.set_query("project", function() {
return{
filters:[
['Project', 'status', 'not in', 'Completed, Cancelled']
]
}
});
},
onload: function(frm) {
if (!frm.doc.status)
frm.doc.status = 'Draft';
@ -28,9 +84,8 @@ frappe.ui.form.on("Production Order", {
function(doc) { return (frm.doc.qty==doc.completed_qty) ? "green" : "orange" })
erpnext.production_order.set_custom_buttons(frm);
erpnext.production_order.setup_company_filter(frm);
erpnext.production_order.setup_bom_filter(frm);
},
refresh: function(frm) {
erpnext.toggle_naming_series();
erpnext.production_order.set_custom_buttons(frm);
@ -53,6 +108,7 @@ frappe.ui.form.on("Production Order", {
})
}
},
show_progress: function(frm) {
var bars = [];
var message = '';
@ -85,10 +141,83 @@ frappe.ui.form.on("Production Order", {
}
}
frm.dashboard.add_progress(__('Status'), bars, message);
},
production_item: function(frm) {
frappe.call({
method: "erpnext.manufacturing.doctype.production_order.production_order.get_item_details",
args: {
item: frm.doc.production_item,
project: frm.doc.project
},
callback: function(r) {
if(r.message) {
erpnext.in_production_item_onchange = true;
$.each(["description", "stock_uom", "project", "bom_no"], function(i, field) {
frm.set_value(field, r.message[field]);
});
if(r.message["set_scrap_wh_mandatory"]){
frm.toggle_reqd("scrap_warehouse", true);
}
erpnext.in_production_item_onchange = false;
}
}
});
},
project: function(frm) {
if(!erpnext.in_production_item_onchange) {
frm.trigger("production_item");
}
},
bom_no: function(frm) {
return frm.call({
doc: frm.doc,
method: "get_items_and_operations_from_bom",
callback: function(r) {
if(r.message["set_scrap_wh_mandatory"]){
frm.toggle_reqd("scrap_warehouse", true);
}
}
});
},
use_multi_level_bom: function(frm) {
if(frm.doc.bom_no) {
frm.trigger("bom_no");
}
},
qty: function(frm) {
frm.trigger('bom_no');
},
before_submit: function(frm) {
frm.toggle_reqd(["fg_warehouse", "wip_warehouse"], true);
frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true);
}
});
frappe.ui.form.on("Production Order Item", {
source_warehouse: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
if(row.source_warehouse) {
frappe.call({
"method": "erpnext.stock.utils.get_latest_stock_qty",
args: {
item_code: row.item_code,
warehouse: row.source_warehouse
},
callback: function (r) {
frappe.model.set_value(row.doctype, row.name,
"available_qty_at_source_warehouse", r.message);
}
})
}
}
})
frappe.ui.form.on("Production Order Operation", {
workstation: function(frm, cdt, cdn) {
@ -119,38 +248,45 @@ erpnext.production_order = {
var doc = frm.doc;
if (doc.docstatus === 1) {
if (doc.status != 'Stopped' && doc.status != 'Completed') {
frm.add_custom_button(__('Stop'), cur_frm.cscript['Stop Production Order'], __("Status"));
frm.add_custom_button(__('Stop'), function() {
erpnext.production_order.stop_production_order(frm, "Stopped");
}, __("Status"));
} else if (doc.status == 'Stopped') {
frm.add_custom_button(__('Re-open'), cur_frm.cscript['Unstop Production Order'], __("Status"));
frm.add_custom_button(__('Re-open'), function() {
erpnext.production_order.stop_production_order(frm, "Resumed");
}, __("Status"));
}
if(!frm.doc.skip_transfer){
if ((flt(doc.material_transferred_for_manufacturing) < flt(doc.qty)) && frm.doc.status != 'Stopped') {
if ((flt(doc.material_transferred_for_manufacturing) < flt(doc.qty))
&& frm.doc.status != 'Stopped') {
frm.has_start_btn = true;
var btn = frm.add_custom_button(__('Start'),
cur_frm.cscript['Transfer Raw Materials']);
btn.addClass('btn-primary');
}
var start_btn = frm.add_custom_button(__('Start'), function() {
erpnext.production_order.make_se(frm, 'Material Transfer for Manufacture');
});
start_btn.addClass('btn-primary');
}
}
if(!frm.doc.skip_transfer){
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing)) && frm.doc.status != 'Stopped') {
if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))
&& frm.doc.status != 'Stopped') {
frm.has_finish_btn = true;
var btn = frm.add_custom_button(__('Finish'),
cur_frm.cscript['Update Finished Goods']);
var finish_btn = frm.add_custom_button(__('Finish'), function() {
erpnext.production_order.make_se(frm, 'Manufacture');
});
if(doc.material_transferred_for_manufacturing==doc.qty) {
// all materials transferred for manufacturing,
// make this primary
btn.addClass('btn-primary');
// 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 btn = frm.add_custom_button(__('Finish'),
var finish_btn = frm.add_custom_button(__('Finish'),
cur_frm.cscript['Update Finished Goods']);
btn.addClass('btn-primary');
finish_btn.addClass('btn-primary');
}
}
}
@ -162,8 +298,8 @@ erpnext.production_order = {
doc.planned_operating_cost = 0.0;
for(var i=0;i<op.length;i++) {
var planned_operating_cost = flt(flt(op[i].hour_rate) * flt(op[i].time_in_mins) / 60, 2);
frappe.model.set_value('Production Order Operation',op[i].name, "planned_operating_cost", planned_operating_cost);
frappe.model.set_value('Production Order Operation', op[i].name,
"planned_operating_cost", planned_operating_cost);
doc.planned_operating_cost += planned_operating_cost;
}
refresh_field('planned_operating_cost');
@ -176,37 +312,10 @@ erpnext.production_order = {
frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost))
},
setup_company_filter: function(frm) {
var company_filter = function(doc) {
return {
filters: {
'company': frm.doc.company,
'is_group': 0
}
}
}
frm.fields_dict.source_warehouse.get_query = company_filter;
frm.fields_dict.fg_warehouse.get_query = company_filter;
frm.fields_dict.wip_warehouse.get_query = company_filter;
},
setup_bom_filter: function(frm) {
frm.set_query("bom_no", function(doc) {
if (doc.production_item) {
return{
query: "erpnext.controllers.queries.bom",
filters: {item: cstr(doc.production_item)}
}
} else frappe.msgprint(__("Please enter Production Item first"));
});
},
set_default_warehouse: function(frm) {
if (!(frm.doc.wip_warehouse || frm.doc.fg_warehouse)) {
frappe.call({
method: "erpnext.manufacturing.doctype.production_order.production_order.get_default_warehouse",
callback: function(r) {
if(!r.exe) {
frm.set_value("wip_warehouse", r.message.wip_warehouse);
@ -215,45 +324,15 @@ erpnext.production_order = {
}
});
}
}
}
$.extend(cur_frm.cscript, {
before_submit: function() {
cur_frm.toggle_reqd(["fg_warehouse", "wip_warehouse"], true);
},
production_item: function(doc) {
frappe.call({
method: "erpnext.manufacturing.doctype.production_order.production_order.get_item_details",
args: {
item: doc.production_item,
project: doc.project
},
callback: function(r) {
$.each(["description", "stock_uom", "project", "bom_no"], function(i, field) {
cur_frm.set_value(field, r.message[field]);
});
if(r.message["set_scrap_wh_mandatory"]){
cur_frm.toggle_reqd("scrap_warehouse", true);
}
}
});
},
project: function(doc) {
cur_frm.cscript.production_item(doc)
},
make_se: function(purpose) {
var me = this;
if(!this.frm.doc.skip_transfer){
make_se: function(frm, purpose) {
if(!frm.doc.skip_transfer){
var max = (purpose === "Manufacture") ?
flt(this.frm.doc.material_transferred_for_manufacturing) - flt(this.frm.doc.produced_qty) :
flt(this.frm.doc.qty) - flt(this.frm.doc.material_transferred_for_manufacturing);
flt(frm.doc.material_transferred_for_manufacturing) - flt(frm.doc.produced_qty) :
flt(frm.doc.qty) - flt(frm.doc.material_transferred_for_manufacturing);
} else {
var max = flt(this.frm.doc.qty) - flt(this.frm.doc.produced_qty);
var max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
}
frappe.prompt({fieldtype:"Float", label: __("Qty for {0}", [purpose]), fieldname:"qty",
@ -266,7 +345,7 @@ $.extend(cur_frm.cscript, {
frappe.call({
method:"erpnext.manufacturing.doctype.production_order.production_order.make_stock_entry",
args: {
"production_order_id": me.frm.doc.name,
"production_order_id": frm.doc.name,
"purpose": purpose,
"qty": data.qty
},
@ -277,59 +356,20 @@ $.extend(cur_frm.cscript, {
});
}, __("Select Quantity"), __("Make"));
},
bom_no: function() {
return this.frm.call({
doc: this.frm.doc,
method: "set_production_order_operations",
stop_production_order: function(frm, status) {
frappe.call({
method: "erpnext.manufacturing.doctype.production_order.production_order.stop_unstop",
args: {
production_order: frm.doc.name,
status: status
},
callback: function(r) {
if(r.message["set_scrap_wh_mandatory"]){
cur_frm.toggle_reqd("scrap_warehouse", true);
if(r.message) {
frm.set_value("status", r.message);
frm.reload_doc();
}
}
});
},
use_multi_level_bom: function() {
if(this.frm.doc.bom_no) {
this.frm.trigger("bom_no");
}
},
qty: function() {
frappe.ui.form.trigger("Production Order", 'bom_no')
},
});
cur_frm.cscript['Stop Production Order'] = function() {
$c_obj(cur_frm.doc, 'stop_unstop', 'Stopped', function(r, rt) {cur_frm.refresh();});
}
cur_frm.cscript['Unstop Production Order'] = function() {
$c_obj(cur_frm.doc, 'stop_unstop', 'Unstopped', function(r, rt) {cur_frm.refresh();});
}
cur_frm.cscript['Transfer Raw Materials'] = function() {
cur_frm.cscript.make_se('Material Transfer for Manufacture');
}
cur_frm.cscript['Update Finished Goods'] = function() {
cur_frm.cscript.make_se('Manufacture');
}
cur_frm.fields_dict['production_item'].get_query = function(doc) {
return {
query: "erpnext.controllers.queries.item_query",
filters:{
'is_stock_item': 1,
}
}
}
cur_frm.fields_dict['project'].get_query = function(doc, dt, dn) {
return{
filters:[
['Project', 'status', 'not in', 'Completed, Cancelled']
]
})
}
}

View File

@ -95,7 +95,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nSubmitted\nNot Started\nStopped\nUnstopped\nIn Process\nCompleted\nCancelled",
"options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nStopped\nCancelled",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
@ -139,38 +139,6 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "project",
"fieldtype": "Link",
"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": "Project",
"length": 0,
"no_copy": 0,
"oldfieldname": "project",
"oldfieldtype": "Link",
"options": "Project",
"permlevel": 0,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -266,37 +234,6 @@
"unique": 0,
"width": "50%"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "sales_order",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sales Order",
"length": 0,
"no_copy": 0,
"options": "Sales Order",
"permlevel": 0,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -329,37 +266,6 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Check if material transfer entry is not required",
"fieldname": "skip_transfer",
"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": "Skip Material Transfer",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -427,6 +333,100 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "sales_order",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Sales Order",
"length": 0,
"no_copy": 0,
"options": "Sales Order",
"permlevel": 0,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "project",
"fieldtype": "Link",
"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": "Project",
"length": 0,
"no_copy": 0,
"oldfieldname": "project",
"oldfieldtype": "Link",
"options": "Project",
"permlevel": 0,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Check if material transfer entry is not required",
"fieldname": "skip_transfer",
"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": "Skip Material Transfer",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -457,38 +457,6 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "source_warehouse",
"fieldtype": "Link",
"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": "Source Warehouse (for reserving Items)",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -519,34 +487,6 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_12",
"fieldtype": "Column Break",
"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,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -579,6 +519,34 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_12",
"fieldtype": "Column Break",
"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,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -616,7 +584,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "time",
"fieldname": "required_items_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
@ -625,10 +593,9 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Time",
"label": "Required Items",
"length": 0,
"no_copy": 0,
"options": "fa fa-time",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -647,9 +614,8 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "expected_delivery_date",
"fieldtype": "Date",
"fieldname": "required_items",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -657,10 +623,43 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Expected Delivery Date",
"label": "Required Items",
"length": 0,
"no_copy": 1,
"options": "Production Order Item",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "time",
"fieldtype": "Section Break",
"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": "Time",
"length": 0,
"no_copy": 0,
"options": "fa fa-time",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
@ -708,7 +707,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "planned_end_date",
"fieldname": "actual_start_date",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
@ -717,9 +716,9 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Planned End Date",
"label": "Actual Start Date",
"length": 0,
"no_copy": 1,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -767,7 +766,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "actual_start_date",
"fieldname": "planned_end_date",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
@ -776,9 +775,9 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Actual Start Date",
"label": "Planned End Date",
"length": 0,
"no_copy": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -821,6 +820,36 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "expected_delivery_date",
"fieldtype": "Date",
"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": "Expected Delivery Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -1070,67 +1099,6 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "required_items_section",
"fieldtype": "Section Break",
"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": "Required Items",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "required_items",
"fieldtype": "Table",
"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": "Required Items",
"length": 0,
"no_copy": 1,
"options": "Production Order Item",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,

View File

@ -3,24 +3,21 @@
from __future__ import unicode_literals
import frappe
import json
from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate
from frappe import _
from frappe.utils import time_diff_in_seconds
from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict
from dateutil.relativedelta import relativedelta
from erpnext.stock.doctype.item.item import validate_end_of_life
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError, NotInWorkingHoursError
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError
from erpnext.projects.doctype.timesheet.timesheet import OverlapError
from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
from erpnext.stock.utils import get_bin
from frappe.utils.csvutils import getlink
from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
from erpnext.utilities.transaction_base import validate_uom_is_integer
class OverProductionError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass
@ -38,15 +35,19 @@ class ProductionOrder(Document):
validate_bom_no(self.production_item, self.bom_no)
self.validate_sales_order()
self.validate_warehouse()
self.validate_warehouse_belongs_to_company()
self.calculate_operating_cost()
self.validate_qty()
self.validate_operation_time()
self.status = self.get_status()
from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
if not self.get("required_items"):
self.set_required_items()
else:
self.set_available_qty()
def validate_sales_order(self):
if self.sales_order:
so = frappe.db.sql("""select name, delivery_date, project from `tabSales Order`
@ -64,11 +65,14 @@ class ProductionOrder(Document):
else:
frappe.throw(_("Sales Order {0} is not valid").format(self.sales_order))
def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company
def validate_warehouse_belongs_to_company(self):
warehouses = [self.fg_warehouse, self.wip_warehouse]
for d in self.get("required_items"):
if d.source_warehouse not in warehouses:
warehouses.append(d.source_warehouse)
for w in [self.source_warehouse, self.fg_warehouse, self.wip_warehouse]:
validate_warehouse_company(w, self.company)
for wh in warehouses:
validate_warehouse_company(wh, self.company)
def calculate_operating_cost(self):
self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
@ -79,7 +83,8 @@ class ProductionOrder(Document):
self.planned_operating_cost += flt(d.planned_operating_cost)
self.actual_operating_cost += flt(d.actual_operating_cost)
variable_cost = self.actual_operating_cost if self.actual_operating_cost else self.planned_operating_cost
variable_cost = self.actual_operating_cost if self.actual_operating_cost \
else self.planned_operating_cost
self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost)
def validate_production_order_against_so(self):
@ -101,22 +106,16 @@ class ProductionOrder(Document):
# total qty in SO
so_qty = flt(so_item_qty) + flt(dnpi_qty)
allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", "over_production_allowance_percentage"))
allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
"over_production_allowance_percentage"))
if total_qty > so_qty + (allowance_percentage/100 * so_qty):
frappe.throw(_("Cannot produce more Item {0} than Sales Order quantity {1}").format(self.production_item,
so_qty), OverProductionError)
def stop_unstop(self, status):
""" Called from client side on Stop/Unstop event"""
status = self.update_status(status)
self.update_planned_qty()
frappe.msgprint(_("Production Order status is {0}").format(status))
self.notify_update()
frappe.throw(_("Cannot produce more Item {0} than Sales Order quantity {1}")
.format(self.production_item, so_qty), OverProductionError)
def update_status(self, status=None):
'''Update status of production order if unknown'''
if not status:
if status != "Stopped":
status = self.get_status(status)
if status != self.status:
@ -167,7 +166,6 @@ class ProductionOrder(Document):
self.db_set(fieldname, qty)
def before_submit(self):
self.set_required_items()
self.make_time_logs()
def on_submit(self):
@ -184,10 +182,10 @@ class ProductionOrder(Document):
self.validate_cancel()
frappe.db.set(self,'status', 'Cancelled')
self.clear_required_items()
self.delete_timesheet()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_reserved_qty_for_production()
def validate_cancel(self):
if self.status == "Stopped":
@ -214,12 +212,11 @@ class ProductionOrder(Document):
def set_production_order_operations(self):
"""Fetch operations from BOM and set in 'Production Order'"""
self.set('operations', [])
if not self.bom_no \
or cint(frappe.db.get_single_value("Manufacturing Settings", "disable_capacity_planning")):
return
self.set('operations', [])
if self.use_multi_level_bom:
bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree()
@ -240,8 +237,6 @@ class ProductionOrder(Document):
self.set('operations', operations)
self.calculate_time()
return check_if_scrap_warehouse_mandatory(self.bom_no)
def calculate_time(self):
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
@ -403,62 +398,60 @@ class ProductionOrder(Document):
update bin reserved_qty_for_production
called from Stock Entry for production, after submit, cancel
'''
if self.docstatus==1 and self.source_warehouse:
if self.material_transferred_for_manufacturing == self.produced_qty:
# clear required items table and save document
self.clear_required_items()
else:
# calculate transferred qty based on submitted
# stock entries
self.update_transaferred_qty_for_required_items()
if self.docstatus==1:
# calculate transferred qty based on submitted stock entries
self.update_transaferred_qty_for_required_items()
# update in bin
self.update_reserved_qty_for_production()
def clear_required_items(self):
'''Remove the required_items table and update the bins'''
items = [d.item_code for d in self.required_items]
self.required_items = []
self.update_child_table('required_items')
# completed, update reserved qty in bin
self.update_reserved_qty_for_production(items)
# update in bin
self.update_reserved_qty_for_production()
def update_reserved_qty_for_production(self, items=None):
'''update reserved_qty_for_production in bins'''
if not self.source_warehouse:
return
if not items:
items = [d.item_code for d in self.required_items]
for item in items:
stock_bin = get_bin(item, self.source_warehouse)
stock_bin.update_reserved_qty_for_production()
for d in self.required_items:
if d.source_warehouse:
stock_bin = get_bin(d.item_code, d.source_warehouse)
stock_bin.update_reserved_qty_for_production()
def get_items_and_operations_from_bom(self):
self.set_required_items()
self.set_production_order_operations()
return check_if_scrap_warehouse_mandatory(self.bom_no)
def set_available_qty(self):
for d in self.get("required_items"):
if d.source_warehouse:
d.available_qty_at_source_warehouse = get_latest_stock_qty(d.item_code, d.source_warehouse)
if self.wip_warehouse:
d.available_qty_at_wip_warehouse = get_latest_stock_qty(d.item_code, self.wip_warehouse)
def set_required_items(self):
'''set required_items for production to keep track of reserved qty'''
if self.source_warehouse:
self.required_items = []
if self.bom_no and self.qty:
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=self.qty,
fetch_exploded = self.use_multi_level_bom)
for item in item_dict.values():
self.append('required_items', {'item_code': item.item_code,
'required_qty': item.qty})
#print frappe.as_json(self.required_items)
self.append('required_items', {
'item_code': item.item_code,
'required_qty': item.qty,
'source_warehouse': item.source_warehouse or item.default_warehouse
})
self.set_available_qty()
def update_transaferred_qty_for_required_items(self):
'''update transferred qty from submitted stock entries for that item against
the production order'''
for d in self.required_items:
transferred_qty = frappe.db.sql('''select count(qty)
transferred_qty = frappe.db.sql('''select sum(qty)
from `tabStock Entry` entry, `tabStock Entry Detail` detail
where
entry.production_order = %s
entry.purpose = "Material Transfer for Manufacture"
and entry.purpose = "Material Transfer for Manufacture"
and entry.docstatus = 1
and detail.parent = entry.name
and detail.item_code = %s''', (self.name, d.item_code))[0][0]
@ -496,10 +489,12 @@ def get_item_details(item, project = None):
if not res["bom_no"]:
if project:
frappe.throw(_("Default BOM for {0} not found for Project {1}").format(item, project))
frappe.throw(_("Default BOM for {0} not found").format(item))
res = get_item_details(item)
frappe.msgprint(_("Default BOM not found for Item {0} and Project {1}").format(item, project))
else:
frappe.throw(_("Default BOM for {0} not found").format(item))
res['project'] = frappe.db.get_value('BOM', res['bom_no'], 'project')
res['project'] = project or frappe.db.get_value('BOM', res['bom_no'], 'project')
res.update(check_if_scrap_warehouse_mandatory(res["bom_no"]))
return res
@ -507,16 +502,21 @@ def get_item_details(item, project = None):
@frappe.whitelist()
def check_if_scrap_warehouse_mandatory(bom_no):
res = {"set_scrap_wh_mandatory": False }
bom = frappe.get_doc("BOM", bom_no)
if bom_no:
bom = frappe.get_doc("BOM", bom_no)
if len(bom.scrap_items) > 0:
res["set_scrap_wh_mandatory"] = True
if len(bom.scrap_items) > 0:
res["set_scrap_wh_mandatory"] = True
return res
@frappe.whitelist()
def make_stock_entry(production_order_id, purpose, qty=None):
production_order = frappe.get_doc("Production Order", production_order_id)
if not frappe.db.get_value("Warehouse", production_order.wip_warehouse, "is_group"):
wip_warehouse = production_order.wip_warehouse
else:
wip_warehouse = None
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.purpose = purpose
@ -528,12 +528,10 @@ def make_stock_entry(production_order_id, purpose, qty=None):
stock_entry.fg_completed_qty = qty or (flt(production_order.qty) - flt(production_order.produced_qty))
if purpose=="Material Transfer for Manufacture":
if production_order.source_warehouse:
stock_entry.from_warehouse = production_order.source_warehouse
stock_entry.to_warehouse = production_order.wip_warehouse
stock_entry.to_warehouse = wip_warehouse
stock_entry.project = production_order.project
else:
stock_entry.from_warehouse = production_order.wip_warehouse
stock_entry.from_warehouse = wip_warehouse
stock_entry.to_warehouse = production_order.fg_warehouse
additional_costs = get_additional_costs(production_order, fg_qty=stock_entry.fg_completed_qty)
stock_entry.project = production_order.project
@ -601,3 +599,18 @@ def make_new_timesheet(source_name, target_doc=None):
frappe.throw(_("Already completed"))
return ts
@frappe.whitelist()
def stop_unstop(production_order, status):
""" Called from client side on Stop/Unstop event"""
if not frappe.has_permission("Production Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
pro_order = frappe.get_doc("Production Order", production_order)
pro_order.update_status(status)
pro_order.update_planned_qty()
frappe.msgprint(_("Production Order has been {0}").format(status))
pro_order.notify_update()
return pro_order.status

View File

@ -8,7 +8,7 @@ import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_days, cint
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.production_order.production_order \
import make_stock_entry, ItemHasVariantError
import make_stock_entry, ItemHasVariantError, stop_unstop
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.item.test_item import get_total_projected_qty
from erpnext.stock.utils import get_bin
@ -228,10 +228,46 @@ class TestProductionOrder(unittest.TestCase):
cint(bin1_on_start_production.reserved_qty_for_production))
self.assertEqual(cint(bin1_on_end_production.projected_qty),
cint(bin1_on_end_production.projected_qty))
def test_reserved_qty_for_stopped_production(self):
test_stock_entry.make_stock_entry(item_code="_Test Item",
target= self.warehouse, qty=100, basic_rate=100)
test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target= self.warehouse, qty=100, basic_rate=100)
# required_items removed
self.pro_order.reload()
self.assertEqual(len(self.pro_order.required_items), 0)
# 0 0 0
self.test_reserved_qty_for_production_submit()
#2 0 -2
s = frappe.get_doc(make_stock_entry(self.pro_order.name,
"Material Transfer for Manufacture", 1))
s.submit()
#1 -1 0
bin1_on_start_production = get_bin(self.item, self.warehouse)
# reserved_qty_for_producion updated
self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production) + 1,
cint(bin1_on_start_production.reserved_qty_for_production))
# projected qty will now be 2 less (becuase of item movement)
self.assertEqual(cint(self.bin1_at_start.projected_qty),
cint(bin1_on_start_production.projected_qty) + 2)
# STOP
stop_unstop(self.pro_order.name, "Stopped")
bin1_on_stop_production = get_bin(self.item, self.warehouse)
# no change in reserved / projected
self.assertEqual(cint(bin1_on_stop_production.reserved_qty_for_production),
cint(self.bin1_at_start.reserved_qty_for_production))
self.assertEqual(cint(bin1_on_stop_production.projected_qty) + 1,
cint(self.bin1_at_start.projected_qty))
def test_scrap_material_qty(self):
prod_order = make_prod_order_test_record(planned_start_date=now(), qty=2)
@ -286,10 +322,11 @@ def make_prod_order_test_record(**args):
pro_order.company = args.company or "_Test Company"
pro_order.stock_uom = args.stock_uom or "_Test UOM"
pro_order.use_multi_level_bom=0
pro_order.set_production_order_operations()
pro_order.get_items_and_operations_from_bom()
if args.source_warehouse:
pro_order.source_warehouse = args.source_warehouse
for item in pro_order.get("required_items"):
item.source_warehouse = args.source_warehouse
if args.planned_start_date:
pro_order.planned_start_date = args.planned_start_date

View File

@ -13,6 +13,7 @@
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -43,6 +44,157 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "source_warehouse",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Source Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"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,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_name",
"fieldtype": "Data",
"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": "Item Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text",
"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": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "qty_section",
"fieldtype": "Section Break",
"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": "Qty",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -72,6 +224,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -99,6 +252,95 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_9",
"fieldtype": "Column Break",
"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,
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "available_qty_at_source_warehouse",
"fieldtype": "Float",
"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": "Available Qty at Source Warehouse",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "available_qty_at_wip_warehouse",
"fieldtype": "Float",
"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": "Available Qty at WIP Warehouse",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
@ -111,7 +353,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-03-28 14:18:36.342161",
"modified": "2017-05-15 17:37:20.212361",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Order Item",

View File

@ -13,6 +13,7 @@
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -42,6 +43,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -74,6 +76,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -85,7 +88,7 @@
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "BOM",
"length": 0,
@ -104,6 +107,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -135,6 +139,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -163,6 +168,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -193,6 +199,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -224,6 +231,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -256,6 +264,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -285,6 +294,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -314,6 +324,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -343,6 +354,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -371,6 +383,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -403,6 +416,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -434,6 +448,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -464,6 +479,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -493,6 +509,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -522,6 +539,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -552,6 +570,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -580,6 +599,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -610,6 +630,7 @@
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -651,7 +672,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-03-27 15:56:29.010336",
"modified": "2017-05-29 18:02:04.252419",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Order Operation",

View File

@ -412,3 +412,4 @@ erpnext.patches.v8_1.setup_gst_india #2017-06-27
execute:frappe.reload_doc('regional', 'doctype', 'gst_hsn_code')
erpnext.patches.v8_1.removed_roles_from_gst_report_non_indian_account
erpnext.patches.v8_1.gst_fixes
erpnext.patches.v8_0.update_production_orders

View File

@ -0,0 +1,44 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
# reload schema
for doctype in ("Production Order", "Production Order Item", "Production Order Operation",
"BOM Item", "BOM Explosion Item", "BOM"):
frappe.reload_doctype(doctype)
# fetch all draft and submitted production orders
fields = ["name"]
if "source_warehouse" in frappe.db.get_table_columns("Production Order"):
fields.append("source_warehouse")
pro_orders = frappe.get_all("Production Order", filters={"docstatus": ["!=", 2]}, fields=fields)
for p in pro_orders:
pro_order = frappe.get_doc("Production Order", p.name)
# set required items table
pro_order.set_required_items()
for item in pro_order.get("required_items"):
# set source warehouse based on parent
if not item.source_warehouse and "source_warehouse" in fields:
item.source_warehouse = pro_order.get("source_warehouse")
item.db_update()
if pro_order.docstatus == 1:
# update transferred qty based on Stock Entry, it also updates db
pro_order.update_transaferred_qty_for_required_items()
# Set status where it was 'Unstopped', as it is deprecated
if pro_order.status == "Unstopped":
status = pro_order.get_status()
pro_order.db_set("status", status)
elif pro_order.status == "Stopped":
pro_order.update_reserved_qty_for_production()

View File

@ -3,7 +3,6 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt, nowdate
import frappe.defaults
from frappe.model.document import Document
@ -15,7 +14,6 @@ class Bin(Document):
self.validate_mandatory()
self.set_projected_qty()
self.block_transactions_against_group_warehouse()
def on_update(self):
update_item_projected_qty(self.item_code)
@ -26,10 +24,6 @@ class Bin(Document):
if (not getattr(self, f, None)) or (not self.get(f)):
self.set(f, 0.0)
def block_transactions_against_group_warehouse(self):
from erpnext.stock.utils import is_group_warehouse
is_group_warehouse(self.warehouse)
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
@ -91,7 +85,8 @@ class Bin(Document):
item.item_code = %s
and item.parent = pro.name
and pro.docstatus = 1
and pro.source_warehouse = %s''', (self.item_code, self.warehouse))[0][0]
and item.source_warehouse = %s
and pro.status not in ("Stopped", "Completed")''', (self.item_code, self.warehouse))[0][0]
self.set_projected_qty()

View File

@ -708,13 +708,11 @@ class StockEntry(StockController):
issue (item quantity) that is pending to issue or desire to transfer,
whichever is less
"""
item_dict = self.get_bom_raw_materials(1)
issued_item_qty = self.get_issued_qty()
item_dict = self.get_pro_order_required_items()
max_qty = flt(self.pro_doc.qty)
for item in item_dict:
pending_to_issue = (max_qty * item_dict[item]["qty"]) - issued_item_qty.get(item, 0)
desire_to_transfer = flt(self.fg_completed_qty) * item_dict[item]["qty"]
for item, item_details in item_dict.items():
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty
if desire_to_transfer <= pending_to_issue:
item_dict[item]["qty"] = desire_to_transfer
@ -734,34 +732,43 @@ class StockEntry(StockController):
return item_dict
def get_issued_qty(self):
issued_item_qty = {}
result = frappe.db.sql("""select t1.item_code, sum(t1.qty)
from `tabStock Entry Detail` t1, `tabStock Entry` t2
where t1.parent = t2.name and t2.production_order = %s and t2.docstatus = 1
and t2.purpose = 'Material Transfer for Manufacture'
group by t1.item_code""", self.production_order)
for t in result:
issued_item_qty[t[0]] = flt(t[1])
return issued_item_qty
def get_pro_order_required_items(self):
item_dict = frappe._dict()
pro_order = frappe.get_doc("Production Order", self.production_order)
if not frappe.db.get_value("Warehouse", pro_order.wip_warehouse, "is_group"):
wip_warehouse = pro_order.wip_warehouse
else:
wip_warehouse = None
for d in pro_order.get("required_items"):
if flt(d.required_qty) > flt(d.transferred_qty):
item_row = d.as_dict()
if d.source_warehouse and not frappe.db.get_value("Warehouse", d.source_warehouse, "is_group"):
item_row["from_warehouse"] = d.source_warehouse
item_row["to_warehouse"] = wip_warehouse
item_dict.setdefault(d.item_code, item_row)
return item_dict
def add_to_stock_entry_detail(self, item_dict, bom_no=None):
expense_account, cost_center = frappe.db.get_values("Company", self.company, \
["default_expense_account", "cost_center"])[0]
for d in item_dict:
stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
se_child = self.append('items')
se_child.s_warehouse = item_dict[d].get("from_warehouse")
se_child.t_warehouse = item_dict[d].get("to_warehouse")
se_child.item_code = cstr(d)
se_child.item_name = item_dict[d]["item_name"]
se_child.description = item_dict[d]["description"]
se_child.uom = item_dict[d]["stock_uom"]
se_child.stock_uom = item_dict[d]["stock_uom"]
se_child.uom = stock_uom
se_child.stock_uom = stock_uom
se_child.qty = flt(item_dict[d]["qty"])
se_child.expense_account = item_dict[d]["expense_account"] or expense_account
se_child.cost_center = item_dict[d]["cost_center"] or cost_center
se_child.expense_account = item_dict[d].get("expense_account") or expense_account
se_child.cost_center = item_dict[d].get("cost_center") or cost_center
if se_child.s_warehouse==None:
se_child.s_warehouse = self.from_warehouse

View File

@ -132,7 +132,7 @@ def get_ordered_qty(item_code, warehouse):
def get_planned_qty(item_code, warehouse):
planned_qty = frappe.db.sql("""
select sum(qty - produced_qty) from `tabProduction Order`
where production_item = %s and fg_warehouse = %s and status != "Stopped"
where production_item = %s and fg_warehouse = %s and status not in ("Stopped", "Completed")
and docstatus=1 and qty > produced_qty""", (item_code, warehouse))
return flt(planned_qty[0][0]) if planned_qty else 0

View File

@ -41,7 +41,8 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
sle_map = {}
for sle in stock_ledger_entries:
sle_map[sle.item_code] = sle_map.get(sle.item_code, 0.0) + flt(sle.stock_value)
if not sle_map.has_key((sle.item_code, sle.warehouse)):
sle_map[(sle.item_code, sle.warehouse)] = flt(sle.stock_value)
return sum(sle_map.values())
@ -67,6 +68,28 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
else:
return last_entry.qty_after_transaction if last_entry else 0.0
@frappe.whitelist()
def get_latest_stock_qty(item_code, warehouse=None):
values, condition = [item_code], ""
if warehouse:
lft, rgt, is_group = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt", "is_group"])
if is_group:
values.extend([lft, rgt])
condition += "and exists (\
select name from `tabWarehouse` wh where wh.name = tabBin.warehouse\
and wh.lft >= %s and wh.rgt <= %s)"
else:
values.append(warehouse)
condition += " AND warehouse = %s"
actual_qty = frappe.db.sql("""select sum(actual_qty) from tabBin
where item_code=%s {0}""".format(condition), values)[0][0]
return actual_qty
def get_latest_stock_balance():
bin_map = {}
for d in frappe.db.sql("""SELECT item_code, warehouse, stock_value as stock_value