Contract Manufacturing : Customer Provided Items (#15828)

* Material Request from Production Plan for Customer provided parts

* Test cases

* Customer web portal for their material requests
This commit is contained in:
Doridel Cahanap 2018-11-13 14:37:16 +08:00 committed by Nabin Hait
parent 35b2627112
commit 2b14d6a058
20 changed files with 5207 additions and 4876 deletions

View File

@ -133,6 +133,13 @@ website_route_rules = [
{"from_route": "/admissions", "to_route": "Student Admission"},
{"from_route": "/boms", "to_route": "BOM"},
{"from_route": "/timesheets", "to_route": "Timesheet"},
{"from_route": "/material-requests", "to_route": "Material Request"},
{"from_route": "/material-requests/<path:name>", "to_route": "material_request_info",
"defaults": {
"doctype": "Material Request",
"parents": [{"label": _("Material Request"), "route": "material-requests"}]
}
},
]
standard_portal_menu_items = [
@ -155,6 +162,7 @@ standard_portal_menu_items = [
{"title": _("Newsletter"), "route": "/newsletters", "reference_doctype": "Newsletter"},
{"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission"},
{"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application"},
{"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"},
]
default_roles = [
@ -168,6 +176,7 @@ has_website_permission = {
"Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Sales Invoice": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Supplier Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Material Request": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Delivery Note": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Issue": "erpnext.support.doctype.issue.issue.has_website_permission",
"Timesheet": "erpnext.controllers.website_list_for_contact.has_website_permission",

View File

@ -159,28 +159,30 @@ class BOM(WebsiteGenerator):
if arg.get('scrap_items'):
rate = self.get_valuation_rate(arg)
elif arg:
if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom:
rate = self.get_bom_unitcost(arg['bom_no'])
else:
if self.rm_cost_as_per == 'Valuation Rate':
rate = self.get_valuation_rate(arg)
elif self.rm_cost_as_per == 'Last Purchase Rate':
rate = arg.get('last_purchase_rate') \
or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate")
elif self.rm_cost_as_per == "Price List":
if not self.buying_price_list:
frappe.throw(_("Please select Price List"))
rate = frappe.db.get_value("Item Price", {"price_list": self.buying_price_list,
"item_code": arg["item_code"]}, "price_list_rate") or 0.0
#Customer Provided parts will have zero rate
if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item'):
if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom:
rate = self.get_bom_unitcost(arg['bom_no'])
else:
if self.rm_cost_as_per == 'Valuation Rate':
rate = self.get_valuation_rate(arg)
elif self.rm_cost_as_per == 'Last Purchase Rate':
rate = arg.get('last_purchase_rate') \
or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate")
elif self.rm_cost_as_per == "Price List":
if not self.buying_price_list:
frappe.throw(_("Please select Price List"))
rate = frappe.db.get_value("Item Price", {"price_list": self.buying_price_list,
"item_code": arg["item_code"]}, "price_list_rate") or 0.0
price_list_currency = frappe.db.get_value("Price List",
self.buying_price_list, "currency")
if price_list_currency != self.company_currency():
rate = flt(rate * self.conversion_rate)
price_list_currency = frappe.db.get_value("Price List",
self.buying_price_list, "currency")
if price_list_currency != self.company_currency():
rate = flt(rate * self.conversion_rate)
if not rate:
frappe.msgprint(_("{0} not found for Item {1}")
.format(self.rm_cost_as_per, arg["item_code"]), alert=True)
if not rate:
frappe.msgprint(_("{0} not found for Item {1}")
.format(self.rm_cost_as_per, arg["item_code"]), alert=True)
return flt(rate)

View File

@ -131,4 +131,4 @@ class TestBOM(unittest.TestCase):
self.assertEqual(bom.base_total_cost, 33000)
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@ -419,8 +419,8 @@ class ProductionPlan(Document):
for item in self.mr_items:
item_doc = frappe.get_cached_doc('Item', item.item_code)
# key for Sales Order:Material Request Type
key = '{}:{}'.format(item.sales_order, item_doc.default_material_request_type)
# key for Sales Order:Material Request Type:Customer
key = '{}:{}:{}'.format(item.sales_order, item_doc.default_material_request_type,item_doc.customer or '')
schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days))
if not key in material_request_map:
@ -432,7 +432,8 @@ class ProductionPlan(Document):
"status": "Draft",
"company": self.company,
"requested_by": frappe.session.user,
'material_request_type': item_doc.default_material_request_type
'material_request_type': item_doc.default_material_request_type,
'customer': item_doc.customer or ''
})
material_request_list.append(material_request)
else:

View File

@ -142,12 +142,27 @@ class TestProductionPlan(unittest.TestCase):
self.assertEqual(sales_orders, [])
def test_pp_to_mr_customer_provided(self):
#Material Request from Production Plan for Customer Provided
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
create_item('Production Item CUST')
for item, raw_materials in {'Production Item CUST': ['Raw Material Item 1', 'CUST-0987']}.items():
if not frappe.db.get_value('BOM', {'item': item}):
make_bom(item = item, raw_materials = raw_materials)
production_plan = create_production_plan(item_code = 'Production Item CUST')
production_plan.make_material_request()
material_request = frappe.get_value('Material Request Item', {'production_plan': production_plan.name}, 'parent')
mr = frappe.get_doc('Material Request', material_request)
self.assertTrue(mr.material_request_type, 'Customer Provided')
self.assertTrue(mr.customer, '_Test Customer')
def create_production_plan(**args):
args = frappe._dict(args)
pln = frappe.get_doc({
'doctype': 'Production Plan',
'company': args.company or '_Test Company',
'customer': args.customer or '_Test Customer',
'posting_date': nowdate(),
'include_non_stock_items': args.include_non_stock_items or 1,
'include_subcontracted_items': args.include_subcontracted_items or 1,

View File

@ -68,12 +68,19 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
}
this.frm.set_query("item_code", "items", function() {
if(me.frm.doc.is_subcontracted == "Yes") {
if (me.frm.doc.is_subcontracted == "Yes") {
return{
query: "erpnext.controllers.queries.item_query",
filters:{ 'is_sub_contracted_item': 1 }
}
} else {
}
else if (me.frm.doc.material_request_type == "Customer Provided") {
return{
query: "erpnext.controllers.queries.item_query",
filters:{ 'customer': me.frm.doc.customer }
}
}
else {
return{
query: "erpnext.controllers.queries.item_query",
filters: {'is_purchase_item': 1}

View File

@ -177,6 +177,9 @@ class DeliveryNote(SellingController):
frappe.msgprint(_("Note: Item {0} entered multiple times").format(d.item_code))
else:
chk_dupl_itm.append(f)
#Customer Provided parts will have zero valuation rate
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
def validate_warehouse(self):
super(DeliveryNote, self).validate_warehouse()

View File

@ -115,6 +115,8 @@ frappe.ui.form.on("Item", {
['is_stock_item', 'has_serial_no', 'has_batch_no'].forEach((fieldname) => {
frm.set_df_property(fieldname, 'read_only', stock_exists);
});
frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0);
},
validate: function(frm){
@ -124,6 +126,10 @@ frappe.ui.form.on("Item", {
image: function() {
refresh_field("image_view");
},
is_customer_provided_item: function(frm) {
frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0);
},
is_fixed_asset: function(frm) {
frm.call({

File diff suppressed because it is too large Load Diff

View File

@ -124,6 +124,7 @@ class Item(WebsiteGenerator):
self.validate_uom_conversion_factor()
self.validate_item_defaults()
self.update_defaults_from_item_group()
self.validate_customer_provided_part()
if not self.get("__islocal"):
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@ -143,6 +144,14 @@ class Item(WebsiteGenerator):
if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')):
self.description = clean_html(self.description)
def validate_customer_provided_part(self):
if self.is_customer_provided_item:
if self.is_purchase_item:
frappe.throw(_('"Customer Provided Item" cannot be Purchase Item also'))
if self.valuation_rate:
frappe.throw(_('"Customer Provided Item" cannot have Valuation Rate'))
self.default_material_request_type = "Customer Provided"
def add_price(self, price_list=None):
'''Add a new price'''
if not price_list:

View File

@ -319,7 +319,7 @@ def make_item_variant():
test_records = frappe.get_test_records('Item')
def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None):
def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, is_customer_provided_item=None, customer=None, is_purchase_item=None):
if not frappe.db.exists("Item", item_code):
item = frappe.new_doc("Item")
item.item_code = item_code
@ -328,6 +328,9 @@ def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None)
item.item_group = "All Item Groups"
item.is_stock_item = is_stock_item or 1
item.valuation_rate = valuation_rate or 0.0
item.is_purchase_item = is_purchase_item
item.is_customer_provided_item = is_customer_provided_item
item.customer = customer or ''
item.append("item_defaults", {
"default_warehouse": warehouse or '_Test Warehouse - _TC',
"company": "_Test Company"

View File

@ -40,6 +40,7 @@ frappe.ui.form.on('Material Request', {
refresh: function(frm) {
frm.events.make_custom_buttons(frm);
frm.toggle_reqd('customer', frm.doc.material_request_type=="Customer Provided");
},
make_custom_buttons: function(frm) {
@ -61,6 +62,11 @@ frappe.ui.form.on('Material Request', {
() => frm.events.make_stock_entry(frm), __("Make"));
}
if (frm.doc.material_request_type === "Customer Provided") {
frm.add_custom_button(__("Material Receipt"),
() => frm.events.make_stock_entry(frm), __("Make"));
}
if (frm.doc.material_request_type === "Purchase") {
frm.add_custom_button(__('Purchase Order'),
() => frm.events.make_purchase_order(frm), __("Make"));
@ -259,6 +265,9 @@ frappe.ui.form.on('Material Request', {
}
});
},
material_request_type: function(frm) {
frm.toggle_reqd('customer', frm.doc.material_request_type=="Customer Provided");
},
});

File diff suppressed because it is too large Load Diff

View File

@ -70,7 +70,7 @@ class MaterialRequest(BuyingController):
from erpnext.controllers.status_updater import validate_status
validate_status(self.status,
["Draft", "Submitted", "Stopped", "Cancelled", "Pending",
"Partially Ordered", "Ordered", "Issued", "Transferred"])
"Partially Ordered", "Ordered", "Issued", "Transferred", "Received"])
validate_for_items(self)
@ -154,7 +154,7 @@ class MaterialRequest(BuyingController):
for d in self.get("items"):
if d.name in mr_items:
if self.material_request_type in ("Material Issue", "Material Transfer"):
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
d.ordered_qty = flt(frappe.db.sql("""select sum(transfer_qty)
from `tabStock Entry Detail` where material_request = %s
and material_request_item = %s and docstatus = 1""",
@ -239,6 +239,18 @@ def update_item(obj, target, source_parent):
target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty))/ target.conversion_factor
target.stock_qty = (target.qty * target.conversion_factor)
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context)
list_context.update({
'show_sidebar': True,
'show_search': True,
'no_breadcrumbs': True,
'title': _('Material Request'),
})
return list_context
@frappe.whitelist()
def update_status(name, status):
material_request = frappe.get_doc('Material Request', name)
@ -400,7 +412,7 @@ def make_stock_entry(source_name, target_doc=None):
target.transfer_qty = qty * obj.conversion_factor
target.conversion_factor = obj.conversion_factor
if source_parent.material_request_type == "Material Transfer":
if source_parent.material_request_type == "Material Transfer" or source_parent.material_request_type == "Customer Provided":
target.t_warehouse = obj.warehouse
else:
target.s_warehouse = obj.warehouse
@ -410,6 +422,9 @@ def make_stock_entry(source_name, target_doc=None):
if source.job_card:
target.purpose = 'Material Transfer for Manufacture'
if source.material_request_type == "Customer Provided":
target.purpose = "Material Receipt"
target.run_method("calculate_rate_and_amount")
target.set_job_card_data()

View File

@ -14,6 +14,8 @@ frappe.listview_settings['Material Request'] = {
return [__("Transfered"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Material Issue") {
return [__("Issued"), "green", "per_ordered,=,100"];
} else if (doc.material_request_type == "Customer Provided") {
return [__("Received"), "green", "per_ordered,=,100"];
}
}
}

View File

@ -8,6 +8,7 @@ from __future__ import unicode_literals
import frappe, unittest, erpnext
from frappe.utils import flt, today
from erpnext.stock.doctype.material_request.material_request import raise_work_orders
from erpnext.stock.doctype.item.test_item import create_item
class TestMaterialRequest(unittest.TestCase):
def setUp(self):
@ -601,11 +602,25 @@ class TestMaterialRequest(unittest.TestCase):
mr = frappe.get_doc("Material Request", mr.name)
self.assertEqual(mr.per_ordered, 100)
def test_customer_provided_parts_mr(self):
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
mr = make_material_request(item_code='CUST-0987', material_request_type='Customer Provided')
se = make_stock_entry(mr.name)
se.insert()
se.submit()
self.assertEqual(se.get("items")[0].amount, 0)
self.assertEqual(se.get("items")[0].material_request, mr.name)
mr = frappe.get_doc("Material Request", mr.name)
mr.submit()
self.assertEqual(mr.per_ordered, 100)
def make_material_request(**args):
args = frappe._dict(args)
mr = frappe.new_doc("Material Request")
mr.material_request_type = args.material_request_type or "Purchase"
mr.company = args.company or "_Test Company"
mr.customer = args.customer or '_Test Customer'
mr.append("items", {
"item_code": args.item_code or "_Test Item",
"qty": args.qty or 10,

View File

@ -179,6 +179,10 @@ class StockEntry(StockController):
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
frappe.MandatoryError)
#Customer Provided parts will have zero valuation rate
if frappe.db.get_value('Item', item.item_code, 'is_customer_provided_item'):
item.allow_zero_valuation_rate = 1
def validate_qty(self):
manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]

View File

@ -720,6 +720,13 @@ class TestStockEntry(unittest.TestCase):
for d in stock_entry.get('items'):
self.assertEqual(item_quantity.get(d.item_code), d.qty)
def test_customer_provided_parts_se(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
se = make_stock_entry(item_code='CUST-0987', purporse = 'Material Receipt', qty=4, to_warehouse = "_Test Warehouse - _TC")
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0)
def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None):
se = frappe.copy_doc(test_records[0])
se.get("items")[0].item_code = item_code or "_Test Serialized Item With Series"

View File

@ -0,0 +1,74 @@
{% extends "templates/web.html" %}
{% from "erpnext/templates/includes/order/order_macros.html" import item_name_and_description %}
{% block breadcrumbs %}
{% include "templates/includes/breadcrumbs.html" %}
{% endblock %}
{% block title %}{{ doc.name }}{% endblock %}
{% block header %}
<h1>{{ doc.name }}</h1>
{% endblock %}
{% block header_actions %}
<a class='btn btn-xs btn-default' href='/printview?doctype={{ doc.doctype}}&name={{ doc.name }}&format={{ print_format }}' target="_blank" rel="noopener noreferrer">{{ _("Print") }}</a>
{% endblock %}
{% block page_content %}
<div class="row transaction-subheading">
<div class="col-xs-6">
<span class="indicator {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
{{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
</span>
</div>
<div class="col-xs-6 text-muted text-right small">
{{ frappe.utils.formatdate(doc.transaction_date, 'medium') }}
</div>
</div>
{% if doc._header %}
{{ doc._header }}
{% endif %}
<div class="order-container">
<!-- items -->
<div class="order-item-table">
<div class="row order-items order-item-header text-muted">
<div class="col-sm-6 col-xs-6 h6 text-uppercase">
{{ _("Item") }}
</div>
<div class="col-sm-3 col-xs-3 text-right h6 text-uppercase">
{{ _("Work Order") }}
</div>
<div class="col-sm-3 col-xs-3 text-right h6 text-uppercase">
{{ _("Quantity") }}
</div>
</div>
{% for d in doc.items %}
{% if d.customer_provided %}
<div class="row order-items">
<div class="col-sm-6 col-xs-6">
{{ item_name_and_description(d) }}
</div>
<div class="col-sm-3 col-xs-3 text-right">
{% for wo in d.work_orders %}
<p class="text-muted small">{{_(wo.name) }}</p>
{% endfor %}
</div>
<div class="col-sm-3 col-xs-3 text-right">
{{ d.qty }}
{% if d.delivered_qty is defined and d.delivered_qty != None %}
<p class="text-muted small">{{
_("Delivered: {0}").format(d.delivered_qty) }}</p>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,49 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
def get_context(context):
context.no_cache = 1
context.show_sidebar = True
context.doc = frappe.get_doc(frappe.form_dict.doctype, frappe.form_dict.name)
if hasattr(context.doc, "set_indicator"):
context.doc.set_indicator()
context.parents = frappe.form_dict.parents
context.title = frappe.form_dict.name
if not frappe.has_website_permission(context.doc):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=frappe.form_dict.doctype), "value")
if default_print_format:
context.print_format = default_print_format
else:
context.print_format = "Standard"
context.doc.items = get_more_items_info(context.doc.items, context.doc.name)
def get_more_items_info(items, material_request):
for item in items:
item.customer_provided = frappe.get_value('Item', item.item_code, 'is_customer_provided_item')
item.work_orders = frappe.db.sql("""
select
wo.name, wo.status, wo_item.consumed_qty
from
`tabWork Order Item` wo_item, `tabWork Order` wo
where
wo_item.item_code=%s
and wo_item.consumed_qty=0
and wo_item.parent=wo.name
and wo.status not in ('Completed', 'Cancelled', 'Stopped')
order by
wo.name asc""", item.item_code, as_dict=1)
item.delivered_qty = flt(frappe.db.sql("""select sum(transfer_qty)
from `tabStock Entry Detail` where material_request = %s
and item_code = %s and docstatus = 1""",
(material_request, item.item_code))[0][0])
return items