From a1064a6149d82d18fe5423274091986186a81c58 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 3 Mar 2016 14:00:35 +0530 Subject: [PATCH] [enhancement] request for quotation --- erpnext/accounts/party.py | 4 +- .../request_for_quotation.js | 54 ++++--- .../request_for_quotation.json | 54 ++----- .../request_for_quotation.py | 144 ++++++++++++++++-- .../test_request_for_quotation.py | 29 +++- .../request_for_quotation_item.json | 34 +---- .../doctype/rfq_supplier/rfq_supplier.json | 133 +++++++++++++++- .../supplier_quotation.json | 20 +++ .../supplier_quotation_item.json | 58 +++---- erpnext/controllers/buying_controller.py | 7 +- .../controllers/website_list_for_contact.py | 30 +++- erpnext/hooks.py | 7 + erpnext/shopping_cart/utils.py | 27 ++++ erpnext/startup/notifications.py | 3 + .../material_request/material_request.js | 4 +- .../emails/request_for_quotation.html | 12 ++ erpnext/templates/includes/rfq.js | 84 ++++++++++ erpnext/templates/includes/rfq/rfq_items.html | 30 ++++ .../templates/includes/rfq/rfq_macros.html | 21 +++ erpnext/templates/pages/rfq.html | 83 ++++++++++ erpnext/templates/pages/rfq.py | 31 ++++ 21 files changed, 722 insertions(+), 147 deletions(-) create mode 100644 erpnext/templates/emails/request_for_quotation.html create mode 100644 erpnext/templates/includes/rfq.js create mode 100644 erpnext/templates/includes/rfq/rfq_items.html create mode 100644 erpnext/templates/includes/rfq/rfq_macros.html create mode 100644 erpnext/templates/pages/rfq.html create mode 100644 erpnext/templates/pages/rfq.py diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 0c8d999a98..244d39e1b6 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -16,7 +16,7 @@ class DuplicatePartyAccountError(frappe.ValidationError): pass @frappe.whitelist() def get_party_details(party=None, account=None, party_type="Customer", company=None, - posting_date=None, price_list=None, currency=None, doctype=None): + posting_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False): if not party: return {} @@ -25,7 +25,7 @@ def get_party_details(party=None, account=None, party_type="Customer", company=N frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) return _get_party_details(party, account, party_type, - company, posting_date, price_list, currency, doctype) + company, posting_date, price_list, currency, doctype, ignore_permissions) def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False): diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index f4fb5e269b..0d509e2498 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -6,26 +6,30 @@ frappe.require("assets/erpnext/js/utils.js"); -erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.extend({ - refresh: function() { - this._super(); - - if (this.frm.doc.docstatus === 1) { - cur_frm.add_custom_button(__("Supplier Quotation"), this.make_suppplier_quotation, - __("Make")); - cur_frm.page.set_inner_btn_group_as_primary(__("Make")); +frappe.ui.form.on("Request for Quotation",{ + setup: function(frm){ + frm.fields_dict["suppliers"].grid.get_field("contact_person").get_query = function(doc, cdt, cdn){ + var d =locals[cdt][cdn]; + return { + filters: {'supplier': d.supplier} + } } }, - - calculate_taxes_and_totals: function() { - return; + + onload: function(frm){ + frm.add_fetch('standard_reply', 'response', 'response'); }, - tc_name: function() { - this.get_terms(); + refresh: function(frm, cdt, cdn){ + if (frm.doc.docstatus === 1) { + frm.add_custom_button(__("Supplier Quotation"), function(){ frm.trigger("make_suppplier_quotation") }, + __("Make")); + frm.page.set_inner_btn_group_as_primary(__("Make")); + } }, - - make_suppplier_quotation: function() { + + make_suppplier_quotation: function(frm){ + var doc = frm.doc; var dialog = new frappe.ui.Dialog({ title: __("For Supplier"), fields: [ @@ -33,13 +37,13 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e "get_query": function () { return { query:"erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier", - filters: {'parent': cur_frm.doc.name} + filters: {'parent': doc.name} } }, "reqd": 1 }, {"fieldtype": "Button", "label": __("Make Supplier Quotation"), "fieldname": "make_supplier_quotation", "cssClass": "btn-primary"}, ] }); - + dialog.fields_dict.make_supplier_quotation.$input.click(function(){ args = dialog.get_values(); if(!args) return; @@ -48,7 +52,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e type: "GET", method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation", args: { - "source_name": cur_frm.doc.name, + "source_name": doc.name, "for_supplier": args.supplier }, freeze: true, @@ -62,8 +66,22 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e }); dialog.show() } +}) +erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.extend({ + refresh: function() { + this._super(); + }, + + calculate_taxes_and_totals: function() { + return; + }, + + tc_name: function() { + this.get_terms(); + } }); + // for backward compatibility: combine new and previous states $.extend(cur_frm.cscript, new erpnext.buying.RequestforQuotationController({frm: cur_frm})); diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index d46bbff183..88a91a0a37 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -9,32 +9,6 @@ "doctype": "DocType", "document_type": "Document", "fields": [ - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "section_break_1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_on_submit": 0, "bold": 0, @@ -150,14 +124,14 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "section_break_2", + "fieldname": "suppliers_section", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Supplier Detail", + "label": "", "length": 0, "no_copy": 0, "permlevel": 0, @@ -188,7 +162,7 @@ "options": "RFQ Supplier", "permlevel": 0, "precision": "", - "print_hide": 0, + "print_hide": 1, "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, @@ -208,7 +182,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Items", + "label": "", "length": 0, "no_copy": 0, "oldfieldtype": "Section Break", @@ -256,7 +230,7 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "section_break_3", + "fieldname": "supplier_response_section", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, @@ -281,16 +255,17 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "message_for_supplier", - "fieldtype": "Small Text", + "fieldname": "standard_reply", + "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, - "label": "Message for Supplier", + "label": "Standard Reply", "length": 0, "no_copy": 0, + "options": "Standard Reply", "permlevel": 0, "precision": "", "print_hide": 0, @@ -306,22 +281,23 @@ "allow_on_submit": 0, "bold": 0, "collapsible": 0, - "fieldname": "column_break2", - "fieldtype": "Column Break", + "fieldname": "response", + "fieldtype": "Text Editor", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 0, + "label": "Message for Supplier", "length": 0, "no_copy": 0, "permlevel": 0, "precision": "", - "print_hide": 0, + "print_hide": 1, "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, - "reqd": 0, + "reqd": 1, "search_index": 0, "set_only_once": 0, "unique": 0 @@ -633,7 +609,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-03-02 18:50:02.603258", + "modified": "2016-03-25 01:14:01.194848", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 63b0357091..92089e4a16 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -3,28 +3,107 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe +import frappe, json from frappe import _ +from frappe.utils import get_url, cint +from frappe.utils.user import get_user_fullname from frappe.model.mapper import get_mapped_doc from erpnext.stock.doctype.material_request.material_request import set_missing_values -from frappe.model.document import Document +from erpnext.controllers.buying_controller import BuyingController -class RequestforQuotation(Document): +STANDARD_USERS = ("Guest", "Administrator") + +class RequestforQuotation(BuyingController): def validate(self): self.validate_duplicate_supplier() - + self.validate_common() + def validate_duplicate_supplier(self): supplier_list = [d.supplier for d in self.suppliers] if len(supplier_list) != len(set(supplier_list)): frappe.throw(_("Same supplier has been entered multiple times")) - + + def validate_common(self): + pc = frappe.get_doc('Purchase Common') + pc.validate_for_items(self) + + def on_submit(self): + frappe.db.set(self, 'status', 'Submitted') + self.send_to_supplier() + + def on_cancel(self): + frappe.db.set(self, 'status', 'Cancelled') + + def send_to_supplier(self): + link = get_url("/rfq/" + self.name) + for supplier_data in self.suppliers: + if supplier_data.email_id and cint(supplier_data.sent_email_to_supplier)==1: + update_password_link = self.create_supplier_user(supplier_data, link) + self.supplier_rfq_mail(supplier_data, update_password_link, link) + + def create_supplier_user(self, supplier_data, link): + from frappe.utils import random_string, get_url + + update_password_link = '' + if not supplier_data.user_id: + user = self.create_user(supplier_data) + key = random_string(32) + user.reset_password_key = key + user.redirect_url = link + user.save(ignore_permissions=True) + + update_password_link = get_url("/update-password?key=" + key) + frappe.get_doc('Contact', supplier_data.contact_person).save() + + return update_password_link + + def create_user(self, supplier_data): + user = frappe.get_doc({ + 'doctype': 'User', + 'send_welcome_email': 0, + 'email': supplier_data.email_id, + 'first_name': supplier_data.supplier_name, + 'user_type': 'Website User' + }) + + return user + + def supplier_rfq_mail(self, data, update_password_link, rfq_link): + full_name = get_user_fullname(frappe.session['user']) + if full_name == "Guest": + full_name = "Administrator" + + args = { + 'update_password_link': update_password_link, + 'message': frappe.render_template(self.response, data.as_dict()), + 'rfq_link': rfq_link, + 'user_fullname': full_name + } + + subject = _("Request for Quotation") + template = "templates/emails/request_for_quotation.html" + sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None + + frappe.sendmail(recipients=data.email_id, sender=sender, subject=subject, + message=frappe.get_template(template).render(args), + attachments = [frappe.attach_print('Request for Quotation', self.name)],as_bulk=True) + + frappe.msgprint(_("Email sent to supplier {0}").format(data.supplier)) + +def get_list_context(context=None): + from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) + return list_context + @frappe.whitelist() def get_supplier(doctype, txt, searchfield, start, page_len, filters): - query = """ Select supplier from `tabRFQ Supplier` where parent = '{parent}' and supplier like %s - limit {start}, {page_len} """ + query = """Select supplier from `tabRFQ Supplier` where parent = %(parent)s and supplier like %(txt)s + limit %(start)s, %(page_len)s """ - return frappe.db.sql(query.format(parent=filters.get('parent'), start=start, page_len=page_len), '%{0}%'.format(txt)) - + return frappe.db.sql(query, {'parent': filters.get('parent'), + 'start': start, 'page_len': page_len, 'txt': "%%%s%%" % frappe.db.escape(txt)}) + +# This method is used to make supplier quotation from material request form. @frappe.whitelist() def make_supplier_quotation(source_name, for_supplier, target_doc=None): def postprocess(source, target_doc): @@ -49,3 +128,50 @@ def make_supplier_quotation(source_name, for_supplier, target_doc=None): }, target_doc, postprocess) return doclist + +# This method is used to make supplier quotation from supplier's portal. +@frappe.whitelist() +def create_supplier_quotation(doc): + if isinstance(doc, basestring): + doc = json.loads(doc) + + supplier = frappe.get_doc('Supplier', doc.get('supplier')) + + try: + sq_doc = frappe.get_doc({ + "doctype": "Supplier Quotation", + "supplier": supplier.name, + "terms": doc.get("terms"), + "company": doc.get("company"), + "currency": supplier.default_currency, + "buying_price_list": supplier.default_price_list or frappe.db.get_value('Buying Settings', None, 'buying_price_list') + }) + add_items(sq_doc, supplier, doc.get('items')) + sq_doc.flags.ignore_permissions = True + sq_doc.run_method("set_missing_values") + sq_doc.save() + frappe.msgprint(_("Supplier Quotation {0} created").format(sq_doc.name)) + return sq_doc.name + except Exception: + return + +def add_items(sq_doc, supplier, items): + for data in items: + if data.get("qty") > 0: + if isinstance(data, dict): + data = frappe._dict(data) + + create_rfq_items(sq_doc, supplier, data) + +def create_rfq_items(sq_doc, supplier, data): + sq_doc.append('items', { + "item_code": data.item_code, + "item_name": data.item_name, + "description": data.description, + "qty": data.qty, + "rate": data.rate, + "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"), + "warehouse": data.warehouse or '', + "request_for_quotation_item": data.name, + "request_for_quotation": data.parent + }) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index db828b842c..d643a051ba 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -12,33 +12,48 @@ class TestRequestforQuotation(unittest.TestCase): from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation rfq = make_request_for_quotation() - sq = make_supplier_quotation(rfq.name) - sq.supplier = rfq.get('supplier_detail')[0].supplier + sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier) sq.submit() - sq1 = make_supplier_quotation(rfq.name) - sq1.supplier = rfq.get('supplier_detail')[1].supplier + sq1 = make_supplier_quotation(rfq.name, rfq.get('suppliers')[1].supplier) sq1.submit() - self.assertEquals(sq.supplier, rfq.get('supplier_detail')[0].supplier) + self.assertEquals(sq.supplier, rfq.get('suppliers')[0].supplier) self.assertEquals(sq.get('items')[0].request_for_quotation, rfq.name) self.assertEquals(sq.get('items')[0].item_code, "_Test Item") self.assertEquals(sq.get('items')[0].qty, 5) - self.assertEquals(sq1.supplier, rfq.get('supplier_detail')[1].supplier) + self.assertEquals(sq1.supplier, rfq.get('suppliers')[1].supplier) self.assertEquals(sq1.get('items')[0].request_for_quotation, rfq.name) self.assertEquals(sq1.get('items')[0].item_code, "_Test Item") self.assertEquals(sq1.get('items')[0].qty, 5) + + def test_make_supplier_quotation_from_portal(self): + from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation + rfq = make_request_for_quotation() + rfq.get('items')[0].rate = 100 + rfq.supplier = rfq.suppliers[0].supplier + supplier_quotation_name = create_supplier_quotation(rfq) + supplier_quotation_doc = frappe.get_doc('Supplier Quotation', supplier_quotation_name) + + self.assertEquals(supplier_quotation_doc.supplier, rfq.get('suppliers')[0].supplier) + self.assertEquals(supplier_quotation_doc.get('items')[0].request_for_quotation, rfq.name) + self.assertEquals(supplier_quotation_doc.get('items')[0].item_code, "_Test Item") + self.assertEquals(supplier_quotation_doc.get('items')[0].qty, 5) + self.assertEquals(supplier_quotation_doc.get('items')[0].amount, 500) + + def make_request_for_quotation(): supplier_data = get_supplier_data() rfq = frappe.new_doc('Request for Quotation') rfq.transaction_date = nowdate() rfq.status = 'Draft' rfq.company = '_Test Company' + rfq.response = 'Test Data' for data in supplier_data: - rfq.append('supplier_detail', data) + rfq.append('suppliers', data) rfq.append("items", { "item_code": "_Test Item", diff --git a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json index 08bc16d91e..305253c193 100644 --- a/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json +++ b/erpnext/buying/doctype/request_for_quotation_item/request_for_quotation_item.json @@ -28,7 +28,7 @@ "options": "Item", "permlevel": 0, "precision": "", - "print_hide": 0, + "print_hide": 1, "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, @@ -37,32 +37,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "description": "", - "fieldname": "supplier_part_no", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Supplier Part Number", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_on_submit": 0, "bold": 0, @@ -105,7 +79,7 @@ "oldfieldtype": "Data", "permlevel": 0, "precision": "", - "print_hide": 1, + "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, @@ -474,7 +448,7 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -624,7 +598,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-03-03 13:42:11.020082", + "modified": "2016-03-25 01:14:38.490488", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation Item", diff --git a/erpnext/buying/doctype/rfq_supplier/rfq_supplier.json b/erpnext/buying/doctype/rfq_supplier/rfq_supplier.json index b4a3f6884b..02a6f0ba33 100644 --- a/erpnext/buying/doctype/rfq_supplier/rfq_supplier.json +++ b/erpnext/buying/doctype/rfq_supplier/rfq_supplier.json @@ -29,6 +29,83 @@ "print_hide_if_no_value": 0, "read_only": 0, "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "contact_person", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Contact Person", + "length": 0, + "no_copy": 1, + "options": "Contact", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "default": "1", + "description": "Send Request for Quotation to Supplier", + "fieldname": "sent_email_to_supplier", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Sent Email to Supplier", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, "reqd": 0, "search_index": 0, "set_only_once": 0, @@ -45,7 +122,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_list_view": 1, - "label": "Name", + "label": "Supplier Name", "length": 0, "no_copy": 0, "options": "supplier.supplier_name", @@ -59,6 +136,58 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "email_id", + "fieldtype": "Read Only", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Email Id", + "length": 0, + "no_copy": 1, + "options": "contact_person.email_id", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "fieldname": "user_id", + "fieldtype": "Read Only", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "User Id", + "length": 0, + "no_copy": 1, + "options": "contact_person.user", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "hide_heading": 0, @@ -70,7 +199,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-03-03 12:52:45.937140", + "modified": "2016-03-25 13:18:11.017660", "modified_by": "Administrator", "module": "Buying", "name": "RFQ Supplier", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index e12c9fff6f..c9bcc46214 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -1885,6 +1885,26 @@ "share": 0, "submit": 0, "write": 1 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 1, + "print": 0, + "read": 1, + "report": 0, + "role": "All", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 } ], "read_only": 0, diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json index 2999b07120..1275fcdf31 100644 --- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json +++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.json @@ -879,6 +879,32 @@ "unique": 0, "width": "120px" }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "fieldname": "request_for_quotation", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Request for Quotation", + "length": 0, + "no_copy": 1, + "options": "Request for Quotation", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_on_submit": 0, "bold": 0, @@ -928,32 +954,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "request_for_quotation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Request for Quotation", - "length": 0, - "no_copy": 1, - "options": "Request for Quotation", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_on_submit": 0, "bold": 0, @@ -970,9 +970,9 @@ "no_copy": 1, "permlevel": 0, "precision": "", - "print_hide": 0, + "print_hide": 1, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "report_hide": 0, "reqd": 0, "search_index": 0, @@ -1097,7 +1097,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-03-18 05:15:03.936587", + "modified": "2016-03-25 17:01:59.632826", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation Item", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index c9b660db4c..536112662d 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -20,8 +20,9 @@ class BuyingController(StockController): } def get_feed(self): - return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, - self.grand_total) + if self.get("supplier_name"): + return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, + self.grand_total) def validate(self): super(BuyingController, self).validate() @@ -40,7 +41,7 @@ class BuyingController(StockController): # set contact and address details for supplier, if they are not mentioned if getattr(self, "supplier", None): - self.update_if_missing(get_party_details(self.supplier, party_type="Supplier")) + self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions)) self.set_missing_item_details() diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index ae6e9e4362..588257828f 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -28,15 +28,14 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p filters.append((doctype, "docstatus", "=", 1)) if user != "Guest" and is_website_user(): + parties_doctype = 'RFQ Supplier' if doctype == 'Request for Quotation' else doctype # find party for this contact - customers, suppliers = get_customers_suppliers(doctype, user) + customers, suppliers = get_customers_suppliers(parties_doctype, user) + key, parties = get_party_details(customers, suppliers) - if customers: - key, parties = "customer", customers - elif suppliers: - key, parties = "supplier", suppliers - else: - key, parties = "customer", [] + if doctype == 'Request for Quotation': + if key == 'customer': frappe.throw(_("Not Permitted"), frappe.PermissionError) + return rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length) filters.append((doctype, key, "in", parties)) @@ -52,6 +51,23 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p return post_process(doctype, get_list(doctype, txt, filters, limit_start, limit_page_length, fields="name", order_by = "modified desc")) +def get_party_details(customers, suppliers): + if customers: + key, parties = "customer", customers + elif suppliers: + key, parties = "supplier", suppliers + else: + key, parties = "customer", [] + + return key, parties + +def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length): + data = frappe.db.sql("""select distinct parent as name, supplier from `tab{doctype}` + where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""". + format(doctype=parties_doctype, supplier=parties[0], start=limit_start, len = limit_page_length), as_dict=1) + + return post_process(doctype, data) + def post_process(doctype, data): result = [] for d in data: diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6e86d84b17..01104b8335 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -71,6 +71,13 @@ website_route_rules = [ "parents": [{"title": _("Shipments"), "name": "shipments"}] } }, + {"from_route": "/rfq", "to_route": "Request for Quotation"}, + {"from_route": "/rfq/", "to_route": "rfq", + "defaults": { + "doctype": "Request for Quotation", + "parents": [{"title": _("Request for Quotation"), "name": "rfq"}] + } + }, {"from_route": "/jobs", "to_route": "Job Opening"}, ] diff --git a/erpnext/shopping_cart/utils.py b/erpnext/shopping_cart/utils.py index 3e1afe27fa..7cd269d0ce 100644 --- a/erpnext/shopping_cart/utils.py +++ b/erpnext/shopping_cart/utils.py @@ -16,6 +16,8 @@ def show_cart_count(): return False def set_cart_count(login_manager): + role, parties = check_customer_or_supplier() + if role == 'Supplier': return if show_cart_count(): from erpnext.shopping_cart.cart import set_cart_count set_cart_count() @@ -29,6 +31,19 @@ def update_website_context(context): context["shopping_cart_enabled"] = cart_enabled def update_my_account_context(context): + check_user_role, parties = check_customer_or_supplier() + + if check_user_role == 'Supplier': + get_supplier_context(context) + else: + get_customer_context(context) + +def get_supplier_context(context): + context["my_account_list"].extend([ + {"label": _("Request for Quotations"), "url": "rfq"}, + ]) + +def get_customer_context(context): context["my_account_list"].extend([ {"label": _("Projects"), "url": "project"}, {"label": _("Orders"), "url": "orders"}, @@ -37,3 +52,15 @@ def update_my_account_context(context): {"label": _("Issues"), "url": "issues"}, {"label": _("Addresses"), "url": "addresses"} ]) + +def check_customer_or_supplier(): + if frappe.session.user: + contacts = frappe.get_all("Contact", fields=["customer", "supplier", "email_id"], + filters={"email_id": frappe.session.user}) + + customer = [d.customer for d in contacts if d.customer] or None + supplier = [d.supplier for d in contacts if d.supplier] or None + + if customer: return 'Customer', customer + if supplier : return 'Supplier', supplier + return 'Customer', None \ No newline at end of file diff --git a/erpnext/startup/notifications.py b/erpnext/startup/notifications.py index 75553122c8..75e55b51f6 100644 --- a/erpnext/startup/notifications.py +++ b/erpnext/startup/notifications.py @@ -32,6 +32,9 @@ def get_notification_config(): "status": ("not in", ("Stopped",)), "per_ordered": ("<", 100) }, + "Request for Quotation": { + "docstatus": 0 + }, "Purchase Order": { "status": ("not in", ("Completed", "Closed")), "docstatus": ("<", 2) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 72f2060ca7..982534de6b 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -48,8 +48,10 @@ erpnext.buying.MaterialRequestController = erpnext.buying.BuyingController.exten if(doc.material_request_type === "Purchase") cur_frm.add_custom_button(__('Purchase Order'), this.make_purchase_order, __("Make")); + + if(doc.material_request_type === "Purchase") cur_frm.add_custom_button(__("Request for Quotation"), - this.make_request_for_quotation, __("Make")) + this.make_request_for_quotation, __("Make")); if(doc.material_request_type === "Purchase") cur_frm.add_custom_button(__("Supplier Quotation"), diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html new file mode 100644 index 0000000000..91bdd6b782 --- /dev/null +++ b/erpnext/templates/emails/request_for_quotation.html @@ -0,0 +1,12 @@ +

{{_("Request for Quotation")}}

+
+

{{ message }}

+{% if update_password_link %} +

{{_("Please click on the following link to set your new password")}}:

+

{{ update_password_link }}

+{% else %} +

{{_("Request for quotation can be access by clicking following link")}}:

+

{{ rfq_link }}

+{% endif %} +

{{_("Thank you")}},
+{{ user_fullname }}

\ No newline at end of file diff --git a/erpnext/templates/includes/rfq.js b/erpnext/templates/includes/rfq.js new file mode 100644 index 0000000000..3623d77932 --- /dev/null +++ b/erpnext/templates/includes/rfq.js @@ -0,0 +1,84 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +window.doc={{ doc.as_json() }}; + +$(document).ready(function() { + new rfq(); + doc.supplier = "{{ doc.supplier }}" +}); + +rfq = Class.extend({ + init: function(){ + this.onfocus_select_all(); + this.change_qty(); + this.change_rate(); + this.terms(); + this.submit_rfq(); + }, + + onfocus_select_all: function(){ + $("input").click(function(){ + $(this).select(); + }) + }, + + change_qty: function(){ + var me = this; + $('.rfq-items').on("change", ".rfq-qty", function(){ + me.idx = parseFloat($(this).attr('data-idx')); + me.qty = parseFloat($(this).val()); + me.rate = parseFloat($(repl('.rfq-rate[data-idx=%(idx)s]',{'idx': me.idx})).val()); + me.update_qty_rate(); + }) + }, + + change_rate: function(){ + var me = this; + $(".rfq-items").on("change", ".rfq-rate", function(){ + me.idx = parseFloat($(this).attr('data-idx')); + me.rate = parseFloat($(this).val()); + me.qty = parseFloat($(repl('.rfq-qty[data-idx=%(idx)s]',{'idx': me.idx})).val()); + me.update_qty_rate(); + }) + }, + + terms: function(){ + $(".terms").on("change", ".terms-feedback", function(){ + doc.terms = $(this).val(); + }) + }, + + update_qty_rate: function(){ + var me = this; + doc.grand_total = 0.0; + $.each(doc.items, function(idx, data){ + if(data.idx == me.idx){ + data.qty = me.qty; + data.rate = me.rate; + data.amount = (me.rate * me.qty) || 0.0; + $(repl('.rfq-amount[data-idx=%(idx)s]',{'idx': me.idx})).text(data.amount.toFixed(2)); + } + + doc.grand_total += flt(data.amount); + $('.tax-grand-total').text(doc.grand_total.toFixed(2)); + }) + }, + + submit_rfq: function(){ + $('.btn-sm').click(function(){ + frappe.freeze(); + frappe.call({ + type: "POST", + method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.create_supplier_quotation", + args: { + doc: doc + }, + btn: this, + callback: function(r){ + frappe.unfreeze(); + } + }) + }) + } +}) diff --git a/erpnext/templates/includes/rfq/rfq_items.html b/erpnext/templates/includes/rfq/rfq_items.html new file mode 100644 index 0000000000..de9a95fdaa --- /dev/null +++ b/erpnext/templates/includes/rfq/rfq_items.html @@ -0,0 +1,30 @@ +{% from "erpnext/templates/includes/rfq/rfq_macros.html" import item_name_and_description %} + +{% for d in doc.items %} +
+
+
+ {{ item_name_and_description(d, doc) }} +
+ +
+ +

+ {{_("UOM") + ": "+ d.uom}} +

+
+
+ +
+
+ 0.00 +
+
+
+{% endfor %} \ No newline at end of file diff --git a/erpnext/templates/includes/rfq/rfq_macros.html b/erpnext/templates/includes/rfq/rfq_macros.html new file mode 100644 index 0000000000..95bbcfec3f --- /dev/null +++ b/erpnext/templates/includes/rfq/rfq_macros.html @@ -0,0 +1,21 @@ +{% from "erpnext/templates/includes/macros.html" import product_image_square %} + +{% macro item_name_and_description(d, doc) %} +
+
+
+ {{ product_image_square(d.image) }} +
+
+
+ {{ d.item_code }} +

{{ d.description }}

+ {% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %} +

+ {% if supplier_part_no %} + {{_("Supplier Part No") + ": "+ supplier_part_no}} + {% endif %} +

+
+
+{% endmacro %} diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html new file mode 100644 index 0000000000..5556d26d96 --- /dev/null +++ b/erpnext/templates/pages/rfq.html @@ -0,0 +1,83 @@ +{% extends "templates/web.html" %} + +{% block header %} +

{{ doc.name }}

+{% endblock %} + +{% block script %} + +{% endblock %} + +{% block breadcrumbs %} + {% include "templates/includes/breadcrumbs.html" %} +{% endblock %} + +{% block style %} + +{% endblock %} + +{% block header_actions %} +{% if doc.items %} + +{% endif %} +{% endblock %} + +{% block page_content %} +
+
+
{{ doc.supplier }}
+
+
+ {{ doc.get_formatted("transaction_date") }} +
+
+ +
+
+
+
+
+ Items +
+
+ Qty +
+
+ Rate +
+
+ Amount +
+
+
+ {% if doc.items %} +
+ {% include "templates/includes/rfq/rfq_items.html" %} +
+ {% endif %} +
+ {% if doc.items %} +
+
{{ _("Grand Total") }}
+
+ 0.0 +
+
+ {% endif %} +
+
{{ _("Terms and Conditions: ") }}
+
+
+
+ +
+
+
+
+ + +{% endblock %} diff --git a/erpnext/templates/pages/rfq.py b/erpnext/templates/pages/rfq.py new file mode 100644 index 0000000000..fbdd6dbd96 --- /dev/null +++ b/erpnext/templates/pages/rfq.py @@ -0,0 +1,31 @@ +# 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 _ + +def get_context(context): + context.no_cache = 1 + context.doc = frappe.get_doc(frappe.form_dict.doctype, frappe.form_dict.name) + context.parents = frappe.form_dict.parents + context.doc.supplier = get_supplier() + unauthrized_user(context.doc.supplier) + context["title"] = frappe.form_dict.name + +def unauthrized_user(supplier): + status = check_supplier_has_docname_access(supplier) + if status == False: + frappe.throw(_("Not Permitted"), frappe.PermissionError) + +def get_supplier(): + from erpnext.shopping_cart.utils import check_customer_or_supplier + key, parties = check_customer_or_supplier() + return parties[0] if key == 'Supplier' else '' + +def check_supplier_has_docname_access(supplier): + status = True + if frappe.form_dict.name not in frappe.db.sql_list("""select parent from `tabRFQ Supplier` + where supplier = '{supplier}'""".format(supplier=supplier)): + status = False + return status