From e176c3c1d1dc138f5155a979002e7ef92a7daa67 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 3 Dec 2019 16:56:48 +0530 Subject: [PATCH 1/8] feat: Receivable / payable summary based on payment terms --- .../accounts_payable_summary/accounts_payable_summary.js | 5 +++++ .../accounts_receivable_summary.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 5f0fdc9f2c..4a9f1b0dc4 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -88,6 +88,11 @@ frappe.query_reports["Accounts Payable Summary"] = { "label": __("Supplier Group"), "fieldtype": "Link", "options": "Supplier Group" + }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", } ], diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 0120608a8f..d54824b685 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -106,6 +106,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { "label": __("Sales Person"), "fieldtype": "Link", "options": "Sales Person" + }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", } ], From 51f2131a41b4c6cb57241aa8b35cc170287efac0 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Wed, 4 Dec 2019 11:56:44 +0530 Subject: [PATCH 2/8] fix: joining and relieving Date can be on same date as valid use case on emoloyee form (#19798) * fix: date validation on inpatient record, else condition removing on clinical prcd templ which is not req * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix: joining and relieving Date can be on same date as valid use case * Update employee.py --- erpnext/hr/doctype/employee/employee.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 2f88e1e363..242531bd17 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -152,8 +152,8 @@ class Employee(NestedSet): elif self.date_of_retirement and self.date_of_joining and (getdate(self.date_of_retirement) <= getdate(self.date_of_joining)): throw(_("Date Of Retirement must be greater than Date of Joining")) - elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) <= getdate(self.date_of_joining)): - throw(_("Relieving Date must be greater than Date of Joining")) + elif self.relieving_date and self.date_of_joining and (getdate(self.relieving_date) < getdate(self.date_of_joining)): + throw(_("Relieving Date must be greater than or equal to Date of Joining")) elif self.contract_end_date and self.date_of_joining and (getdate(self.contract_end_date) <= getdate(self.date_of_joining)): throw(_("Contract End Date must be greater than Date of Joining")) From eb9c3e1c5b68cd348b35b91dcf392e6ebb05480e Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Wed, 4 Dec 2019 12:01:31 +0530 Subject: [PATCH 3/8] fix(patch): set proper tax_type and proper account (#19794) --- .../move_item_tax_to_item_tax_template.py | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index f25b9eaf52..e47344bd92 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -62,12 +62,12 @@ def execute(): ] for dt in doctypes: - for d in frappe.db.sql("""select name, parent, item_code, item_tax_rate from `tab{0} Item` + for d in frappe.db.sql("""select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item` where ifnull(item_tax_rate, '') not in ('', '{{}}') and item_tax_template is NULL""".format(dt), as_dict=1): item_tax_map = json.loads(d.item_tax_rate) item_tax_template_name = get_item_tax_template(item_tax_templates, - item_tax_map, d.item_code, d.parent) + item_tax_map, d.item_code, d.parenttype, d.parent) frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name) frappe.db.auto_commit_on_many_writes = False @@ -77,7 +77,7 @@ def execute(): settings.determine_address_tax_category_from = "Billing Address" settings.save() -def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=None): +def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None): # search for previously created item tax template by comparing tax maps for template, item_tax_template_map in iteritems(item_tax_templates): if item_tax_map == item_tax_template_map: @@ -88,23 +88,44 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=No item_tax_template.title = make_autoname("Item Tax Template-.####") for tax_type, tax_rate in iteritems(item_tax_map): - if not frappe.db.exists("Account", tax_type): + account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type'], as_dict=1) + if account_details: + if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): + frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable') + else: parts = tax_type.strip().split(" - ") account_name = " - ".join(parts[:-1]) - company = frappe.db.get_value("Company", filters={"abbr": parts[-1]}) + company = get_company(parts[-1], parenttype, parent) parent_account = frappe.db.get_value("Account", filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account") - - frappe.get_doc({ - "doctype": "Account", + filters = { "account_name": account_name, - "company": company, - "account_type": "Tax", - "parent_account": parent_account - }).insert() + "company": company, + "account_type": "Tax", + "parent_account": parent_account + } + tax_type = frappe.db.get_value("Account", filters) + if not tax_type: + account = frappe.new_doc("Account") + account.update(filters) + account.insert() + tax_type = account.name item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate}) item_tax_templates.setdefault(item_tax_template.title, {}) item_tax_templates[item_tax_template.title][tax_type] = tax_rate item_tax_template.save() return item_tax_template.name + +def get_company(company_abbr, parenttype=None, parent=None): + if parenttype and parent: + company = frappe.get_cached_value(parenttype, parent, 'company') + else: + company = frappe.db.get_value("Company", filters={"abbr": company_abbr}) + + if not company: + companies = frappe.get_all('Company') + if len(companies) == 1: + company = companies[0].name + + return company From a830f89a59ab5a1072840eabdfd91763cb22565a Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 4 Dec 2019 13:32:31 +0530 Subject: [PATCH 4/8] feat: add date filter in the fixed asset register (#19799) * feat: add date filter in the fixed asset register * fix: remove function from keyword argument --- .../fixed_asset_register.js | 6 ++++ .../fixed_asset_register.py | 29 ++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 426caaad92..8c737d066b 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -26,5 +26,11 @@ frappe.query_reports["Fixed Asset Register"] = { fieldtype: "Link", options: "Finance Book" }, + { + fieldname:"date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today() + }, ] }; diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index f395499ad6..57b68b4ed2 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cstr +from frappe.utils import cstr, today, flt def execute(filters=None): filters = frappe._dict(filters or {}) @@ -86,8 +86,8 @@ def get_columns(filters): "width": 90 }, { - "label": _("Current Value"), - "fieldname": "current_value", + "label": _("Asset Value"), + "fieldname": "asset_value", "options": "Currency", "width": 90 }, @@ -114,7 +114,7 @@ def get_data(filters): data = [] conditions = get_conditions(filters) - current_value_map = get_finance_book_value_map(filters.finance_book) + depreciation_amount_map = get_finance_book_value_map(filters.date, filters.finance_book) pr_supplier_map = get_purchase_receipt_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map() @@ -125,7 +125,9 @@ def get_data(filters): "available_for_use_date", "status", "purchase_invoice"]) for asset in assets_record: - if current_value_map.get(asset.name) is not None: + asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ + - flt(depreciation_amount_map.get(asset.name)) + if asset_value: row = { "asset_id": asset.name, "asset_name": asset.asset_name, @@ -138,19 +140,24 @@ def get_data(filters): "location": asset.location, "asset_category": asset.asset_category, "purchase_date": asset.purchase_date, - "current_value": current_value_map.get(asset.name) + "asset_value": asset_value } data.append(row) return data -def get_finance_book_value_map(finance_book=''): +def get_finance_book_value_map(date, finance_book=''): + if not date: + date = today() return frappe._dict(frappe.db.sql(''' Select - parent, value_after_depreciation - FROM `tabAsset Finance Book` + parent, SUM(depreciation_amount) + FROM `tabDepreciation Schedule` WHERE - parentfield='finance_books' - AND ifnull(finance_book, '')=%s''', cstr(finance_book))) + parentfield='schedules' + AND schedule_date<=%s + AND journal_entry IS NOT NULL + AND ifnull(finance_book, '')=%s + GROUP BY parent''', (date, cstr(finance_book)))) def get_purchase_receipt_supplier_map(): return frappe._dict(frappe.db.sql(''' Select From acdd5081da0465a436c86bab30449b79a6d8a8d5 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 4 Dec 2019 15:30:01 +0530 Subject: [PATCH 5/8] fix: Service start and end date validation for deferred accounting (#19805) --- .../purchase_invoice_item.json | 13 +++++++++---- .../sales_invoice_item/sales_invoice_item.json | 13 +++++++++---- erpnext/controllers/accounts_controller.py | 13 ++++++++++++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 27d8233a44..acb0398b5c 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -507,7 +508,8 @@ "depends_on": "enable_deferred_expense", "fieldname": "service_stop_date", "fieldtype": "Date", - "label": "Service Stop Date" + "label": "Service Stop Date", + "no_copy": 1 }, { "default": "0", @@ -523,13 +525,15 @@ "depends_on": "enable_deferred_expense", "fieldname": "service_start_date", "fieldtype": "Date", - "label": "Service Start Date" + "label": "Service Start Date", + "no_copy": 1 }, { "depends_on": "enable_deferred_expense", "fieldname": "service_end_date", "fieldtype": "Date", - "label": "Service End Date" + "label": "Service End Date", + "no_copy": 1 }, { "fieldname": "reference", @@ -766,7 +770,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-21 16:27:52.043744", + "links": [], + "modified": "2019-12-04 12:23:17.046413", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 779ac4f656..b2294e4318 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-06-04 11:02:19", "doctype": "DocType", @@ -484,7 +485,8 @@ "depends_on": "enable_deferred_revenue", "fieldname": "service_stop_date", "fieldtype": "Date", - "label": "Service Stop Date" + "label": "Service Stop Date", + "no_copy": 1 }, { "default": "0", @@ -500,13 +502,15 @@ "depends_on": "enable_deferred_revenue", "fieldname": "service_start_date", "fieldtype": "Date", - "label": "Service Start Date" + "label": "Service Start Date", + "no_copy": 1 }, { "depends_on": "enable_deferred_revenue", "fieldname": "service_end_date", "fieldtype": "Date", - "label": "Service End Date" + "label": "Service End Date", + "no_copy": 1 }, { "collapsible": 1, @@ -783,7 +787,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-07-16 16:36:46.527606", + "links": [], + "modified": "2019-12-04 12:22:38.517710", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4e0dd6f1e6..75564afe59 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -61,7 +61,6 @@ class AccountsController(TransactionBase): _('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1) def validate(self): - if not self.get('is_return'): self.validate_qty_is_not_zero() @@ -100,11 +99,23 @@ class AccountsController(TransactionBase): if self.is_return: self.validate_qty() + else: + self.validate_deferred_start_and_end_date() validate_regional(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) + def validate_deferred_start_and_end_date(self): + for d in self.items: + if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): + if not (d.service_start_date and d.service_end_date): + frappe.throw(_("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx)) + elif getdate(d.service_start_date) > getdate(d.service_end_date): + frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) + elif getdate(self.posting_date) > getdate(d.service_end_date): + frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) + def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() self.set_due_date() From d285e9b7bd428c79edaa79bb7ee852d425f9b5c4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 4 Dec 2019 15:30:58 +0530 Subject: [PATCH 6/8] optimize: Optimization of Receivable report filtered based on sales person (#19796) --- .../accounts_receivable.py | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 41989bf863..2c53f6e997 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -60,6 +60,7 @@ class ReceivablePayableReport(object): def get_data(self): self.get_gl_entries() + self.get_sales_invoices_or_customers_based_on_sales_person() self.voucher_balance = OrderedDict() self.init_voucher_balance() # invoiced, paid, credit_note, outstanding @@ -103,12 +104,18 @@ class ReceivablePayableReport(object): def get_invoices(self, gle): if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): - self.invoices.add(gle.voucher_no) + if self.filters.get("sales_person"): + if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \ + or gle.party in self.sales_person_records.get("Customer", []): + self.invoices.add(gle.voucher_no) + else: + self.invoices.add(gle.voucher_no) def update_voucher_balance(self, gle): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance row = self.get_voucher_balance(gle) + if not row: return # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) @@ -129,8 +136,13 @@ class ReceivablePayableReport(object): row.paid -= gle_balance def get_voucher_balance(self, gle): - voucher_balance = None + if self.filters.get("sales_person"): + against_voucher = gle.against_voucher or gle.voucher_no + if not (gle.party in self.sales_person_records.get("Customer", []) or \ + against_voucher in self.sales_person_records.get("Sales Invoice", [])): + return + voucher_balance = None if gle.against_voucher: # find invoice against_voucher = gle.against_voucher @@ -512,6 +524,22 @@ class ReceivablePayableReport(object): order by posting_date, party""" .format(select_fields, conditions), values, as_dict=True) + def get_sales_invoices_or_customers_based_on_sales_person(self): + if self.filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", + self.filters.get("sales_person"), ["lft", "rgt"]) + + records = frappe.db.sql(""" + select distinct parent, parenttype + from `tabSales Team` steam + where parenttype in ('Customer', 'Sales Invoice') + and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) + """, (lft, rgt), as_dict=1) + + self.sales_person_records = frappe._dict() + for d in records: + self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent) + def prepare_conditions(self): conditions = [""] values = [self.party_type, self.filters.report_date] @@ -564,16 +592,6 @@ class ReceivablePayableReport(object): conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)") values.append(self.filters.get("sales_partner")) - if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) - - conditions.append("""exists(select name from `tabSales Team` steam where - steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) - and ((steam.parent = voucher_no and steam.parenttype = voucher_type) - or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) - def add_supplier_filters(self, conditions, values): if self.filters.get("supplier_group"): conditions.append("""party in (select name from tabSupplier From 114e0db4190cb12964055fcc2fed76c1d7cf49ad Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Wed, 4 Dec 2019 10:01:43 +0000 Subject: [PATCH 7/8] fix: query for finding lost quotation (#19800) * fix: query for finding lost quotation * Update opportunity.py --- erpnext/crm/doctype/opportunity/opportunity.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 99486fa206..2880c8050e 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -130,10 +130,10 @@ class Opportunity(TransactionBase): def has_lost_quotation(self): lost_quotation = frappe.db.sql(""" - select q.name - from `tabQuotation` q, `tabQuotation Item` qi - where q.name = qi.parent and q.docstatus=1 - and qi.prevdoc_docname =%s and q.status = 'Lost' + select name + from `tabQuotation` + where docstatus=1 + and opportunity =%s and status = 'Lost' """, self.name) if lost_quotation: if self.has_active_quotation(): From 370cdc017022252bf6a5faaee8abc9f8d7dca23f Mon Sep 17 00:00:00 2001 From: ronelvcabrera <44422325+ronelvcabrera@users.noreply.github.com> Date: Wed, 4 Dec 2019 18:37:11 +0800 Subject: [PATCH 8/8] feat(Sales/Purchase Order): optional to reference a Blanket Order (#19612) --- .../purchase_order/test_purchase_order.py | 26 ++++++++++++++++++- .../purchase_order_item.json | 12 ++++----- .../doctype/blanket_order/blanket_order.py | 4 +++ erpnext/public/js/controllers/transaction.js | 8 ++++++ .../doctype/sales_order/test_sales_order.py | 23 +++++++++++++++- .../sales_order_item/sales_order_item.json | 11 +++++++- erpnext/stock/get_item_details.py | 13 ++++++---- 7 files changed, 83 insertions(+), 14 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index a0a1e8ed5c..08f5d8b4d0 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -17,6 +17,8 @@ from erpnext.stock.doctype.material_request.material_request import make_purchas from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.controllers.status_updater import OverAllowanceError +from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order + class TestPurchaseOrder(unittest.TestCase): def test_make_purchase_receipt(self): @@ -620,6 +622,27 @@ class TestPurchaseOrder(unittest.TestCase): po.save() self.assertEqual(po.schedule_date, add_days(nowdate(), 2)) + + def test_po_optional_blanket_order(self): + """ + Expected result: Blanket order Ordered Quantity should only be affected on Purchase Order with against_blanket_order = 1. + Second Purchase Order should not add on to Blanket Orders Ordered Quantity. + """ + + bo = make_blanket_order(blanket_order_type = "Purchasing", quantity = 10, rate = 10) + + po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) + po_doc = frappe.get_doc('Purchase Order', po.get('name')) + # To test if the PO has a Blanket Order + self.assertTrue(po_doc.items[0].blanket_order) + + po = create_purchase_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) + po_doc = frappe.get_doc('Purchase Order', po.get('name')) + # To test if the PO does NOT have a Blanket Order + self.assertEqual(po_doc.items[0].blanket_order, None) + + + def make_pr_against_po(po, received_qty=0): pr = make_purchase_receipt(po) @@ -693,7 +716,8 @@ def create_purchase_order(**args): "qty": args.qty or 10, "rate": args.rate or 500, "schedule_date": add_days(nowdate(), 1), - "include_exploded_items": args.get('include_exploded_items', 1) + "include_exploded_items": args.get('include_exploded_items', 1), + "against_blanket_order": args.against_blanket_order }) if not args.do_not_save: po.insert() diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index c409c1f46e..15bc97c2a4 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -43,7 +43,6 @@ "base_amount", "pricing_rules", "is_free_item", - "is_fixed_asset", "section_break_29", "net_rate", "net_amount", @@ -67,6 +66,7 @@ "supplier_quotation", "supplier_quotation_item", "col_break5", + "against_blanket_order", "blanket_order", "blanket_order_rate", "item_group", @@ -511,6 +511,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", @@ -518,6 +519,7 @@ "options": "Blanket Order" }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", @@ -703,16 +705,14 @@ }, { "default": "0", - "fetch_from": "item_code.is_fixed_asset", - "fieldname": "is_fixed_asset", + "fieldname": "against_blanket_order", "fieldtype": "Check", - "label": "Is Fixed Asset", - "read_only": 1 + "label": "Against Blanket Order" } ], "idx": 1, "istable": 1, - "modified": "2019-11-07 17:19:12.090355", + "modified": "2019-11-19 14:10:52.865006", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index faed707d60..38118bd78d 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -44,6 +44,8 @@ def make_sales_order(source_name): target.item_name = item.get("item_name") target.description = item.get("description") target.uom = item.get("stock_uom") + target.against_blanket_order = 1 + target.blanket_order = source_name target_doc = get_mapped_doc("Blanket Order", source_name, { "Blanket Order": { @@ -71,6 +73,8 @@ def make_purchase_order(source_name): target.description = item.get("description") target.uom = item.get("stock_uom") target.warehouse = item.get("default_warehouse") + target.against_blanket_order = 1 + target.blanket_order = source_name target_doc = get_mapped_doc("Blanket Order", source_name, { "Blanket Order": { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5da949320a..46a58fba7c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1716,6 +1716,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + against_blanket_order: function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + if(!item.against_blanket_order) { + frappe.model.set_value(this.frm.doctype + " Item", item.name, "blanket_order", null); + frappe.model.set_value(this.frm.doctype + " Item", item.name, "blanket_order_rate", 0.00); + } + }, + blanket_order: function(doc, cdt, cdn) { var me = this; var item = locals[cdt][cdn]; diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index bd07841488..feb6b76c4d 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -12,6 +12,7 @@ from erpnext.selling.doctype.sales_order.sales_order import make_work_orders from erpnext.controllers.accounts_controller import update_child_qty_rate import json from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request +from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order class TestSalesOrder(unittest.TestCase): def tearDown(self): @@ -819,6 +820,25 @@ class TestSalesOrder(unittest.TestCase): mr_doc = frappe.get_doc('Material Request',mr.get('name')) self.assertEqual(mr_doc.items[0].sales_order, so.name) + def test_so_optional_blanket_order(self): + """ + Expected result: Blanket order Ordered Quantity should only be affected on Sales Order with against_blanket_order = 1. + Second Sales Order should not add on to Blanket Orders Ordered Quantity. + """ + + bo = make_blanket_order(blanket_order_type = "Selling", quantity = 10, rate = 10) + + so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 1) + so_doc = frappe.get_doc('Sales Order', so.get('name')) + # To test if the SO has a Blanket Order + self.assertTrue(so_doc.items[0].blanket_order) + + so = make_sales_order(item_code= "_Test Item", qty = 5, against_blanket_order = 0) + so_doc = frappe.get_doc('Sales Order', so.get('name')) + # To test if the SO does NOT have a Blanket Order + self.assertEqual(so_doc.items[0].blanket_order, None) + + def make_sales_order(**args): so = frappe.new_doc("Sales Order") args = frappe._dict(args) @@ -845,7 +865,8 @@ def make_sales_order(**args): "warehouse": args.warehouse, "qty": args.qty or 10, "uom": args.uom or None, - "rate": args.rate or 100 + "rate": args.rate or 100, + "against_blanket_order": args.against_blanket_order }) so.delivery_date = add_days(so.transaction_date, 10) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 3fd1e6461e..86b09c2814 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -68,6 +68,7 @@ "target_warehouse", "prevdoc_docname", "col_break4", + "against_blanket_order", "blanket_order", "blanket_order_rate", "planning_section", @@ -574,6 +575,7 @@ "report_hide": 1 }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", @@ -581,6 +583,7 @@ "options": "Blanket Order" }, { + "depends_on": "eval:doc.against_blanket_order", "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", @@ -741,11 +744,17 @@ "fieldname": "image_section", "fieldtype": "Section Break", "label": "Image" + }, + { + "default": "0", + "fieldname": "against_blanket_order", + "fieldtype": "Check", + "label": "Against Blanket Order" } ], "idx": 1, "istable": 1, - "modified": "2019-10-10 08:46:26.244823", + "modified": "2019-11-19 14:19:29.491945", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 55f4be136b..76644ed846 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -213,7 +213,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): project: "", qty: "", stock_qty: "", - conversion_factor: "" + conversion_factor: "", + against_blanket_order: 0/1 } :param item: `item_code` of Item object :return: frappe._dict @@ -302,7 +303,8 @@ def get_basic_details(args, item, overwrite_warehouse=True): "weight_per_unit":item.weight_per_unit, "weight_uom":item.weight_uom, "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0, - "transaction_date": args.get("transaction_date") + "transaction_date": args.get("transaction_date"), + "against_blanket_order": args.get("against_blanket_order") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): @@ -996,9 +998,10 @@ def get_serial_no(args, serial_nos=None, sales_order=None): def update_party_blanket_order(args, out): - blanket_order_details = get_blanket_order_details(args) - if blanket_order_details: - out.update(blanket_order_details) + if out["against_blanket_order"]: + blanket_order_details = get_blanket_order_details(args) + if blanket_order_details: + out.update(blanket_order_details) @frappe.whitelist() def get_blanket_order_details(args):