From 5999944a5c60d52454776ec22d3c44c7438fede3 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 10 Dec 2012 18:11:42 +0530 Subject: [PATCH] bom cleanup: update cost and flat bom by traversing full tree --- production/doctype/bom/bom.js | 48 ++-- production/doctype/bom/bom.py | 192 ++++++++------ production/doctype/bom/bom.txt | 25 +- production/doctype/bom/test_bom.py | 147 +++++++++++ production/doctype/bom_control/__init__.py | 1 - production/doctype/bom_control/bom_control.py | 114 --------- .../doctype/bom_control/bom_control.txt | 31 --- .../bom_explosion_item/bom_explosion_item.txt | 241 ++++++++---------- .../doctype/production_control/__init__.py | 1 - .../production_control/production_control.py | 138 ---------- .../production_control/production_control.txt | 31 --- .../production_order/production_order.js | 13 +- .../production_order/production_order.py | 29 ++- .../production_planning_tool.js | 7 +- .../production_planning_tool.py | 42 ++- .../page/production_home/production_home.html | 10 + public/js/utils.js | 20 ++ stock/doctype/stock_entry/stock_entry.py | 4 +- tests/data/item/android_jack_d.txt | 3 + tests/data/item/android_jack_s.txt | 3 + tests/data/item/home_desktop_100.txt | 1 + 21 files changed, 514 insertions(+), 587 deletions(-) create mode 100644 production/doctype/bom/test_bom.py delete mode 100644 production/doctype/bom_control/__init__.py delete mode 100644 production/doctype/bom_control/bom_control.py delete mode 100644 production/doctype/bom_control/bom_control.txt delete mode 100644 production/doctype/production_control/__init__.py delete mode 100644 production/doctype/production_control/production_control.py delete mode 100644 production/doctype/production_control/production_control.txt diff --git a/production/doctype/bom/bom.js b/production/doctype/bom/bom.js index 9dffd6cb32f..9d0175f552b 100644 --- a/production/doctype/bom/bom.js +++ b/production/doctype/bom/bom.js @@ -17,11 +17,11 @@ // On REFRESH cur_frm.cscript.refresh = function(doc,dt,dn){ cur_frm.toggle_enable("item", doc.__islocal); + if (!doc.__islocal && doc.docstatus==0) { + cur_frm.set_intro("Submit the BOM to use it in production"); + } } - -// Triggers -//-------------------------------------------------------------------------------------------------- cur_frm.cscript.item = function(doc, dt, dn) { if (doc.item) { get_server_fields('get_item_detail',doc.item,'',doc,dt,dn,1); @@ -36,7 +36,8 @@ cur_frm.cscript.workstation = function(doc,dt,dn) { calculate_op_cost(doc, dt, dn); calculate_total(doc); } - get_server_fields('get_workstation_details',d.workstation,'bom_operations',doc,dt,dn,1, callback); + get_server_fields('get_workstation_details', d.workstation, + 'bom_operations', doc, dt, dn, 1, callback); } } @@ -49,12 +50,10 @@ cur_frm.cscript.hour_rate = function(doc, dt, dn) { cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate; - cur_frm.cscript.item_code = function(doc, cdt, cdn) { get_bom_material_detail(doc, cdt, cdn); } - cur_frm.cscript.bom_no = function(doc, cdt, cdn) { get_bom_material_detail(doc, cdt, cdn); } @@ -89,9 +88,16 @@ cur_frm.cscript.qty = function(doc, cdt, cdn) { calculate_total(doc); } - -cur_frm.cscript.rate = cur_frm.cscript.qty; - +cur_frm.cscript.rate = function(doc, cdt, cdn) { + var d = locals[cdt][cdn]; + if (d.bom_no) { + msgprint("You can not change rate if BOM mentioned agianst any item"); + get_bom_material_detail(doc, cdt, cdn); + } else { + calculate_rm_cost(doc, cdt, cdn); + calculate_total(doc); + } +} cur_frm.cscript.is_default = function(doc, cdt, cdn) { if (doc.docstatus == 1) @@ -104,13 +110,11 @@ cur_frm.cscript.is_active = function(doc, dt, dn) { $c_obj(make_doclist(dt, dn), 'manage_active_bom', '', ''); } - -// Calculate Operating Cost var calculate_op_cost = function(doc, dt, dn) { var op = getchildren('BOM Operation', doc.name, 'bom_operations'); total_op_cost = 0; for(var i=0;i NOW()) AND `tabItem`.`%(key)s` like "%s" ORDER BY `tabItem`.`name` LIMIT 50'; + return 'SELECT DISTINCT `tabItem`.`name`, `tabItem`.description FROM `tabItem` \ + WHERE is_manufactured_item = "Yes" and (IFNULL(`tabItem`.`end_of_life`,"") = "" OR \ + `tabItem`.`end_of_life` = "0000-00-00" OR `tabItem`.`end_of_life` > NOW()) AND \ + `tabItem`.`%(key)s` like "%s" ORDER BY `tabItem`.`name` LIMIT 50'; } cur_frm.fields_dict['project_name'].get_query = function(doc, dt, dn) { @@ -152,12 +158,18 @@ cur_frm.fields_dict['project_name'].get_query = function(doc, dt, dn) { } cur_frm.fields_dict['bom_materials'].grid.get_field('item_code').get_query = function(doc) { - return 'SELECT DISTINCT `tabItem`.`name`, `tabItem`.description FROM `tabItem` WHERE (IFNULL(`tabItem`.`end_of_life`,"") = "" OR `tabItem`.`end_of_life` = "0000-00-00" OR `tabItem`.`end_of_life` > NOW()) AND `tabItem`.`%(key)s` like "%s" ORDER BY `tabItem`.`name` LIMIT 50'; + return 'SELECT DISTINCT `tabItem`.`name`, `tabItem`.description FROM `tabItem` \ + WHERE (IFNULL(`tabItem`.`end_of_life`,"") = "" OR `tabItem`.`end_of_life` = "0000-00-00" \ + OR `tabItem`.`end_of_life` > NOW()) AND `tabItem`.`%(key)s` like "%s" \ + ORDER BY `tabItem`.`name` LIMIT 50'; } cur_frm.fields_dict['bom_materials'].grid.get_field('bom_no').get_query = function(doc) { var d = locals[this.doctype][this.docname]; - return 'SELECT DISTINCT `tabBOM`.`name`, `tabBOM`.`remarks` FROM `tabBOM` WHERE `tabBOM`.`item` = "' + d.item_code + '" AND `tabBOM`.`is_active` = "Yes" AND `tabBOM`.docstatus = 1 AND `tabBOM`.`name` like "%s" ORDER BY `tabBOM`.`name` LIMIT 50'; + return 'SELECT DISTINCT `tabBOM`.`name`, `tabBOM`.`remarks` FROM `tabBOM` \ + WHERE `tabBOM`.`item` = "' + d.item_code + '" AND `tabBOM`.`is_active` = "Yes" AND \ + `tabBOM`.docstatus = 1 AND `tabBOM`.`name` like "%s" \ + ORDER BY `tabBOM`.`name` LIMIT 50'; } cur_frm.cscript.validate = function(doc, dt, dn) { diff --git a/production/doctype/bom/bom.py b/production/doctype/bom/bom.py index ac21d213783..857458efc81 100644 --- a/production/doctype/bom/bom.py +++ b/production/doctype/bom/bom.py @@ -64,22 +64,20 @@ class DocType: def get_workstation_details(self,workstation): """ Fetch hour rate from workstation master""" - ws = sql("select hour_rate from `tabWorkstation` where name = %s",workstation , as_dict = 1) - ret = { - 'hour_rate' : ws and flt(ws[0]['hour_rate']) or '', - } - return ret - + ws = sql("select hour_rate from `tabWorkstation` where name = %s", + workstation , as_dict = 1) + return {'hour_rate' : ws and flt(ws[0]['hour_rate']) or ''} def validate_rm_item(self, item): """ Validate raw material items""" if item[0]['name'] == self.doc.item: - msgprint(" Item_code: "+item[0]['name']+" in materials tab cannot be same as FG Item in BOM := " +cstr(self.doc.name), raise_exception=1) + msgprint("Item_code: %s in materials tab cannot be same as FG Item", + item[0]['name'], raise_exception=1) if item and item[0]['is_asset_item'] == 'Yes': - msgprint("Sorry!!! Item " + item[0]['name'] + " is an Asset of the company. Entered in BOM => " + cstr(self.doc.name), raise_exception = 1) + msgprint("Item: %s is an asset item, please check", item[0]['name'], raise_exception=1) if not item or item[0]['docstatus'] == 2: msgprint("Item %s does not exist in system" % item[0]['item_code'], raise_exception = 1) @@ -113,9 +111,7 @@ class DocType: """ Get raw material rate as per selected method, if bom exists takes bom cost """ if arg['bom_no']: - bom = sql("""select name, total_cost/quantity as unit_cost from `tabBOM` - where is_active = 'Yes' and name = %s""", arg['bom_no'], as_dict=1) - rate = bom and bom[0]['unit_cost'] or 0 + rate = self.get_bom_unitcost(arg['bom_no']) elif arg and (arg['is_purchase_item'] == 'Yes' or arg['is_sub_contracted_item'] == 'Yes'): if self.doc.rm_cost_as_per == 'Valuation Rate': rate = self.get_valuation_rate(arg) @@ -126,7 +122,10 @@ class DocType: return rate - + def get_bom_unitcost(self, bom_no): + bom = sql("""select name, total_cost/quantity as unit_cost from `tabBOM` + where is_active = 'Yes' and name = %s""", bom_no, as_dict=1) + return bom and bom[0]['unit_cost'] or 0 def get_valuation_rate(self, arg): """ Get average valuation rate of relevant warehouses @@ -139,7 +138,8 @@ class DocType: warehouse = sql("select warehouse from `tabBin` where item_code = %s", arg['item_code']) rate = [] for wh in warehouse: - r = get_obj('Valuation Control').get_incoming_rate(dt, time, arg['item_code'], wh[0], qty = arg.get('qty', 0)) + r = get_obj('Valuation Control').get_incoming_rate(dt, time, + arg['item_code'], wh[0], qty=arg.get('qty', 0)) if r: rate.append(r) @@ -148,15 +148,20 @@ class DocType: def manage_default_bom(self): - """ Uncheck others if current one is selected as default, update default bom in item master""" + """ Uncheck others if current one is selected as default, + update default bom in item master + """ if self.doc.is_default and self.doc.is_active == 'Yes': - sql("update `tabBOM` set is_default = 0 where name != %s and item=%s", (self.doc.name, self.doc.item)) + sql("update `tabBOM` set is_default = 0 where name != %s and item=%s", + (self.doc.name, self.doc.item)) # update default bom in Item Master - sql("update `tabItem` set default_bom = %s where name = %s", (self.doc.name, self.doc.item)) + sql("update `tabItem` set default_bom = %s where name = %s", + (self.doc.name, self.doc.item)) else: - sql("update `tabItem` set default_bom = '' where name = %s and default_bom = %s", (self.doc.item, self.doc.name)) + sql("update `tabItem` set default_bom = '' where name = %s and default_bom = %s", + (self.doc.item, self.doc.name)) def manage_active_bom(self): @@ -181,39 +186,33 @@ class DocType: def calculate_cost(self): """Calculate bom totals""" - self.doc.costing_date = nowdate() self.calculate_op_cost() self.calculate_rm_cost() self.doc.total_cost = self.doc.raw_material_cost + self.doc.operating_cost self.doc.modified = now() self.doc.save() - - self.update_flat_bom_engine(is_submit = self.doc.docstatus) - def calculate_op_cost(self): """Update workstation rate and calculates totals""" total_op_cost = 0 for d in getlist(self.doclist, 'bom_operations'): - hour_rate = sql("select hour_rate from `tabWorkstation` where name = %s", cstr(d.workstation)) - d.hour_rate = hour_rate and flt(hour_rate[0][0]) or d.hour_rate or 0 - d.operating_cost = d.hour_rate and d.time_in_mins and \ - flt(d.hour_rate) * flt(d.time_in_mins) / 60 or d.operating_cost + if d.hour_rate and d.time_in_mins: + d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0 d.save() - total_op_cost += d.operating_cost + total_op_cost += flt(d.operating_cost) self.doc.operating_cost = total_op_cost + def calculate_rm_cost(self): """Fetch RM rate as per today's valuation rate and calculate totals""" total_rm_cost = 0 for d in getlist(self.doclist, 'bom_materials'): - arg = {'item_code': d.item_code, 'qty': d.qty, 'bom_no': d.bom_no} - ret = self.get_bom_material_detail(cstr(arg)) - for k in ret: - d.fields[k] = ret[k] + if d.bom_no: + d.rate = self.get_bom_unitcost(d.bom_no) d.amount = flt(d.rate) * flt(d.qty) + d.qty_consumed_per_unit = flt(d.qty) / flt(self.doc.quantity) d.save() total_rm_cost += d.amount self.doc.raw_material_cost = total_rm_cost @@ -224,20 +223,22 @@ class DocType: """ Validate main FG item""" item = self.get_item_det(self.doc.item) if not item: - msgprint("Item %s does not exists in the system or expired." % self.doc.item, raise_exception = 1) + msgprint("Item %s does not exists in the system or expired." % + self.doc.item, raise_exception = 1) - elif item[0]['is_manufactured_item'] != 'Yes' and item[0]['is_sub_contracted_item'] != 'Yes': - msgprint("""As Item: %s is not a manufactured / sub-contracted item, + elif item[0]['is_manufactured_item'] != 'Yes' \ + and item[0]['is_sub_contracted_item'] != 'Yes': + msgprint("""As Item: %s is not a manufactured / sub-contracted item, \ you can not make BOM for it""" % self.doc.item, raise_exception = 1) - def validate_operations(self): """ Check duplicate operation no""" self.op = [] for d in getlist(self.doclist, 'bom_operations'): if cstr(d.operation_no) in self.op: - msgprint("Operation no: %s is repeated in Operations Table"% d.operation_no, raise_exception=1) + msgprint("Operation no: %s is repeated in Operations Table" % + d.operation_no, raise_exception=1) else: # add operation in op list self.op.append(cstr(d.operation_no)) @@ -248,40 +249,47 @@ class DocType: for m in getlist(self.doclist, 'bom_materials'): # check if operation no not in op table if cstr(m.operation_no) not in self.op: - msgprint("""Operation no: %s against item: %s at row no: %s is not present - at Operations table"""% (m.operation_no, m.item_code, m.idx), raise_exception = 1) + msgprint("""Operation no: %s against item: %s at row no: %s \ + is not present at Operations table""" % + (m.operation_no, m.item_code, m.idx), raise_exception = 1) item = self.get_item_det(m.item_code) - if item[0]['is_manufactured_item'] == 'Yes' or item[0]['is_sub_contracted_item'] == 'Yes': + if item[0]['is_manufactured_item'] == 'Yes' or \ + item[0]['is_sub_contracted_item'] == 'Yes': if not m.bom_no: - msgprint("Please enter BOM No aginst item: %s at row no: %s"% (m.item_code, m.idx), raise_exception=1) + msgprint("Please enter BOM No aginst item: %s at row no: %s" % + (m.item_code, m.idx), raise_exception=1) else: self.validate_bom_no(m.item_code, m.bom_no, m.idx) elif m.bom_no: - msgprint("""As Item %s is not a manufactured / sub-contracted item, - you can enter BOM against it (Row No: %s)."""% (m.item_code, m.idx), raise_excepiton = 1) + msgprint("""As Item %s is not a manufactured / sub-contracted item, \ + you can enter BOM against it (Row No: %s).""" % + (m.item_code, m.idx), raise_exception = 1) if flt(m.qty) <= 0: - msgprint("Please enter qty against raw material: %s at row no: %s"% (m.item_code, m.idx), raise_exception = 1) + msgprint("Please enter qty against raw material: %s at row no: %s" % + (m.item_code, m.idx), raise_exception = 1) self.check_if_item_repeated(m.item_code, m.operation_no, check_list) - def validate_bom_no(self, item, bom_no, idx): """Validate BOM No of sub-contracted items""" bom = sql("""select name from `tabBOM` where name = %s and item = %s - and ifnull(is_active, 'No') = 'Yes' and docstatus < 2 """, (bom_no, item), as_dict =1) + and ifnull(is_active, 'No') = 'Yes' and docstatus < 2 """, + (bom_no, item), as_dict =1) if not bom: msgprint("""Incorrect BOM No: %s against item: %s at row no: %s. - It may be inactive or cancelled or for some other item."""% (bom_no, item, idx), raise_exception = 1) + It may be inactive or cancelled or for some other item.""" % + (bom_no, item, idx), raise_exception = 1) def check_if_item_repeated(self, item, op, check_list): if [cstr(item), cstr(op)] in check_list: - msgprint("Item %s has been entered twice against same operation" % item, raise_exception = 1) + msgprint("Item %s has been entered twice against same operation" % + item, raise_exception = 1) else: check_list.append([cstr(item), cstr(op)]) @@ -292,13 +300,14 @@ class DocType: self.validate_materials() def check_recursion(self): - """ Check whether reqursion occurs in any bom""" + """ Check whether recursion occurs in any bom""" check_list = [['parent', 'bom_no', 'parent'], ['bom_no', 'parent', 'child']] for d in check_list: bom_list, count = [self.doc.name], 0 while (len(bom_list) > count ): - boms = sql(" select %s from `tabBOM Item` where %s = '%s' " % (d[0], d[1], cstr(bom_list[count]))) + boms = sql(" select %s from `tabBOM Item` where %s = '%s' " % + (d[0], d[1], cstr(bom_list[count]))) count = count + 1 for b in boms: if b[0] == self.doc.name: @@ -308,30 +317,32 @@ class DocType: bom_list.append(b[0]) - def on_update(self): self.check_recursion() + self.update_cost_by_traversing() + self.update_flat_bom_by_traversing() + - - def add_to_flat_bom_detail(self, is_submit = 0): + def add_to_flat_bom_detail(self): "Add items to Flat BOM table" self.doclist = self.doc.clear_table(self.doclist, 'flat_bom_details', 1) for d in self.cur_flat_bom_items: ch = addchild(self.doc, 'flat_bom_details', 'BOM Explosion Item', 1, self.doclist) for i in d.keys(): ch.fields[i] = d[i] - ch.docstatus = is_submit + ch.docstatus = self.doc.docstatus ch.save(1) self.doc.save() - def get_child_flat_bom_items(self, bom_no, qty): """ Add all items from Flat BOM of child BOM""" - - child_fb_items = sql("""select item_code, description, stock_uom, qty, rate, amount, parent_bom, mat_detail_no, qty_consumed_per_unit - from `tabBOM Explosion Item` where parent = '%s' and docstatus = 1""" % bom_no, as_dict = 1) + + child_fb_items = sql("""select item_code, description, stock_uom, qty, rate, + amount, parent_bom, mat_detail_no, qty_consumed_per_unit + from `tabBOM Explosion Item` where parent = '%s' and docstatus = 1""" % + bom_no, as_dict = 1) for d in child_fb_items: self.cur_flat_bom_items.append({ 'item_code' : d['item_code'], @@ -347,31 +358,30 @@ class DocType: }) - # Get Current Flat BOM Items - # ----------------------------- - def get_current_flat_bom_items(self): + def get_flat_bom_items(self): """ Get all raw materials including items from child bom""" self.cur_flat_bom_items = [] for d in getlist(self.doclist, 'bom_materials'): - self.cur_flat_bom_items.append({ - 'item_code' : d.item_code, - 'description' : d.description, - 'stock_uom' : d.stock_uom, - 'qty' : flt(d.qty), - 'rate' : flt(d.rate), - 'amount' : flt(d.amount), - 'parent_bom' : d.parent, #item and item[0][0]=='No' and d.bom_no or d.parent, - 'mat_detail_no' : d.name, - 'qty_consumed_per_unit' : flt(d.qty_consumed_per_unit) - }) if d.bom_no: self.get_child_flat_bom_items(d.bom_no, d.qty) + else: + self.cur_flat_bom_items.append({ + 'item_code' : d.item_code, + 'description' : d.description, + 'stock_uom' : d.stock_uom, + 'qty' : flt(d.qty), + 'rate' : flt(d.rate), + 'amount' : flt(d.amount), + 'parent_bom' : d.parent, + 'mat_detail_no' : d.name, + 'qty_consumed_per_unit' : flt(d.qty_consumed_per_unit) + }) - def update_flat_bom_engine(self, is_submit = 0): + def update_flat_bom(self): """ Update Flat BOM, following will be correct data""" - self.get_current_flat_bom_items() - self.add_to_flat_bom_detail(is_submit) + self.get_flat_bom_items() + self.add_to_flat_bom_detail() def get_parent_bom_list(self, bom_no): @@ -381,17 +391,43 @@ class DocType: def on_submit(self): self.manage_default_bom() - self.update_flat_bom_engine(1) - def on_cancel(self): # check if used in any other bom par = sql("""select t1.parent from `tabBOM Item` t1, `tabBOM` t2 - where t1.parent = t2.name and t1.bom_no = %s and t1.docstatus = 1 and t2.is_active = 'Yes'""", self.doc.name) + where t1.parent = t2.name and t1.bom_no = %s and t1.docstatus = 1 + and t2.is_active = 'Yes'""", self.doc.name) if par: - msgprint("BOM can not be cancelled, as it is a child item in following active BOM %s"% [d[0] for d in par]) - raise Exception + msgprint("""BOM can not be cancelled, as it is a child item \ + in following active BOM %s""" % [d[0] for d in par], raise_exception=1) webnotes.conn.set(self.doc, "is_active", "No") webnotes.conn.set(self.doc, "is_default", 0) self.manage_default_bom() + self.update_flat_bom_by_traversing() + + def traverse_tree(self): + def _get_childs(bom_no): + return [cstr(d[0]) for d in webnotes.conn.sql("""select bom_no from `tabBOM Item` + where parent = %s and ifnull(bom_no, '') != ''""", bom_no)] + + bom_list, count = [self.doc.name], 0 + while(count < len(bom_list)): + for child_bom in _get_childs(bom_list[count]): + if child_bom not in bom_list: + bom_list.append(child_bom) + count += 1 + + return bom_list + + def update_cost_by_traversing(self): + bom_list = self.traverse_tree() + bom_list.reverse() + for bom in bom_list: + get_obj("BOM", bom, with_children=1).calculate_cost() + + def update_flat_bom_by_traversing(self): + bom_list = self.traverse_tree() + bom_list.reverse() + for bom in bom_list: + get_obj("BOM", bom, with_children=1).update_flat_bom() \ No newline at end of file diff --git a/production/doctype/bom/bom.txt b/production/doctype/bom/bom.txt index 58cb5cb01e1..5c029cd1758 100644 --- a/production/doctype/bom/bom.txt +++ b/production/doctype/bom/bom.txt @@ -4,7 +4,7 @@ "docstatus": 0, "creation": "2012-07-03 13:30:03", "modified_by": "Administrator", - "modified": "2012-12-03 13:29:26" + "modified": "2012-12-10 12:03:14" }, { "istable": 0, @@ -56,12 +56,10 @@ { "description": "Select the item code for which Bill of Material is being created", "oldfieldtype": "Link", - "colour": "White:FFF", "doctype": "DocField", "label": "Item", "oldfieldname": "item", "permlevel": 0, - "trigger": "Client", "fieldname": "item", "fieldtype": "Link", "search_index": 1, @@ -72,7 +70,6 @@ { "description": "Total quantity of items for which raw materials required and operations done will be defined", "oldfieldtype": "Currency", - "colour": "White:FFF", "doctype": "DocField", "label": "Quantity", "oldfieldname": "quantity", @@ -91,7 +88,6 @@ { "no_copy": 1, "oldfieldtype": "Select", - "colour": "White:FFF", "allow_on_submit": 1, "doctype": "DocField", "label": "Is Active", @@ -104,10 +100,9 @@ "options": "\nYes\nNo" }, { - "allow_on_submit": 1, "no_copy": 1, "oldfieldtype": "Check", - "colour": "White:FFF", + "allow_on_submit": 1, "doctype": "DocField", "label": "Is Default", "oldfieldname": "is_default", @@ -126,7 +121,6 @@ { "description": "Specify the operations, operating cost and give a unique Operation no to your operations.", "oldfieldtype": "Table", - "colour": "White:FFF", "doctype": "DocField", "label": "BOM Operations", "oldfieldname": "bom_operations", @@ -154,7 +148,6 @@ { "description": "Enter the raw materials required to manufacture the BOM item. Specify the operation no as entered in the previous tab which will be performed on the raw materials entered.", "oldfieldtype": "Table", - "colour": "White:FFF", "doctype": "DocField", "label": "BOM Item", "oldfieldname": "bom_materials", @@ -185,6 +178,12 @@ "fieldtype": "Float", "permlevel": 1 }, + { + "doctype": "DocField", + "fieldname": "col_break24", + "fieldtype": "Column Break", + "permlevel": 0 + }, { "doctype": "DocField", "label": "Total Cost", @@ -202,13 +201,12 @@ { "description": "Select name of the project if BOM need to be created against any project", "oldfieldtype": "Link", + "doctype": "DocField", "label": "Project Name", "oldfieldname": "project_name", - "trigger": "Client", + "options": "Project", "fieldname": "project_name", "fieldtype": "Link", - "doctype": "DocField", - "options": "Project", "permlevel": 0, "in_filter": 1 }, @@ -225,7 +223,6 @@ "doctype": "DocField", "label": "Item Description", "oldfieldname": "description", - "width": "300px", "fieldname": "description", "fieldtype": "Small Text", "permlevel": 0 @@ -269,7 +266,6 @@ { "no_copy": 1, "oldfieldtype": "Text", - "colour": "White:FFF", "doctype": "DocField", "label": "Remarks", "oldfieldname": "remarks", @@ -292,7 +288,6 @@ "permlevel": 1, "no_copy": 1, "oldfieldtype": "Table", - "colour": "White:FFF", "doctype": "DocField", "label": "BOM Explosion Item", "oldfieldname": "flat_bom_details", diff --git a/production/doctype/bom/test_bom.py b/production/doctype/bom/test_bom.py new file mode 100644 index 00000000000..68d9ce85f43 --- /dev/null +++ b/production/doctype/bom/test_bom.py @@ -0,0 +1,147 @@ +# ERPNext - web based ERP (http://erpnext.com) +# Copyright (C) 2012 Web Notes Technologies Pvt Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from __future__ import unicode_literals +import unittest +import webnotes +import webnotes.model +from webnotes.utils import nowdate, flt +from accounts.utils import get_fiscal_year +from webnotes.model.doclist import DocList +import copy + +company = webnotes.conn.get_default("company") + + +def load_data(): + + # create default warehouse + if not webnotes.conn.exists("Warehouse", "Default Warehouse"): + webnotes.insert({"doctype": "Warehouse", + "warehouse_name": "Default Warehouse", + "warehouse_type": "Stores"}) + + # create UOM: Nos. + if not webnotes.conn.exists("UOM", "Nos"): + webnotes.insert({"doctype": "UOM", "uom_name": "Nos"}) + + from webnotes.tests import insert_test_data + # create item groups and items + insert_test_data("Item Group", + sort_fn=lambda ig: (ig[0].get('parent_item_group'), ig[0].get('name'))) + insert_test_data("Item") + +base_bom_fg = [ + {"doctype": "BOM", "item": "Android Jack D", "quantity": 1, + "is_active": "Yes", "is_default": 1, "uom": "Nos"}, + {"doctype": "BOM Operation", "operation_no": 1, "parentfield": "bom_operations", + "opn_description": "Development", "hour_rate": 10, "time_in_mins": 90}, + {"doctype": "BOM Item", "item_code": "Home Desktop 300", "operation_no": 1, + "qty": 2, "rate": 20, "stock_uom": "Nos", "parentfield": "bom_materials"}, + {"doctype": "BOM Item", "item_code": "Home Desktop 100", "operation_no": 1, + "qty": 1, "rate": 300, "stock_uom": "Nos", "parentfield": "bom_materials"}, + {"doctype": "BOM Item", "item_code": "Nebula 7", "operation_no": 1, + "qty": 5, "stock_uom": "Nos", "parentfield": "bom_materials"}, +] + +base_bom_child = [ + {"doctype": "BOM", "item": "Nebula 7", "quantity": 5, + "is_active": "Yes", "is_default": 1, "uom": "Nos"}, + {"doctype": "BOM Operation", "operation_no": 1, "parentfield": "bom_operations", + "opn_description": "Development"}, + {"doctype": "BOM Item", "item_code": "Android Jack S", "operation_no": 1, + "qty": 10, "stock_uom": "Nos", "parentfield": "bom_materials"} +] + +base_bom_grandchild = [ + {"doctype": "BOM", "item": "Android Jack S", "quantity": 1, + "is_active": "Yes", "is_default": 1, "uom": "Nos"}, + {"doctype": "BOM Operation", "operation_no": 1, "parentfield": "bom_operations", + "opn_description": "Development"}, + {"doctype": "BOM Item", "item_code": "Home Desktop 300", "operation_no": 1, + "qty": 3, "rate": 10, "stock_uom": "Nos", "parentfield": "bom_materials"} +] + + +class TestPurchaseReceipt(unittest.TestCase): + def setUp(self): + webnotes.conn.begin() + load_data() + + def test_bom_validation(self): + # show throw error bacause bom no missing for sub-assembly item + bom_fg = copy.deepcopy(base_bom_fg) + self.assertRaises(webnotes.ValidationError, webnotes.insert, DocList(bom_fg)) + + # main item is not a manufacturing item + bom_fg = copy.deepcopy(base_bom_fg) + bom_fg[0]["item"] = "Home Desktop 200" + bom_fg.pop(4) + self.assertRaises(webnotes.ValidationError, webnotes.insert, DocList(bom_fg)) + + # operation no mentioed in material table not matching with operation table + bom_fg = copy.deepcopy(base_bom_fg) + bom_fg.pop(4) + bom_fg[2]["operation_no"] = 2 + self.assertRaises(webnotes.ValidationError, webnotes.insert, DocList(bom_fg)) + + + def test_bom(self): + gc_wrapper = webnotes.insert(DocList(base_bom_grandchild)) + gc_wrapper.submit() + + bom_child = copy.deepcopy(base_bom_child) + bom_child[2]["bom_no"] = gc_wrapper.doc.name + child_wrapper = webnotes.insert(DocList(bom_child)) + child_wrapper.submit() + + bom_fg = copy.deepcopy(base_bom_fg) + bom_fg[4]["bom_no"] = child_wrapper.doc.name + fg_wrapper = webnotes.insert(DocList(bom_fg)) + fg_wrapper.load_from_db() + + self.check_bom_cost(fg_wrapper) + + self.check_flat_bom(fg_wrapper, child_wrapper, gc_wrapper) + + def check_bom_cost(self, fg_wrapper): + expected_values = { + "operating_cost": 15, + "raw_material_cost": 640, + "total_cost": 655 + } + + for key in expected_values: + self.assertEqual(flt(expected_values[key]), flt(fg_wrapper.doc.fields.get(key))) + + def check_flat_bom(self, fg_wrapper, child_wrapper, gc_wrapper): + expected_flat_bom_items = { + ("Home Desktop 300", fg_wrapper.doc.name): (2, 20), + ("Home Desktop 100", fg_wrapper.doc.name): (1, 300), + ("Home Desktop 300", gc_wrapper.doc.name): (30, 10) + } + + self.assertEqual(len(fg_wrapper.doclist.get({"parentfield": "flat_bom_details"})), 3) + + for key, val in expected_flat_bom_items.items(): + flat_bom = fg_wrapper.doclist.get({"parentfield": "flat_bom_details", + "item_code": key[0], "parent_bom": key[1]})[0] + self.assertEqual(val, (flat_bom.qty, flat_bom.rate)) + + + def tearDown(self): + webnotes.conn.rollback() \ No newline at end of file diff --git a/production/doctype/bom_control/__init__.py b/production/doctype/bom_control/__init__.py deleted file mode 100644 index baffc488252..00000000000 --- a/production/doctype/bom_control/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from __future__ import unicode_literals diff --git a/production/doctype/bom_control/bom_control.py b/production/doctype/bom_control/bom_control.py deleted file mode 100644 index 27812a8c2dc..00000000000 --- a/production/doctype/bom_control/bom_control.py +++ /dev/null @@ -1,114 +0,0 @@ -# ERPNext - web based ERP (http://erpnext.com) -# Copyright (C) 2012 Web Notes Technologies Pvt Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from __future__ import unicode_literals -import webnotes - -from webnotes.utils import cint, flt -from webnotes.model import db_exists -from webnotes.model.wrapper import copy_doclist -from webnotes.model.code import get_obj - -sql = webnotes.conn.sql - - - -class DocType: - def __init__(self, doc, doclist): - self.doc = doc - self.doclist = doclist - - - - def get_item_group(self): - ret = sql("select name from `tabItem Group` ") - item_group = [] - for r in ret: - item =sql("select t1.name from `tabItem` t1, `tabBOM` t2 where t2.item = t1.name and t1.item_group = '%s' " % (r[0])) - if item and item[0][0]: - item_group.append(r[0]) - return '~~~'.join([r for r in item_group]) - - - - def get_item_code(self,item_group): - """ here BOM docstatus = 1 and is_active ='yes' condition is not given because some bom - is under construction that is it is still in saved mode and they want see till where they have reach. - """ - ret = sql("select distinct t1.name from `tabItem` t1, `tabBOM` t2 where t2.item = t1.name and t1.item_group = '%s' " % (item_group)) - return '~~~'.join([r[0] for r in ret]) - - - - def get_bom_no(self,item_code): - ret = sql("select name from `tabBOM` where item = '%s' " % (item_code)) - return '~~~'.join([r[0] for r in ret]) - - - - def get_operations(self,bom_no): - ret = sql("select operation_no,opn_description,workstation,hour_rate,time_in_mins from `tabBOM Operation` where parent = %s", bom_no, as_dict = 1) - cost = sql("select dir_mat_as_per_mar , operating_cost , cost_as_per_mar from `tabBOM` where name = %s", bom_no, as_dict = 1) - - # Validate the BOM ENTRIES - reply = [] - - if ret: - for r in ret: - reply.append(['operation',cint(r['operation_no']), r['opn_description'] or '','%s'% bom_no,r['workstation'],flt(r['hour_rate']),flt(r['time_in_mins']),0,0,0]) - - reply[0][7]= flt(cost[0]['dir_mat_as_per_mar']) - reply[0][8]=flt(cost[0]['operating_cost']) - reply[0][9]=flt(cost[0]['cost_as_per_mar']) - return reply - - - - def get_item_bom(self,data): - data = eval(data) - reply = [] - ret = sql("select item_code,description,bom_no,qty,scrap,stock_uom,value_as_per_mar,moving_avg_rate from `tabBOM Item` where parent = '%s' and operation_no = '%s'" % (data['bom_no'],data['op_no']), as_dict =1 ) - - for r in ret: - item = sql("select is_manufactured_item, is_sub_contracted_item from `tabItem` where name = '%s'" % r['item_code'], as_dict=1) - if not item[0]['is_manufactured_item'] == 'Yes' and not item[0]['is_sub_contracted_item'] =='Yes': - #if item is not manufactured or it is not sub-contracted - reply.append([ 'item_bom', r['item_code'] or '', r['description'] or '', r['bom_no'] or '', flt(r['qty']) or 0, r['stock_uom'] or '', flt(r['scrap']) or 0, flt(r['moving_avg_rate']) or 0, 1]) - else: - # if it is manufactured or sub_contracted this will be considered(here item can be purchase item) - reply.append([ 'item_bom', r['item_code'] or '', r['description'] or '', r['bom_no'] or '', flt(r['qty']) or 0, r['stock_uom'] or '', flt(r['scrap']) or 0, flt(r['value_as_per_mar']) or 0, 0]) - return reply - - - - #------------- Wrapper Code -------------- - def calculate_cost(self, bom_no): - main_bom_list = get_obj('Production Control').traverse_bom_tree( bom_no = bom_no, qty = 1, calculate_cost = 1) - main_bom_list.reverse() - for bom in main_bom_list: - bom_obj = get_obj('BOM', bom, with_children = 1) - bom_obj.calculate_cost() - return 'calculated' - - - - def get_bom_tree_list(self,args): - arg = eval(args) - i =[] - for a in sql("select t1.name from `tabBOM` t1, `tabItem` t2 where t2.item_group like '%s' and t1.item like '%s'"%(arg['item_group'] +'%',arg['item_code'] + '%')): - if a[0] not in i: - i.append(a[0]) - return i diff --git a/production/doctype/bom_control/bom_control.txt b/production/doctype/bom_control/bom_control.txt deleted file mode 100644 index 3c322e1ca9d..00000000000 --- a/production/doctype/bom_control/bom_control.txt +++ /dev/null @@ -1,31 +0,0 @@ -# DocType, BOM Control -[ - - # These values are common in all dictionaries - { - 'creation': '2012-03-27 14:36:02', - 'docstatus': 0, - 'modified': '2012-03-27 14:36:02', - 'modified_by': u'Administrator', - 'owner': u'Administrator' - }, - - # These values are common for all DocType - { - 'colour': u'White:FFF', - 'doctype': 'DocType', - 'issingle': 1, - 'module': u'Production', - 'name': '__common__', - 'section_style': u'Simple', - 'server_code_error': u' ', - 'show_in_menu': 0, - 'version': 108 - }, - - # DocType, BOM Control - { - 'doctype': 'DocType', - 'name': u'BOM Control' - } -] \ No newline at end of file diff --git a/production/doctype/bom_explosion_item/bom_explosion_item.txt b/production/doctype/bom_explosion_item/bom_explosion_item.txt index 1ba688f77ef..d1cac5a3562 100644 --- a/production/doctype/bom_explosion_item/bom_explosion_item.txt +++ b/production/doctype/bom_explosion_item/bom_explosion_item.txt @@ -1,138 +1,107 @@ -# DocType, BOM Explosion Item [ - - # These values are common in all dictionaries - { - 'creation': '2012-03-27 14:36:03', - 'docstatus': 0, - 'modified': '2012-03-27 14:36:03', - 'modified_by': u'Administrator', - 'owner': u'jai@webnotestech.com' - }, - - # These values are common for all DocType - { - 'autoname': u'FBD/.######', - 'colour': u'White:FFF', - 'default_print_format': u'Standard', - 'doctype': 'DocType', - 'istable': 1, - 'module': u'Production', - 'name': '__common__', - 'read_only': 0, - 'section_style': u'Simple', - 'server_code_error': u' ', - 'show_in_menu': 0, - 'version': 24 - }, - - # These values are common for all DocField - { - 'doctype': u'DocField', - 'name': '__common__', - 'parent': u'BOM Explosion Item', - 'parentfield': u'fields', - 'parenttype': u'DocType', - 'permlevel': 0 - }, - - # DocType, BOM Explosion Item - { - 'doctype': 'DocType', - 'name': u'BOM Explosion Item' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'item_code', - 'fieldtype': u'Link', - 'label': u'Item Code', - 'oldfieldname': u'item_code', - 'oldfieldtype': u'Link', - 'options': u'Item' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'description', - 'fieldtype': u'Text', - 'label': u'Description', - 'oldfieldname': u'description', - 'oldfieldtype': u'Text', - 'width': u'300px' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'qty', - 'fieldtype': u'Float', - 'label': u'Qty', - 'oldfieldname': u'qty', - 'oldfieldtype': u'Currency' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'rate', - 'fieldtype': u'Float', - 'label': u'Rate', - 'oldfieldname': u'standard_rate', - 'oldfieldtype': u'Currency' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'amount', - 'fieldtype': u'Float', - 'label': u'Amount', - 'oldfieldname': u'amount_as_per_sr', - 'oldfieldtype': u'Currency' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'stock_uom', - 'fieldtype': u'Link', - 'label': u'Stock UOM', - 'oldfieldname': u'stock_uom', - 'oldfieldtype': u'Link', - 'options': u'UOM' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'parent_bom', - 'fieldtype': u'Link', - 'hidden': 0, - 'label': u'Parent BOM', - 'oldfieldname': u'parent_bom', - 'oldfieldtype': u'Link', - 'width': u'250px' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'mat_detail_no', - 'fieldtype': u'Data', - 'hidden': 1, - 'label': u'Mat Detail No' - }, - - # DocField - { - 'doctype': u'DocField', - 'fieldname': u'qty_consumed_per_unit', - 'fieldtype': u'Float', - 'hidden': 0, - 'label': u'Qty Consumed Per Unit', - 'no_copy': 0 - } + { + "owner": "jai@webnotestech.com", + "docstatus": 0, + "creation": "2012-07-03 13:30:04", + "modified_by": "Administrator", + "modified": "2012-12-10 12:05:27" + }, + { + "read_only": 0, + "istable": 1, + "autoname": "FBD/.######", + "name": "__common__", + "default_print_format": "Standard", + "doctype": "DocType", + "module": "Production" + }, + { + "name": "__common__", + "parent": "BOM Explosion Item", + "doctype": "DocField", + "parenttype": "DocType", + "permlevel": 1, + "parentfield": "fields" + }, + { + "name": "BOM Explosion Item", + "doctype": "DocType" + }, + { + "oldfieldtype": "Link", + "doctype": "DocField", + "label": "Item Code", + "oldfieldname": "item_code", + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item" + }, + { + "oldfieldtype": "Text", + "doctype": "DocField", + "label": "Description", + "oldfieldname": "description", + "width": "300px", + "fieldname": "description", + "fieldtype": "Text" + }, + { + "oldfieldtype": "Currency", + "doctype": "DocField", + "label": "Qty", + "oldfieldname": "qty", + "fieldname": "qty", + "fieldtype": "Float" + }, + { + "oldfieldtype": "Currency", + "doctype": "DocField", + "label": "Rate", + "oldfieldname": "standard_rate", + "fieldname": "rate", + "fieldtype": "Float" + }, + { + "oldfieldtype": "Currency", + "doctype": "DocField", + "label": "Amount", + "oldfieldname": "amount_as_per_sr", + "fieldname": "amount", + "fieldtype": "Float" + }, + { + "oldfieldtype": "Link", + "doctype": "DocField", + "label": "Stock UOM", + "oldfieldname": "stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM" + }, + { + "oldfieldtype": "Link", + "doctype": "DocField", + "label": "Parent BOM", + "oldfieldname": "parent_bom", + "width": "250px", + "fieldname": "parent_bom", + "fieldtype": "Link", + "hidden": 0, + "options": "BOM" + }, + { + "doctype": "DocField", + "label": "Mat Detail No", + "fieldname": "mat_detail_no", + "fieldtype": "Data", + "hidden": 1 + }, + { + "no_copy": 0, + "doctype": "DocField", + "label": "Qty Consumed Per Unit", + "fieldname": "qty_consumed_per_unit", + "fieldtype": "Float", + "hidden": 0 + } ] \ No newline at end of file diff --git a/production/doctype/production_control/__init__.py b/production/doctype/production_control/__init__.py deleted file mode 100644 index baffc488252..00000000000 --- a/production/doctype/production_control/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from __future__ import unicode_literals diff --git a/production/doctype/production_control/production_control.py b/production/doctype/production_control/production_control.py deleted file mode 100644 index b389e694137..00000000000 --- a/production/doctype/production_control/production_control.py +++ /dev/null @@ -1,138 +0,0 @@ -# ERPNext - web based ERP (http://erpnext.com) -# Copyright (C) 2012 Web Notes Technologies Pvt Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from __future__ import unicode_literals -import webnotes - -from webnotes.utils import cstr, flt, get_defaults, now, nowdate -from webnotes.model import db_exists -from webnotes.model.doc import Document -from webnotes.model.wrapper import copy_doclist -from webnotes.model.code import get_obj -from webnotes import msgprint - -sql = webnotes.conn.sql - - - -class DocType: - def __init__( self, doc, doclist=[]): - self.doc = doc - self.doclist = doclist - self.pur_items = {} - self.bom_list = [] - self.sub_assembly_items = [] - self.item_master = {} - - def traverse_bom_tree( self, bom_no, qty, ext_pur_items = 0, ext_sub_assembly_items = 0, calculate_cost = 0, maintain_item_master = 0 ): - count, bom_list, qty_list = 0, [bom_no], [qty] - while (count < len(bom_list)): - # get child items from BOM MAterial Table. - child_items = sql("select item_code, bom_no, qty, qty_consumed_per_unit from `tabBOM Item` where parent = %s", bom_list[count], as_dict = 1) - child_items = child_items and child_items or [] - for item in child_items: - # Calculate qty required for FG's qty. - item['reqd_qty'] = flt(qty) * ((count == 0) and 1 or flt(qty_list[count]) )* flt(item['qty_consumed_per_unit']) - - # extracting Purchase Items - if ext_pur_items and not item['bom_no']: - self.pur_items[item['item_code']] = flt(self.pur_items.get(item['item_code'], 0)) + flt(item['reqd_qty']) - - # For calculate cost extracting BOM Items check for duplicate boms, this optmizes the time complexity for while loop. - if calculate_cost and item['bom_no'] and (item['bom_no'] not in bom_list): - bom_list.append(item['bom_no']) - qty_list.append(item['reqd_qty']) - - # Here repeated bom are considered to calculate total qty of raw material required - if not calculate_cost and item['bom_no']: - bom_list.append(item['bom_no']) - qty_list.append(item['reqd_qty']) - - count += 1 - return bom_list - - - - # Raise Production Order - def create_production_order(self, items): - """Create production order. Called from Production Planning Tool""" - - default_values = { - 'posting_date' : nowdate(), - 'origin' : 'MRP', - 'wip_warehouse' : '', - 'fg_warehouse' : '', - 'status' : 'Draft', - 'fiscal_year' : get_defaults()['fiscal_year'] - } - pro_list = [] - - for item_so in items: - if item_so[1]: - self.validate_production_order_against_so( - item_so[0], item_so[1], items[item_so].get("qty")) - - pro_doc = Document('Production Order') - pro_doc.production_item = item_so[0] - pro_doc.sales_order = item_so[1] - for key in items[item_so]: - pro_doc.fields[key] = items[item_so][key] - - for key in default_values: - pro_doc.fields[key] = default_values[key] - - pro_doc.save(new = 1) - pro_list.append(pro_doc.name) - - return pro_list - - def validate_production_order_against_so(self, item, sales_order, qty, pro_order=None): - # already ordered qty - ordered_qty_against_so = webnotes.conn.sql("""select sum(qty) from `tabProduction Order` - where production_item = %s and sales_order = %s and name != %s""", - (item, sales_order, cstr(pro_order)))[0][0] - # qty including current - total_ordered_qty_against_so = flt(ordered_qty_against_so) + flt(qty) - - # get qty from Sales Order Item table - so_item_qty = webnotes.conn.sql("""select sum(qty) from `tabSales Order Item` - where parent = %s and item_code = %s""", (sales_order, item))[0][0] - # get qty from Packing Item table - dnpi_qty = webnotes.conn.sql("""select sum(qty) from `tabDelivery Note Packing Item` - where parent = %s and parenttype = 'Sales Order' and item_code = %s""", - (sales_order, item))[0][0] - # total qty in SO - so_qty = flt(so_item_qty) + flt(dnpi_qty) - - if total_ordered_qty_against_so > so_qty: - msgprint("""Total production order qty for item: %s against sales order: %s \ - will be %s, which is greater than sales order qty (%s). - Please reduce qty or remove the item.""" % - (item, sales_order, total_ordered_qty_against_so, so_qty), raise_exception=1) - - def update_bom(self, bom_no): - main_bom_list = self.traverse_bom_tree(bom_no, 1) - main_bom_list.reverse() - # run calculate cost and get - for bom in main_bom_list: - if bom and bom not in self.check_bom_list: - bom_obj = get_obj('BOM', bom, with_children = 1) - bom_obj.doc.save() - bom_obj.check_recursion() - bom_obj.update_flat_bom_engine() - bom_obj.doc.docstatus = 1 - bom_obj.doc.save() - self.check_bom_list.append(bom) \ No newline at end of file diff --git a/production/doctype/production_control/production_control.txt b/production/doctype/production_control/production_control.txt deleted file mode 100644 index 4f8564c8e3d..00000000000 --- a/production/doctype/production_control/production_control.txt +++ /dev/null @@ -1,31 +0,0 @@ -# DocType, Production Control -[ - - # These values are common in all dictionaries - { - 'creation': '2012-03-27 14:36:05', - 'docstatus': 0, - 'modified': '2012-03-27 14:36:05', - 'modified_by': u'Administrator', - 'owner': u'Administrator' - }, - - # These values are common for all DocType - { - 'colour': u'White:FFF', - 'doctype': 'DocType', - 'issingle': 1, - 'module': u'Production', - 'name': '__common__', - 'section_style': u'Simple', - 'server_code_error': u' ', - 'show_in_menu': 0, - 'version': 19 - }, - - # DocType, Production Control - { - 'doctype': 'DocType', - 'name': u'Production Control' - } -] \ No newline at end of file diff --git a/production/doctype/production_order/production_order.js b/production/doctype/production_order/production_order.js index 79497573b10..85d9633334b 100644 --- a/production/doctype/production_order/production_order.js +++ b/production/doctype/production_order/production_order.js @@ -92,12 +92,9 @@ cur_frm.fields_dict['project_name'].get_query = function(doc, dt, dn) { AND `tabProject`.name LIKE "%s" ORDER BY `tabProject`.name ASC LIMIT 50'; } -cur_frm.fields_dict['bom_no'].get_query = function(doc) { - if (doc.production_item){ - return 'SELECT DISTINCT `tabBOM`.`name` FROM `tabBOM` WHERE `tabBOM`.`is_active` = "Yes" AND `tabBOM`.docstatus = 1 AND `tabBOM`.`item` = "' + cstr(doc.production_item) + '" AND`tabBOM`.%(key)s LIKE "%s" ORDER BY `tabBOM`.`name` LIMIT 50'; - } - else { - alert(" Please Enter Production Item First.") - } -} +cur_frm.set_query("bom_no", function(doc) { + if (doc.production_item) { + return erpnext.queries.bom({item: cstr(doc.production_item)}); + } else msgprint(" Please enter Production Item first"); +}); \ No newline at end of file diff --git a/production/doctype/production_order/production_order.py b/production/doctype/production_order/production_order.py index 53149715558..07073da918d 100644 --- a/production/doctype/production_order/production_order.py +++ b/production/doctype/production_order/production_order.py @@ -72,8 +72,33 @@ class DocType: where name=%s and docstatus = 1""", self.doc.sales_order): msgprint("Sales Order: %s is not valid" % self.doc.sales_order, raise_exception=1) - get_obj("Production Control").validate_production_order_against_so( - self.doc.production_item, self.doc.sales_order, self.doc.qty, self.doc.name) + self.validate_production_order_against_so() + + + def validate_production_order_against_so(self): + # already ordered qty + ordered_qty_against_so = webnotes.conn.sql("""select sum(qty) from `tabProduction Order` + where production_item = %s and sales_order = %s and docstatus < 2""", + (self.doc.production_item, self.doc.sales_order))[0][0] + + + # get qty from Sales Order Item table + so_item_qty = webnotes.conn.sql("""select sum(qty) from `tabSales Order Item` + where parent = %s and item_code = %s""", + (self.doc.sales_order, self.doc.production_item))[0][0] + # get qty from Packing Item table + dnpi_qty = webnotes.conn.sql("""select sum(qty) from `tabDelivery Note Packing Item` + where parent = %s and parenttype = 'Sales Order' and item_code = %s""", + (self.doc.sales_order, self.doc.production_item))[0][0] + # total qty in SO + so_qty = flt(so_item_qty) + flt(dnpi_qty) + + if ordered_qty_against_so > so_qty: + msgprint("""Total production order qty for item: %s against sales order: %s \ + will be %s, which is greater than sales order qty (%s). + Please reduce qty or remove the item.""" % + (self.doc.production_item, self.doc.sales_order, + ordered_qty_against_so, so_qty), raise_exception=1) def stop_unstop(self, status): diff --git a/production/doctype/production_planning_tool/production_planning_tool.js b/production/doctype/production_planning_tool/production_planning_tool.js index ac4d76d0f75..970da65c282 100644 --- a/production/doctype/production_planning_tool/production_planning_tool.js +++ b/production/doctype/production_planning_tool/production_planning_tool.js @@ -51,10 +51,9 @@ cur_frm.fields_dict['pp_details'].grid.get_field('item_code').get_query = functi cur_frm.fields_dict['pp_details'].grid.get_field('bom_no').get_query = function(doc) { var d = locals[this.doctype][this.docname]; - return 'SELECT DISTINCT `tabBOM`.`name` \ - FROM `tabBOM` WHERE `tabBOM`.`item` = "' + d.item_code + - '" AND `tabBOM`.`is_active` = "Yes" AND `tabBOM`.docstatus = 1 \ - AND `tabBOM`.`name` like "%s" ORDER BY `tabBOM`.`name` LIMIT 50'; + if (d.item_code) { + return erpnext.queries.bom({item: cstr(d.item_code)}); + } else msgprint(" Please enter Item first"); } cur_frm.fields_dict.customer.get_query = erpnext.utils.customer_query; diff --git a/production/doctype/production_planning_tool/production_planning_tool.py b/production/doctype/production_planning_tool/production_planning_tool.py index 64ac73d2b09..fb87fb27a3b 100644 --- a/production/doctype/production_planning_tool/production_planning_tool.py +++ b/production/doctype/production_planning_tool/production_planning_tool.py @@ -16,8 +16,8 @@ from __future__ import unicode_literals import webnotes -from webnotes.utils import cstr, flt -from webnotes.model.doc import addchild +from webnotes.utils import cstr, flt, nowdate, get_defaults +from webnotes.model.doc import addchild, Document from webnotes.model.wrapper import getlist from webnotes.model.code import get_obj from webnotes import msgprint @@ -186,7 +186,7 @@ class DocType: self.validate_data() items = self.get_distinct_items_and_boms()[1] - pro = get_obj('Production Control').create_production_order(items) + pro = self.create_production_order(items) if pro: msgprint("Following Production Order has been generated:\n" + '\n'.join(pro)) else : @@ -199,15 +199,39 @@ class DocType: for d in self.doclist.get({"parentfield": "pp_details"}): bom_dict[d.bom_no] = bom_dict.get(d.bom_no, 0) + flt(d.planned_qty) item_dict[(d.item_code, d.sales_order)] = { - "qty" : flt(item_dict.get((d.item_code, d.sales_order), {}).get("qty")) + \ - flt(d.planned_qty), - "bom_no": d.bom_no, - "description": d.description, - "stock_uom": d.stock_uom, + "qty" : flt(item_dict.get((d.item_code, d.sales_order), \ + {}).get("qty")) + flt(d.planned_qty), + "bom_no" : d.bom_no, + "description" : d.description, + "stock_uom" : d.stock_uom, "use_multi_level_bom": self.doc.use_multi_level_bom, - "company": self.doc.company, + "company" : self.doc.company, + "posting_date" : nowdate(), + "origin" : "MRP", + "wip_warehouse" : "", + "fg_warehouse" : "", + "status" : "Draft", + "fiscal_year" : get_defaults()["fiscal_year"] } return bom_dict, item_dict + + def create_production_order(self, items): + """Create production order. Called from Production Planning Tool""" + + pro_list = [] + for item_so in items: + pro_doc = Document('Production Order') + pro_doc.production_item = item_so[0] + pro_doc.sales_order = item_so[1] + for key in items[item_so]: + pro_doc.fields[key] = items[item_so][key] + + pro_doc.save(new = 1) + + get_obj("Production Order", pro_doc.name).validate_production_order_against_so() + pro_list.append(pro_doc.name) + + return pro_list def download_raw_materials(self): """ Create csv data for required raw material to produce finished goods""" diff --git a/production/page/production_home/production_home.html b/production/page/production_home/production_home.html index 50f99b33e65..4ac410c69eb 100644 --- a/production/page/production_home/production_home.html +++ b/production/page/production_home/production_home.html @@ -22,6 +22,16 @@
+
+
Tools
+ +
Setup
diff --git a/public/js/utils.js b/public/js/utils.js index d02fdb32294..599232d6388 100644 --- a/public/js/utils.js +++ b/public/js/utils.js @@ -103,4 +103,24 @@ erpnext.queries.account = function(opts) { AND tabAccount.%(key)s LIKE "%s" ' + (conditions ? (" AND " + conditions.join(" AND ")) : "") +} + +erpnext.queries.bom = function(opts) { + conditions = []; + if (opts) { + $.each(opts, function(key, val) { + if (esc_quotes(val).charAt(0) != "!") + conditions.push("tabBOM.`" + key + "`='"+esc_quotes(val)+"'"); + else + conditions.push("tabBOM.`" + key + "`!='"+esc_quotes(val).substr(1)+"'"); + }); + } + + return 'SELECT tabBOM.name, tabBOM.item \ + FROM tabBOM \ + WHERE tabBOM.docstatus=1 \ + AND tabBOM.is_active="Yes" \ + AND tabBOM.%(key)s LIKE "%s" ' + (conditions.length + ? (" AND " + conditions.join(" AND ")) + : "") } \ No newline at end of file diff --git a/stock/doctype/stock_entry/stock_entry.py b/stock/doctype/stock_entry/stock_entry.py index 67f1643fdef..04d898aa437 100644 --- a/stock/doctype/stock_entry/stock_entry.py +++ b/stock/doctype/stock_entry/stock_entry.py @@ -379,7 +379,7 @@ class DocType(TransactionBase): d.t_warehouse = self.doc.to_warehouse if not (d.s_warehouse or d.t_warehouse): - msgprint("Atleast one warehouse is mandatory for Stock Entry ") + msgprint("Atleast one warehouse is mandatory for Stock Entry") raise Exception if d.s_warehouse and not sql("select name from tabWarehouse where name = '%s'" % d.s_warehouse): msgprint("Invalid Warehouse: %s" % self.doc.s_warehouse) @@ -390,6 +390,7 @@ class DocType(TransactionBase): if d.s_warehouse == d.t_warehouse: msgprint("Source and Target Warehouse Cannot be Same.") raise Exception + if self.doc.purpose == 'Material Issue': if not cstr(d.s_warehouse): msgprint("Source Warehouse is Mandatory for Purpose => 'Material Issue'") @@ -415,6 +416,7 @@ class DocType(TransactionBase): if not cstr(d.s_warehouse): msgprint("Please Enter Source Warehouse at Row No %s." % (cstr(d.idx))) raise Exception + if self.doc.process == 'Backflush': if flt(d.fg_item): if cstr(pro_obj.doc.production_item) != cstr(d.item_code): diff --git a/tests/data/item/android_jack_d.txt b/tests/data/item/android_jack_d.txt index 24944e3bae3..c7edeee825d 100644 --- a/tests/data/item/android_jack_d.txt +++ b/tests/data/item/android_jack_d.txt @@ -18,6 +18,9 @@ 'has_serial_no': u'No', 'inspection_required': u'No', 'is_purchase_item': u'Yes', + 'is_manufactured_item': u'Yes', + 'is_sub_contracted_item': 'Yes', + 'is_pro_applicable': 'Yes', 'is_sales_item': u'Yes', 'is_service_item': u'No', 'is_stock_item': u'Yes', diff --git a/tests/data/item/android_jack_s.txt b/tests/data/item/android_jack_s.txt index feaceef3683..d32d79373dd 100644 --- a/tests/data/item/android_jack_s.txt +++ b/tests/data/item/android_jack_s.txt @@ -18,6 +18,9 @@ 'has_serial_no': u'No', 'inspection_required': u'No', 'is_purchase_item': u'Yes', + 'is_manufactured_item': u'Yes', + 'is_sub_contracted_item': 'Yes', + 'is_pro_applicable': 'Yes', 'is_sales_item': u'Yes', 'is_service_item': u'No', 'is_stock_item': u'Yes', diff --git a/tests/data/item/home_desktop_100.txt b/tests/data/item/home_desktop_100.txt index 19ef01d341d..6745c0d438e 100644 --- a/tests/data/item/home_desktop_100.txt +++ b/tests/data/item/home_desktop_100.txt @@ -18,6 +18,7 @@ 'has_serial_no': u'No', 'inspection_required': u'No', 'is_purchase_item': u'Yes', + 'is_manufactured_item': u'No', 'is_sales_item': u'Yes', 'is_service_item': u'No', 'is_stock_item': u'Yes',