From 66806ff1b067e9f0dee38456f3e33f777ad7f078 Mon Sep 17 00:00:00 2001 From: Poranut Chollavorn Date: Thu, 18 Jun 2020 13:43:15 +0000 Subject: [PATCH 01/41] fix(pricing_rules): rule won't got use across [item_code, item_group, brands] --- erpnext/accounts/doctype/pricing_rule/utils.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 9876246c47b..fa43e70c2d5 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -30,13 +30,19 @@ apply_on_table = { } def get_pricing_rules(args, doc=None): - pricing_rules = [] - values = {} + pricing_rules_all = [] + values = {} for apply_on in ['Item Code', 'Item Group', 'Brand']: - pricing_rules.extend(_get_pricing_rules(apply_on, args, values)) - if pricing_rules and not apply_multiple_pricing_rules(pricing_rules): - break + pricing_rules_all.extend(_get_pricing_rules(apply_on, args, values)) + + # removing duplicate pricing rule + pricing_rules_title = [] + pricing_rules = [] + for p in pricing_rules_all: + if p['title'] not in pricing_rules_title: + pricing_rules_title.append(p['title']) + pricing_rules.append(p) rules = [] From 850c0ff1212adaa416c861ae1f734a6d11115821 Mon Sep 17 00:00:00 2001 From: Poranut Chollavorn Date: Mon, 22 Jun 2020 07:24:33 +0000 Subject: [PATCH 02/41] fix(pricing_rule): key error on apply_internal_priority apply_internal_priority never check if pricing rule have field --- erpnext/accounts/doctype/pricing_rule/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index fa43e70c2d5..238b37db356 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -328,7 +328,10 @@ def apply_internal_priority(pricing_rules, field_set, args): filtered_rules = [] for field in field_set: if args.get(field): - filtered_rules = filter(lambda x: x[field]==args[field], pricing_rules) + for rule in pricing_rules: + if rule.get(field) == args.get(field): + filtered_rules = [rule] + break if filtered_rules: break return filtered_rules or pricing_rules From 303639db764672ba9347475419c347aad6d06e02 Mon Sep 17 00:00:00 2001 From: Poranut Chollavorn Date: Mon, 22 Jun 2020 10:24:40 +0000 Subject: [PATCH 03/41] fix: use name instead of title --- erpnext/accounts/doctype/pricing_rule/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 238b37db356..f30d0bcaa62 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -37,11 +37,11 @@ def get_pricing_rules(args, doc=None): pricing_rules_all.extend(_get_pricing_rules(apply_on, args, values)) # removing duplicate pricing rule - pricing_rules_title = [] + pricing_rules_name = [] pricing_rules = [] for p in pricing_rules_all: - if p['title'] not in pricing_rules_title: - pricing_rules_title.append(p['title']) + if p['name'] not in pricing_rules_name: + pricing_rules_name.append(p['name']) pricing_rules.append(p) rules = [] From 31a887f045fafe327df54d311d254165baaa7796 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Sep 2020 11:23:35 +0530 Subject: [PATCH 04/41] fix: Taxes not getting fetched from Opportunity to Quotation --- erpnext/controllers/selling_controller.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 1399654ffd2..3ebb12541ab 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -10,6 +10,7 @@ from erpnext.stock.utils import get_incoming_rate from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.doctype.item.item import set_item_default from frappe.contacts.doctype.address.address import get_address_display +from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.stock_controller import StockController @@ -53,10 +54,10 @@ class SellingController(StockController): super(SellingController, self).set_missing_values(for_validate) # set contact and address details for customer, if they are not mentioned - self.set_missing_lead_customer_details() + self.set_missing_lead_customer_details(for_validate=for_validate) self.set_price_list_and_item_details(for_validate=for_validate) - def set_missing_lead_customer_details(self): + def set_missing_lead_customer_details(self, for_validate=False): customer, lead = None, None if getattr(self, "customer", None): customer = self.customer @@ -93,6 +94,11 @@ class SellingController(StockController): posting_date=self.get('transaction_date') or self.get('posting_date'), company=self.company)) + if self.get('taxes_and_charges') and not self.get('taxes') and not for_validate: + taxes = get_taxes_and_charges('Sales Taxes and Charges Template', self.taxes_and_charges) + for tax in taxes: + self.append('taxes', tax) + def set_price_list_and_item_details(self, for_validate=False): self.set_price_list_currency("Selling") self.set_missing_item_details(for_validate=for_validate) From 9a7e433a9d97190e44615a291f7137b420a0ede4 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 22 Sep 2020 13:00:09 +0530 Subject: [PATCH 05/41] fix: quality procedure parent --- .../doctype/quality_procedure/quality_procedure.js | 8 ++++++++ .../doctype/quality_procedure/quality_procedure.json | 8 +++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js index cf2644e0053..ac876229ecb 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.js @@ -10,5 +10,13 @@ frappe.ui.form.on('Quality Procedure', { } }; }); + + frm.set_query('parent_quality_procedure', function(){ + return { + filters: { + is_group: 1 + } + }; + }); } }); \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json index b3c0d948909..1dc4660c44d 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json @@ -1,5 +1,4 @@ { - "actions": [], "allow_rename": 1, "autoname": "format:PRC-{quality_procedure_name}", "creation": "2018-10-06 00:06:29.756804", @@ -18,11 +17,11 @@ ], "fields": [ { + "depends_on": "eval: doc.is_group == 0", "fieldname": "parent_quality_procedure", "fieldtype": "Link", "label": "Parent Procedure", - "options": "Quality Procedure", - "read_only": 1 + "options": "Quality Procedure" }, { "default": "0", @@ -72,8 +71,7 @@ } ], "is_tree": 1, - "links": [], - "modified": "2020-06-17 17:25:03.434953", + "modified": "2020-09-22 12:56:50.700777", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure", From 560f513d6987c9731b38f969ce4d9e9bb76089cf Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 25 Sep 2020 01:12:31 +0530 Subject: [PATCH 06/41] fix: stock ageing report --- .../stock/report/stock_ageing/stock_ageing.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 99d816c4a24..447e58c8748 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -17,14 +17,17 @@ def execute(filters=None): data = [] for item, item_dict in iteritems(item_details): + earliest_age, latest_age = 0, 0 fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func) details = item_dict["details"] - if not fifo_queue or (not item_dict.get("total_qty")): continue + if not fifo_queue and (not item_dict.get("total_qty")): continue average_age = get_average_age(fifo_queue, to_date) - earliest_age = date_diff(to_date, fifo_queue[0][1]) - latest_age = date_diff(to_date, fifo_queue[-1][1]) + + if fifo_queue: + earliest_age = date_diff(to_date, fifo_queue[0][1]) + latest_age = date_diff(to_date, fifo_queue[-1][1]) row = [details.name, details.item_name, details.description, details.item_group, details.brand] @@ -147,7 +150,8 @@ def get_fifo_queue(filters, sle=None): item_details.setdefault(key, {"details": d, "fifo_queue": []}) fifo_queue = item_details[key]["fifo_queue"] - transferred_item_details.setdefault((d.voucher_no, d.name), []) + transferred_item_key = (d.voucher_no, d.name, d.warehouse) + transferred_item_details.setdefault(transferred_item_key, []) if d.voucher_type == "Stock Reconciliation": d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0)) @@ -155,10 +159,10 @@ def get_fifo_queue(filters, sle=None): serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else [] if d.actual_qty > 0: - if transferred_item_details.get((d.voucher_no, d.name)): - batch = transferred_item_details[(d.voucher_no, d.name)][0] + if transferred_item_details.get(transferred_item_key): + batch = transferred_item_details[transferred_item_key][0] fifo_queue.append(batch) - transferred_item_details[((d.voucher_no, d.name))].pop(0) + transferred_item_details[(transferred_item_key)].pop(0) else: if serial_no_list: for serial_no in serial_no_list: @@ -182,11 +186,11 @@ def get_fifo_queue(filters, sle=None): # if batch qty > 0 # not enough or exactly same qty in current batch, clear batch qty_to_pop -= flt(batch[0]) - transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0)) + transferred_item_details[transferred_item_key].append(fifo_queue.pop(0)) else: # all from current batch batch[0] = flt(batch[0]) - qty_to_pop - transferred_item_details[(d.voucher_no, d.name)].append([qty_to_pop, batch[1]]) + transferred_item_details[transferred_item_key].append([qty_to_pop, batch[1]]) qty_to_pop = 0 item_details[key]["qty_after_transaction"] = d.qty_after_transaction From 572ee8b1522d1e9ef36b41891d69a4e341336bfe Mon Sep 17 00:00:00 2001 From: Oliver Liu Date: Mon, 28 Sep 2020 02:24:31 +0200 Subject: [PATCH 07/41] include item_code in query result --- erpnext/portal/product_configurator/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 60aa3b64e82..efad078fe37 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -375,7 +375,7 @@ def get_items(filters=None, search=None): results = frappe.db.sql(''' SELECT - `tabItem`.`name`, `tabItem`.`item_name`, + `tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`, `tabItem`.`website_image`, `tabItem`.`image`, `tabItem`.`web_long_description`, `tabItem`.`description`, `tabItem`.`route` From 031502e64c3259669a6f96113fab9609209c6309 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 28 Sep 2020 13:34:24 +0530 Subject: [PATCH 08/41] fix: longer timeout for company replace abbreviation --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 335cad3598b..60dacb54d1a 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -372,7 +372,7 @@ class Company(NestedSet): @frappe.whitelist() def enqueue_replace_abbr(company, old, new): - kwargs = dict(company=company, old=old, new=new) + kwargs = dict(queue='long', company=company, old=old, new=new) frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs) From c7ef8000ace979d402122ef70da2db147fe93208 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 28 Sep 2020 16:00:48 +0530 Subject: [PATCH 09/41] Update stock_ageing.py --- erpnext/stock/report/stock_ageing/stock_ageing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 447e58c8748..953939bccb8 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -162,7 +162,7 @@ def get_fifo_queue(filters, sle=None): if transferred_item_details.get(transferred_item_key): batch = transferred_item_details[transferred_item_key][0] fifo_queue.append(batch) - transferred_item_details[(transferred_item_key)].pop(0) + transferred_item_details[transferred_item_key].pop(0) else: if serial_no_list: for serial_no in serial_no_list: From 8ba98a52a00fb10baa219911823dda8f2ddf199d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 29 Sep 2020 12:59:54 +0530 Subject: [PATCH 10/41] fix: display item name instead of item code in POS --- erpnext/accounts/page/pos/pos.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index cecf7f5e6fc..1e82e54cdf8 100755 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1064,7 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ $(frappe.render_template("pos_item", { item_code: escape(obj.name), item_price: item_price, - title: obj.name || obj.item_name, + title: obj.name === obj.item_name ? obj.name : obj.item_name, item_name: obj.name === obj.item_name ? "" : obj.item_name, item_image: obj.image, item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj), @@ -1546,7 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ $.each(this.frm.doc.items || [], function (i, d) { $(frappe.render_template("pos_bill_item_new", { item_code: escape(d.item_code), - title: d.item_code || d.item_name, + title: d.item_code === d.item_name ? d.item_code : d.item_name, item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("
" + d.item_name), qty: d.qty, discount_percentage: d.discount_percentage || 0.0, From 3fdabe75d3e9830e8012b91f5e360fc73c6a25e6 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 29 Sep 2020 18:16:45 +0530 Subject: [PATCH 11/41] fix: Check only Read and Write Permission in Update Items --- erpnext/controllers/accounts_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bc90011bf12..67dcc7fd4ce 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1209,7 +1209,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil try: doc.check_permission(perm_type) except frappe.PermissionError: - actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' } + actions = { 'create': 'add', 'write': 'update'} frappe.throw(_("You do not have permissions to {} items in a {}.") .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) @@ -1252,7 +1252,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] parent = frappe.get_doc(parent_doctype, parent_doctype_name) - check_doc_permissions(parent, 'cancel') + check_doc_permissions(parent, 'write') validate_and_delete_children(parent, data) for d in data: From 096da540e3ff436c09e7cdfc024a288748ca7f16 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 29 Sep 2020 18:39:45 +0530 Subject: [PATCH 12/41] fix: Add test --- .../doctype/tax_rule/test_tax_rule.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py index bbbcc7f3a69..632e30db45d 100644 --- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py @@ -6,6 +6,8 @@ from __future__ import unicode_literals import frappe import unittest from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template +from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity +from erpnext.crm.doctype.opportunity.opportunity import make_quotation test_records = frappe.get_test_records('Tax Rule') @@ -144,6 +146,23 @@ class TestTaxRule(unittest.TestCase): self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}), "_Test Sales Taxes and Charges Template 1 - _TC") + def test_taxes_fetch_via_tax_rule(self): + make_tax_rule(customer= "_Test Customer", billing_city = "_Test City", + sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + + # create opportunity for customer + opportunity = make_opportunity(with_items=1) + + # make quotation from opportunity + quotation = make_quotation(opportunity.name) + quotation.save() + + self.assertEqual(quotation.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC") + + # Check if accounts heads and rate fetched are also fetched from tax template or not + self.assertTrue(len(quotation.taxes) > 0) + + def make_tax_rule(**args): args = frappe._dict(args) From 91e552ef3a9df5cdeb1df4bc4e7de6194a587056 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 29 Sep 2020 19:33:00 +0530 Subject: [PATCH 13/41] fix: Only validate group cost center if not from repost --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index cf1ad6eab6f..077a11a9be6 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -138,7 +138,7 @@ class GLEntry(Document): frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}") .format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) - if self.cost_center and _check_is_group(): + if not self.flags.from_repost and self.cost_center and _check_is_group(): frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) From 80379ef9216a63674ce2a411aa0f04d6ce916205 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Sep 2020 10:28:38 +0530 Subject: [PATCH 14/41] fix: Remove any taxes fetched due to unknown rules --- erpnext/selling/doctype/sales_order/test_sales_order.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 31c328fdf3e..fbea7fabebb 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -88,6 +88,8 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(len(si.get("items")), 1) si.insert() + si.set('taxes', []) + si.save() self.assertEqual(si.payment_schedule[0].payment_amount, 500.0) self.assertEqual(si.payment_schedule[0].due_date, so.transaction_date) From b217075f6b8bb165bf4a6aa34c2250bcc2ba309f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 30 Sep 2020 13:00:18 +0530 Subject: [PATCH 15/41] fix: Remove any taxes fetched due to unknown rules --- erpnext/selling/doctype/quotation/test_quotation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index ee6b429ccae..dfb284b7682 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -108,6 +108,10 @@ class TestQuotation(unittest.TestCase): sales_order.transaction_date = nowdate() sales_order.insert() + # Remove any unknown taxes if applied + sales_order.set('taxes', []) + sales_order.save() + self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00) self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date)) self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00) From fe0ce60d75dea4876c70f95af88a801d43888d36 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 2 Oct 2020 12:22:28 +0530 Subject: [PATCH 16/41] fix: Handle Blank from/to range in Numeric Item Attribute --- .../item_attribute/item_attribute.json | 418 ++++-------------- .../doctype/item_attribute/item_attribute.py | 3 +- 2 files changed, 81 insertions(+), 340 deletions(-) diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.json b/erpnext/stock/doctype/item_attribute/item_attribute.json index 2fbff4e614e..5c4678916f3 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.json +++ b/erpnext/stock/doctype/item_attribute/item_attribute.json @@ -1,357 +1,97 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:attribute_name", - "beta": 0, - "creation": "2014-09-26 03:49:54.899170", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:attribute_name", + "creation": "2014-09-26 03:49:54.899170", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "attribute_name", + "numeric_values", + "section_break_4", + "from_range", + "increment", + "column_break_8", + "to_range", + "section_break_5", + "item_attribute_values" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "attribute_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Attribute Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "attribute_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Attribute Name", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "numeric_values", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Numeric Values", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "numeric_values", + "fieldtype": "Check", + "label": "Numeric Values" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "numeric_values", - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "numeric_values", + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "from_range", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "From Range", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "from_range", + "fieldtype": "Float", + "label": "From Range" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "increment", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Increment", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "increment", + "fieldtype": "Float", + "label": "Increment" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_8", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "to_range", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "To Range", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "to_range", + "fieldtype": "Float", + "label": "To Range" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: !doc.numeric_values", - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval: !doc.numeric_values", + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "item_attribute_values", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Attribute Values", - "length": 0, - "no_copy": 0, - "options": "Item Attribute Value", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "item_attribute_values", + "fieldtype": "Table", + "label": "Item Attribute Values", + "options": "Item Attribute Value" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-edit", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-01 13:17:46.524806", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item Attribute", - "name_case": "", - "owner": "Administrator", + ], + "icon": "fa fa-edit", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-10-02 12:03:02.359202", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item Attribute", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 1, - "role": "Item Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "read": 1, + "report": 1, + "role": "Item Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 2f75bbd97c0..7f00201587a 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe import _ +from frappe.utils import flt from erpnext.controllers.item_variant import (validate_is_incremental, validate_item_attribute_value, InvalidItemAttributeValueError) @@ -42,7 +43,7 @@ class ItemAttribute(Document): if self.from_range is None or self.to_range is None: frappe.throw(_("Please specify from/to range")) - elif self.from_range >= self.to_range: + elif flt(self.from_range) >= flt(self.to_range): frappe.throw(_("From Range has to be less than To Range")) if not self.increment: From feade95a2616363646499e72e450d20a90b7432a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 29 Sep 2020 21:37:08 +0530 Subject: [PATCH 17/41] fix: tds calculation, skip invoices with "Apply Tax Withholding Amount" has disabled --- .../tax_withholding_category.py | 6 ++--- .../test_tax_withholding_category.py | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index b539fff74e9..ce770d48a8a 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -139,9 +139,9 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai else: tds_amount = _get_tds(net_total, tax_details.rate) else: - supplier_credit_amount = frappe.get_all('Purchase Invoice Item', - fields = ['sum(net_amount)'], - filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1) + supplier_credit_amount = frappe.get_all('Purchase Invoice', + fields = ['sum(net_total)'], + filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1) supplier_credit_amount = (supplier_credit_amount[0][0] if supplier_credit_amount and supplier_credit_amount[0][0] else 0) diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index b1468999fc1..a0b0cbb9956 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -101,6 +101,29 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self): + invoices = [] + frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS") + pi = create_purchase_invoice(supplier="Test TDS Supplier2") + pi.submit() + invoices.append(pi) + + # TDS not applied + pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True) + pi.submit() + invoices.append(pi) + + pi = create_purchase_invoice(supplier="Test TDS Supplier2") + pi.submit() + invoices.append(pi) + + self.assertEqual(pi.taxes_and_charges_deducted, 2000) + self.assertEqual(pi.grand_total, 8000) + + # delete invoices to avoid clashing + for d in invoices: + d.cancel() + def create_purchase_invoice(**args): # return sales invoice doc object item = frappe.get_doc('Item', {'item_name': 'TDS Item'}) @@ -109,7 +132,7 @@ def create_purchase_invoice(**args): pi = frappe.get_doc({ "doctype": "Purchase Invoice", "posting_date": today(), - "apply_tds": 1, + "apply_tds": 0 if args.do_not_apply_tds else 1, "supplier": args.supplier, "company": '_Test Company', "taxes_and_charges": "", From 8aad02f822c03f1fff5283945b3de01d239bb93d Mon Sep 17 00:00:00 2001 From: Saqib Date: Sun, 4 Oct 2020 18:13:58 +0530 Subject: [PATCH 18/41] fix: naming series - cannot reset current value to zero (#23505) --- erpnext/setup/doctype/naming_series/naming_series.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index b2cffbbf0d8..abff97364c0 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cstr +from frappe.utils import cstr, cint from frappe import msgprint, throw, _ from frappe.model.document import Document @@ -159,7 +159,7 @@ class NamingSeries(Document): prefix = self.parse_naming_series() self.insert_series(prefix) frappe.db.sql("update `tabSeries` set current = %s where name = %s", - (self.current_value, prefix)) + (cint(self.current_value), prefix)) msgprint(_("Series Updated Successfully")) else: msgprint(_("Please select prefix first")) From 5056fbcb9da83ea91bcdfcc0c73e2463028edf37 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 4 Oct 2020 20:14:53 +0530 Subject: [PATCH 19/41] fix: performance issue while adding template item in the cart --- erpnext/stock/doctype/item/item.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index deace33f343..8248e50aa6e 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1001,8 +1001,8 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): order by pr.posting_date desc, pr.posting_time desc, pr.name desc limit 1""", (item_code, cstr(doc_name)), as_dict=1) - - + + purchase_order_date = getdate(last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01") purchase_receipt_date = getdate(last_purchase_receipt and @@ -1010,7 +1010,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): if last_purchase_order and (purchase_order_date >= purchase_receipt_date or not last_purchase_receipt): # use purchase order - + last_purchase = last_purchase_order[0] purchase_date = purchase_order_date @@ -1030,7 +1030,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): "discount_percentage": flt(last_purchase.discount_percentage), "purchase_date": purchase_date }) - + conversion_rate = flt(conversion_rate) or 1.0 out.update({ @@ -1069,8 +1069,7 @@ def invalidate_item_variants_cache_for_website(doc): if item_code: item_cache = ItemVariantsCacheManager(item_code) - item_cache.clear_cache() - + item_cache.rebuild_cache() def check_stock_uom_with_bin(item, stock_uom): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): From 6e1d818c3e2dac24157b2576ce479b6d0fa7c1e7 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 5 Oct 2020 12:24:43 +0530 Subject: [PATCH 20/41] fix: Show Project under Create button in SO --- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index b4e151b2e30..423922e4865 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -169,7 +169,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } // project - if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) { + if(flt(doc.per_delivered, 2) < 100) { this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create')); } From 1c826afa017153b856d7baffa21a26695d6c5e8d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 1 Oct 2020 23:57:43 +0530 Subject: [PATCH 21/41] fix: incorrect operation time calculation for batch size --- .../production_plan/test_production_plan.py | 8 +++- .../doctype/work_order/test_work_order.py | 43 +++++++++++++++++++ .../doctype/work_order/work_order.py | 2 +- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index c67330ad45f..2bf883809da 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -251,6 +251,10 @@ def make_bom(**args): 'rate': item_doc.valuation_rate or args.rate, }) - bom.insert(ignore_permissions=True) - bom.submit() + if not args.do_not_save: + bom.insert(ignore_permissions=True) + + if not args.do_not_submit: + bom.submit() + return bom diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f917b098688..06e8e1ebcb2 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -371,6 +371,49 @@ class TestWorkOrder(unittest.TestCase): ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) self.assertEqual(len(ste1.items), 3) + def test_work_order_with_batch_size(self): + fg_item = "Test Batch Size Item For BOM" + rm1 = "Test Batch Size Item RM 1 For BOM" + + for item in ["Test Batch Size Item For BOM", "Test Batch Size Item RM 1 For BOM"]: + make_item(item, { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + }) + + bom_name = frappe.db.get_value("BOM", + {"item": fg_item, "is_active": 1, "with_operations": 1}, "name") + + if not bom_name: + bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True) + bom.with_operations = 1 + bom.append("operations", { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "description": "Test Data", + "operating_cost": 100, + "time_in_mins": 40, + "batch_size": 5 + }) + + bom.save() + bom.submit() + bom_name = bom.name + + work_order = make_wo_order_test_record(item=fg_item, + planned_start_date=now(), qty=1, do_not_save=True) + + work_order.set_work_order_operations() + work_order.save() + self.assertEqual(work_order.operations[0].time_in_mins, 8.0) + + work_order1 = make_wo_order_test_record(item=fg_item, + planned_start_date=now(), qty=5, do_not_save=True) + + work_order1.set_work_order_operations() + work_order1.save() + self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index b0585e5d734..603c8d4928c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -364,7 +364,7 @@ class WorkOrder(Document): bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") for d in self.get("operations"): - d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * math.ceil(flt(self.qty) / flt(d.batch_size)) + d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size)) self.calculate_operating_cost() From 20262e00dfdce538c3ba0be94e3cc8b7c2ccd7d6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Oct 2020 12:45:29 +0530 Subject: [PATCH 22/41] feat: balance serial nos in stock leder report --- .../stock/report/stock_ledger/stock_ledger.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 6a265ec4cc5..67d3f233c81 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -6,6 +6,7 @@ import frappe from frappe import _ from frappe.utils import cint, flt from erpnext.stock.utils import update_included_uom_in_report +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos def execute(filters=None): include_uom = filters.get("include_uom") @@ -23,6 +24,7 @@ def execute(filters=None): actual_qty = stock_value = 0 + available_serial_nos = {} for sle in sl_entries: item_detail = item_details[sle.item_code] @@ -41,6 +43,9 @@ def execute(filters=None): "stock_value": stock_value }) + if sle.serial_no: + update_available_serial_nos(available_serial_nos, sle) + data.append(sle) if include_uom: @@ -49,6 +54,27 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data +def update_available_serial_nos(available_serial_nos, sle): + serial_nos = get_serial_nos(sle.serial_no) + key = (sle.item_code, sle.warehouse) + if key not in available_serial_nos: + available_serial_nos.setdefault(key, []) + + existing_serial_no = available_serial_nos[key] + for sn in serial_nos: + if sle.actual_qty > 0: + if sn in existing_serial_no: + existing_serial_no.remove(sn) + else: + existing_serial_no.append(sn) + else: + if sn in existing_serial_no: + existing_serial_no.remove(sn) + else: + existing_serial_no.append(sn) + + sle.balance_serial_no = '\n'.join(existing_serial_no) + def get_columns(): columns = [ {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95}, @@ -70,7 +96,8 @@ def get_columns(): {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, - {"label": _("Serial #"), "fieldname": "serial_no", "width": 100}, + {"label": _("Serial No"), "fieldname": "serial_no", "width": 100}, + {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, {"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110} ] From f0108b42fd096bcb5fb2448f245abea1fbf1683c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Oct 2020 13:47:03 +0530 Subject: [PATCH 23/41] refactor: added new filters in the Batch-wise balance history report --- .../batch_wise_balance_history.js | 51 ++++++++++++++++++- .../batch_wise_balance_history.py | 4 ++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 84e95e27ca0..4204aee342b 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -3,6 +3,14 @@ frappe.query_reports["Batch-Wise Balance History"] = { "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, { "fieldname":"from_date", "label": __("From Date"), @@ -18,6 +26,47 @@ frappe.query_reports["Batch-Wise Balance History"] = { "width": "80", "default": frappe.datetime.get_today(), "reqd": 1 - } + }, + { + "fieldname":"item_code", + "label": __("Item Code"), + "fieldtype": "Link", + "options": "Item", + "get_query": function() { + return { + filters: { + "has_batch_no": 1 + } + } + } + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function() { + let company = frappe.query_report.get_filter_value('company'); + return { + filters: { + "company": company + } + } + } + }, + { + "fieldname":"batch_no", + "label": __("Batch No"), + "fieldtype": "Link", + "options": "Batch", + "get_query": function() { + let item_code = frappe.query_report.get_filter_value('item_code'); + return { + filters: { + "item": item_code + } + } + } + }, ] } \ No newline at end of file diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index ec2ef35bb41..1999b7404e6 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -53,6 +53,10 @@ def get_conditions(filters): else: frappe.throw(_("'To Date' is required")) + for field in ["item_code", "warehouse", "batch_no", "company"]: + if filters.get(field): + conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field))) + return conditions #get all details From 3f505b711ddc65b7d33320bcfb946472723efb58 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 7 Oct 2020 19:06:15 +0530 Subject: [PATCH 24/41] fixed: COGS validation in the purchase receipt --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 42cc473b18d..f9394bb0191 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -207,7 +207,6 @@ class PurchaseReceipt(BuyingController): from erpnext.accounts.general_ledger import process_gl_map stock_rbnb = self.get_company_default("stock_received_but_not_billed") - cogs_account = self.get_company_default("default_expense_account") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") @@ -289,6 +288,7 @@ class PurchaseReceipt(BuyingController): if self.is_return or flt(d.item_tax_amount): loss_account = expenses_included_in_valuation else: + cogs_account = self.get_company_default("default_expense_account") loss_account = cogs_account gl_entries.append(self.get_gl_dict({ From 78a98030efb5bb59c9be94a3423266a27d7cb1c1 Mon Sep 17 00:00:00 2001 From: Tunde Akinyanmi Date: Thu, 8 Oct 2020 02:04:53 +0100 Subject: [PATCH 25/41] fix: pass self.flags.ignore_permissions to the ignore_permissions argument of Address and Contact controller --- erpnext/selling/doctype/customer/customer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 1484f6b2290..a4a63d2aef2 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -129,7 +129,7 @@ class Customer(TransactionBase): address = frappe.get_doc('Address', address_name.get('name')) if not address.has_link('Customer', self.name): address.append('links', dict(link_doctype='Customer', link_name=self.name)) - address.save() + address.save(ignore_permissions=self.flags.ignore_permissions) lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True) @@ -147,7 +147,7 @@ class Customer(TransactionBase): contact = frappe.get_doc('Contact', contact_name.get('name')) if not contact.has_link('Customer', self.name): contact.append('links', dict(link_doctype='Customer', link_name=self.name)) - contact.save() + contact.save(ignore_permissions=self.flags.ignore_permissions) else: lead.lead_name = lead.lead_name.lstrip().split(" ") From 5f3e5c8a7c7de911ee151da525127ad2d5a042ac Mon Sep 17 00:00:00 2001 From: michellealva Date: Thu, 8 Oct 2020 09:41:44 +0530 Subject: [PATCH 26/41] fix: Typo in Accounts Settings --- .../accounts/doctype/accounts_settings/accounts_settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index d2f7de3be52..70e6e44d719 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -97,7 +97,7 @@ "default": "1", "fieldname": "unlink_advance_payment_on_cancelation_of_order", "fieldtype": "Check", - "label": "Unlink Advance Payment on Cancelation of Order" + "label": "Unlink Advance Payment on Cancellation of Order" }, { "default": "1", @@ -179,7 +179,7 @@ "icon": "icon-cog", "idx": 1, "issingle": 1, - "modified": "2020-03-11 13:09:26.235848", + "modified": "2020-10-08 09:40:12.121145", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 5da98cce862bc9b34a946eba807fe972f712bf31 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 8 Oct 2020 11:19:41 +0530 Subject: [PATCH 27/41] Update test_work_order.py --- erpnext/manufacturing/doctype/work_order/test_work_order.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 06e8e1ebcb2..d82a4dd9fe8 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -371,7 +371,7 @@ class TestWorkOrder(unittest.TestCase): ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) self.assertEqual(len(ste1.items), 3) - def test_work_order_with_batch_size(self): + def test_operation_time_with_batch_size(self): fg_item = "Test Batch Size Item For BOM" rm1 = "Test Batch Size Item RM 1 For BOM" From 7c760f62c3ebf53d208e0e8356ec2bd21a145655 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 8 Oct 2020 13:49:07 +0530 Subject: [PATCH 28/41] fix: 'stock_rbnb_currency' referenced before assignment (#23528) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index f9394bb0191..08553675f4c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -207,6 +207,8 @@ class PurchaseReceipt(BuyingController): from erpnext.accounts.general_ledger import process_gl_map stock_rbnb = self.get_company_default("stock_received_but_not_billed") + stock_rbnb_currency = get_account_currency(stock_rbnb) + cogs_account = self.get_company_default("default_expense_account") landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") @@ -242,7 +244,6 @@ class PurchaseReceipt(BuyingController): # stock received but not billed if d.base_net_amount: - stock_rbnb_currency = get_account_currency(stock_rbnb) gl_entries.append(self.get_gl_dict({ "account": stock_rbnb, "against": warehouse_account[d.warehouse]["account"], From eef5c5dad403563cd100b7a598c67737e1be5436 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 15 Jul 2020 16:37:21 +0530 Subject: [PATCH 29/41] fix: Show total row in print format of financial statement --- erpnext/accounts/report/financial_statements.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html index 50947ecf5ef..2bb09cf0dc5 100644 --- a/erpnext/accounts/report/financial_statements.html +++ b/erpnext/accounts/report/financial_statements.html @@ -44,7 +44,7 @@ - {% for(let j=0, k=data.length-1; j Date: Thu, 8 Oct 2020 15:08:49 +0530 Subject: [PATCH 30/41] fix: project value is missing from procurement-tracker --- .../procurement_tracker.py | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 88a865f0f85..beeca091c8a 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -143,7 +143,7 @@ def get_conditions(filters): conditions = "" if filters.get("company"): - conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company')) + conditions += " AND parent.company=%s" % frappe.db.escape(filters.get('company')) if filters.get("cost_center") or filters.get("project"): conditions += """ @@ -151,10 +151,10 @@ def get_conditions(filters): """ % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) if filters.get("from_date"): - conditions += " AND par.transaction_date>='%s'" % filters.get('from_date') + conditions += " AND parent.transaction_date>='%s'" % filters.get('from_date') if filters.get("to_date"): - conditions += " AND par.transaction_date<='%s'" % filters.get('to_date') + conditions += " AND parent.transaction_date<='%s'" % filters.get('to_date') return conditions def get_data(filters): @@ -198,21 +198,23 @@ def get_mapped_mr_details(conditions): mr_records = {} mr_details = frappe.db.sql(""" SELECT - par.transaction_date, - par.per_ordered, - par.owner, + parent.transaction_date, + parent.per_ordered, + parent.owner, child.name, child.parent, child.amount, child.qty, child.item_code, child.uom, - par.status - FROM `tabMaterial Request` par, `tabMaterial Request Item` child + parent.status, + child.project, + child.cost_center + FROM `tabMaterial Request` parent, `tabMaterial Request Item` child WHERE - par.per_ordered>=0 - AND par.name=child.parent - AND par.docstatus=1 + parent.per_ordered>=0 + AND parent.name=child.parent + AND parent.docstatus=1 {conditions} """.format(conditions=conditions), as_dict=1) #nosec @@ -232,7 +234,9 @@ def get_mapped_mr_details(conditions): status=record.status, actual_cost=0, purchase_order_amt=0, - purchase_order_amt_in_company_currency=0 + purchase_order_amt_in_company_currency=0, + project = record.project, + cost_center = record.cost_center ) procurement_record_against_mr.append(procurement_record_details) return mr_records, procurement_record_against_mr @@ -280,16 +284,16 @@ def get_po_entries(conditions): child.amount, child.base_amount, child.schedule_date, - par.transaction_date, - par.supplier, - par.status, - par.owner - FROM `tabPurchase Order` par, `tabPurchase Order Item` child + parent.transaction_date, + parent.supplier, + parent.status, + parent.owner + FROM `tabPurchase Order` parent, `tabPurchase Order Item` child WHERE - par.docstatus = 1 - AND par.name = child.parent - AND par.status not in ("Closed","Completed","Cancelled") + parent.docstatus = 1 + AND parent.name = child.parent + AND parent.status not in ("Closed","Completed","Cancelled") {conditions} GROUP BY - par.name, child.item_code + parent.name, child.item_code """.format(conditions=conditions), as_dict=1) #nosec \ No newline at end of file From 4968f162bddef2762710eb44212a0a09287ba535 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 9 Oct 2020 12:30:11 +0530 Subject: [PATCH 31/41] fix: negative stock error while submitting stock reco for batch item --- .../stock_reconciliation.py | 19 ++++++++---- .../test_stock_reconciliation.py | 31 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 9 ++++-- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index ca59e67a676..003403a2f8a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -172,8 +172,9 @@ class StockReconciliation(StockController): row.serial_no = '' # item managed batch-wise not allowed - if item.has_batch_no and not row.batch_no and not item.create_new_batch: - raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code)) + if item.has_batch_no and not row.batch_no and not frappe.flags.in_test: + if not item.create_new_batch or self.purpose != 'Opening Stock': + raise frappe.ValidationError(_("Batch no is required for the batched item {0}").format(item_code)) # docstatus should be < 2 validate_cancelled_item(item_code, item.docstatus, verbose=0) @@ -191,10 +192,11 @@ class StockReconciliation(StockController): serialized_items = False for row in self.items: item = frappe.get_cached_doc("Item", row.item_code) - if not (item.has_serial_no or item.has_batch_no): - if row.serial_no or row.batch_no: + if not (item.has_serial_no): + if row.serial_no: frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \ .format(row.idx, frappe.bold(row.item_code))) + previous_sle = get_previous_sle({ "item_code": row.item_code, "warehouse": row.warehouse, @@ -217,7 +219,12 @@ class StockReconciliation(StockController): or (not previous_sle and not row.qty)): continue - sl_entries.append(self.get_sle_for_items(row)) + sle_data = self.get_sle_for_items(row) + + if row.batch_no: + sle_data.actual_qty = row.quantity_difference + + sl_entries.append(sle_data) else: serialized_items = True @@ -244,7 +251,7 @@ class StockReconciliation(StockController): serial_nos = get_serial_nos(row.serial_no) or [] # To issue existing serial nos - if row.current_qty and (row.current_serial_no or row.batch_no): + if row.current_qty and (row.current_serial_no): args = self.get_sle_for_items(row) args.update({ 'actual_qty': -1 * row.current_qty, diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index a679c9415d0..8b073ec5ab4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -361,6 +361,37 @@ class TestStockReconciliation(unittest.TestCase): doc.cancel() frappe.delete_doc(doc.doctype, doc.name) + def test_allow_negative_for_batch(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + item_code = "Stock-Reco-batch-Item-5" + warehouse = "_Test Warehouse for Stock Reco5 - _TC" + + create_warehouse("_Test Warehouse for Stock Reco5", {"is_group": 0, + "parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"}) + + batch_item_doc = create_item(item_code, is_stock_item=1) + if not batch_item_doc.has_batch_no: + frappe.db.set_value("Item", item_code, { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "Test-C.####" + }) + + ste1=make_stock_entry(posting_date="2020-10-07", posting_time="02:00", item_code=item_code, + target=warehouse, qty=2, basic_rate=100) + + batch_no = ste1.items[0].batch_no + + ste2=make_stock_entry(posting_date="2020-10-09", posting_time="02:00", item_code=item_code, + source=warehouse, qty=2, basic_rate=100, batch_no=batch_no) + + sr = create_stock_reconciliation(item_code=item_code, + warehouse = warehouse, batch_no=batch_no, rate=200) + + for doc in [sr, ste2, ste1]: + doc.cancel() + frappe.delete_doc(doc.doctype, doc.name) + def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5c4bba730e3..4fa080a2fd2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -162,10 +162,13 @@ class update_entries_after(object): self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) else: - if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no: - # assert + if sle.voucher_type=="Stock Reconciliation": + if sle.batch_no: + self.qty_after_transaction += flt(sle.actual_qty) + else: + self.qty_after_transaction = sle.qty_after_transaction + self.valuation_rate = sle.valuation_rate - self.qty_after_transaction = sle.qty_after_transaction self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]] self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate) else: From a7293407bd9813e736658d256e4eed3b6105fcca Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 9 Oct 2020 21:00:33 +0530 Subject: [PATCH 32/41] fix: (revert) Add Delivery Note Count in Sales Invoice Dashboard --- .../accounts/doctype/sales_invoice/sales_invoice_dashboard.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index b35e32c5ca5..4be4c269f17 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -13,8 +13,7 @@ def get_data(): 'Auto Repeat': 'reference_document', }, 'internal_links': { - 'Sales Order': ['items', 'sales_order'], - 'Delivery Note': ['items', 'delivery_note'] + 'Sales Order': ['items', 'sales_order'] }, 'transactions': [ { From 5d7c9d2f61e183058c2971d4f5758bbdf4079a5f Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 9 Oct 2020 21:19:40 +0530 Subject: [PATCH 33/41] fix: update items after submission ignores precision (#23492) * fix: update items after submission ignores precision * chore: add test --- erpnext/controllers/accounts_controller.py | 10 ++++++---- erpnext/public/js/utils.js | 12 +++++++++--- .../doctype/sales_order/test_sales_order.py | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 67dcc7fd4ce..12b8f697cde 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1284,19 +1284,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil validate_quantity(child_item, d) child_item.qty = flt(d.get("qty")) - precision = child_item.precision("rate") or 2 + rate_precision = child_item.precision("rate") or 2 + conv_fac_precision = child_item.precision("conversion_factor") or 2 + qty_precision = child_item.precision("qty") or 2 - if flt(child_item.billed_amt, precision) > flt(flt(d.get("rate")) * flt(d.get("qty")), precision): + if flt(child_item.billed_amt, rate_precision) > flt(flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision): frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.") .format(child_item.idx, child_item.item_code)) else: - child_item.rate = flt(d.get("rate")) + child_item.rate = flt(d.get("rate"), rate_precision) if d.get("conversion_factor"): if child_item.stock_uom == child_item.uom: child_item.conversion_factor = 1 else: - child_item.conversion_factor = flt(d.get('conversion_factor')) + child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision) if d.get("delivery_date") and parent_doctype == 'Sales Order': child_item.delivery_date = d.get('delivery_date') diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 9b1c94e5ba0..264344ca94c 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -452,6 +452,9 @@ erpnext.utils.update_child_items = function(opts) { const frm = opts.frm; const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; + const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`); + const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision; + this.data = []; const fields = [{ fieldtype:'Data', @@ -472,14 +475,16 @@ erpnext.utils.update_child_items = function(opts) { default: 0, read_only: 0, in_list_view: 1, - label: __('Qty') + label: __('Qty'), + precision: get_precision("qty") }, { fieldtype:'Currency', fieldname:"rate", default: 0, read_only: 0, in_list_view: 1, - label: __('Rate') + label: __('Rate'), + precision: get_precision("rate") }]; if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) { @@ -494,7 +499,8 @@ erpnext.utils.update_child_items = function(opts) { fieldtype: 'Float', fieldname: "conversion_factor", in_list_view: 1, - label: __("Conversion Factor") + label: __("Conversion Factor"), + precision: get_precision('conversion_factor') }) } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index fbea7fabebb..55458a51e98 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -402,6 +402,22 @@ class TestSalesOrder(unittest.TestCase): trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + + def test_update_child_with_precision(self): + from frappe.model.meta import get_field_precision + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + + precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate")) + + make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency") + so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664) + + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}]) + update_child_qty_rate('Sales Order', trans_item, so.name) + + so.reload() + self.assertEqual(so.items[0].rate, 200.34669) + make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency") def test_update_child_qty_rate_perm(self): so = make_sales_order(item_code= "_Test Item", qty=4) From 6297778a6acf02f325d73a1f8ba4f23ba96fca72 Mon Sep 17 00:00:00 2001 From: Anupam Date: Mon, 12 Oct 2020 11:00:28 +0530 Subject: [PATCH 34/41] feat: validate if removed item attributes exist in variants --- erpnext/stock/doctype/item/item.py | 71 ++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index deace33f343..4d5fabb9702 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -111,6 +111,7 @@ class Item(WebsiteGenerator): self.synced_with_hub = 0 self.validate_has_variants() + self.validate_attributes_in_variants() self.validate_stock_exists_for_template_item() self.validate_attributes() self.validate_variant_attributes() @@ -806,6 +807,76 @@ class Item(WebsiteGenerator): if frappe.db.exists("Item", {"variant_of": self.name}): frappe.throw(_("Item has variants.")) + def validate_attributes_in_variants(self): + if not self.has_variants or self.get("__islocal"): + return + + old_doc = self.get_doc_before_save() + old_doc_attributes = set([attr.attribute for attr in old_doc.attributes]) + own_attributes = [attr.attribute for attr in self.attributes] + + # Check if old attributes were removed from the list + # Is old_attrs is a subset of new ones + # that means we need not check any changes + if old_doc_attributes.issubset(set(own_attributes)): + return + + from collections import defaultdict + + # get all item variants + items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})] + + # get all deleted attributes + deleted_attribute = list(old_doc_attributes.difference(set(own_attributes))) + + # fetch all attributes of these items + item_attributes = frappe.get_all( + "Item Variant Attribute", + filters={ + "parent": ["in", items], + "attribute": ["in", deleted_attribute] + }, + fields=["attribute", "parent"] + ) + not_included = defaultdict(list) + + for attr in item_attributes: + if attr["attribute"] not in own_attributes: + not_included[attr["parent"]].append(attr["attribute"]) + + if not len(not_included): + return + + def body(docnames): + docnames.sort() + return "
".join(docnames) + + def table_row(title, body): + return """ + {0} + {1} + """.format(title, body) + + rows = '' + for docname, attr_list in not_included.items(): + link = "{0}".format(frappe.bold(_(docname))) + rows += table_row(link, body(attr_list)) + + error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.') + + message = """ +
{0}

+ + + + + + {3} +
{1}{2}
+ """.format(error_description, _('Variant Items'), _('Attributes'), rows) + + frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True) + def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) From dc68b445960020f09c96b71c01a6dadc1b191d84 Mon Sep 17 00:00:00 2001 From: Anupam Date: Mon, 12 Oct 2020 11:53:19 +0530 Subject: [PATCH 35/41] fix: can't save item price after adding child table --- erpnext/stock/doctype/item_price/item_price.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 8e39eb5037d..51b47c50a3b 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -50,16 +50,18 @@ class ItemPrice(Document): def check_duplicates(self): conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s" + condition_data_dict = dict(item_code=self.item_code, price_list=self.price_list, name=self.name) for field in ['uom', 'valid_from', 'valid_upto', 'packing_unit', 'customer', 'supplier']: if self.get(field): conditions += " and {0} = %({1})s".format(field, field) + condition_data_dict[field] = self.get(field) price_list_rate = frappe.db.sql(""" SELECT price_list_rate FROM `tabItem Price` - {conditions} """.format(conditions=conditions), self.as_dict()) + {conditions} """.format(conditions=conditions), condition_data_dict) if price_list_rate : frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem) From 3ca57e493a0d6fdce55a6fe541101269dfadfbbe Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 12 Oct 2020 13:03:25 +0530 Subject: [PATCH 36/41] fix: Do not consider opening entries for TDS calculation (#23598) --- .../tax_withholding_category/tax_withholding_category.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index ce770d48a8a..f6a7218d601 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai from `tabGL Entry` where company = %s and party in %s and fiscal_year=%s and credit > 0 + and is_opening = 'No' """, (company, tuple(suppliers), fiscal_year), as_dict=1) vouchers = [d.voucher_no for d in entries] @@ -192,6 +193,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No select distinct voucher_no from `tabGL Entry` where party in %s and %s and debit > 0 + and is_opening = 'No' """, (tuple(suppliers), condition)) or [] def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): From 772ccde33fc93c4c3f9586f80c00486b7dedbb01 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 12 Oct 2020 13:32:49 +0530 Subject: [PATCH 37/41] fix: Mode of payment getting overwritten by defautl mode of payment for returns --- erpnext/controllers/taxes_and_totals.py | 35 ++++++++------ .../public/js/controllers/taxes_and_totals.js | 46 +++++++++++-------- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index c8f42a5921f..28bfb7a0072 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -629,22 +629,29 @@ class calculate_taxes_and_totals(object): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) def update_paid_amount_for_return(self, total_amount_to_pay): - default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment', - {'parent': self.doc.pos_profile, 'default': 1}, - ['mode_of_payment', 'type', 'account'], as_dict=1) + existing_amount = 0 - self.doc.payments = [] + for payment in self.doc.payments: + existing_amount += payment.amount - if default_mode_of_payment: - self.doc.append('payments', { - 'mode_of_payment': default_mode_of_payment.mode_of_payment, - 'type': default_mode_of_payment.type, - 'account': default_mode_of_payment.account, - 'amount': total_amount_to_pay - }) - else: - self.doc.is_pos = 0 - self.doc.pos_profile = '' + # do not override user entered amount if equal to total_amount_to_pay + if existing_amount != total_amount_to_pay: + default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment', + {'parent': self.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'type', 'account'], as_dict=1) + + self.doc.payments = [] + + if default_mode_of_payment: + self.doc.append('payments', { + 'mode_of_payment': default_mode_of_payment.mode_of_payment, + 'type': default_mode_of_payment.type, + 'account': default_mode_of_payment.account, + 'amount': total_amount_to_pay + }) + else: + self.doc.is_pos = 0 + self.doc.pos_profile = '' self.calculate_paid_amount() diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1e99aa3bf0b..8281bd98e4a 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -594,7 +594,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(actual_taxes_dict, function(key, value) { if (value) total_actual_tax += value; }); - + return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total")); } }, @@ -672,25 +672,33 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ ); } - frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, - ['mode_of_payment', 'account', 'type'], (value) => { - if (this.frm.is_dirty()) { - frappe.model.clear_table(this.frm.doc, 'payments'); - if (value) { - let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); - row.mode_of_payment = value.mode_of_payment; - row.type = value.type; - row.account = value.account; - row.default = 1; - row.amount = total_amount_to_pay; - } else { - this.frm.set_value('is_pos', 1); - } - this.frm.refresh_fields(); - } - }, 'Sales Invoice'); + let existing_amount = 0 + $.each(this.frm.doc.payments || [], function(i, row) { + existing_amount += row.amount; + }) - this.calculate_paid_amount(); + if (existing_amount != total_amount_to_pay) { + frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, + ['mode_of_payment', 'account', 'type'], (value) => { + if (this.frm.is_dirty()) { + frappe.model.clear_table(this.frm.doc, 'payments'); + if (value) { + let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); + row.mode_of_payment = value.mode_of_payment; + row.type = value.type; + row.account = value.account; + row.default = 1; + row.amount = total_amount_to_pay; + } else { + this.frm.set_value('is_pos', 1); + } + this.frm.refresh_fields(); + this.calculate_paid_amount(); + } + }, 'Sales Invoice'); + } else { + this.calculate_paid_amount(); + } }, set_default_payment: function(total_amount_to_pay, update_paid_amount) { From 1270c53dea6302633ee5e3fdd688e13966e7712f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 12 Oct 2020 14:54:54 +0530 Subject: [PATCH 38/41] fix: not able to do overproduction --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index d14c8d82f11..f9c028563bb 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -611,7 +611,7 @@ erpnext.work_order = { description: __('Max: {0}', [max]), default: max }, data => { - max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; + max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; if (data.qty > max) { frappe.msgprint(__('Quantity must not be more than {0}', [max])); From f41fe79528d3478efe60260c5df1806f810ab470 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Oct 2020 14:56:08 +0530 Subject: [PATCH 39/41] fix(asset): cannot create asset if cwip disabled and account not set (#23584) * fix: asset purchase with purchase invoice * chore: allow enable cwip accounting only if cwip account is set * fix: cannot create asset if cwip disabled and account not set --- .../purchase_invoice/purchase_invoice.py | 3 +- .../purchase_invoice/test_purchase_invoice.py | 3 +- erpnext/assets/doctype/asset/asset.py | 73 +++++---- erpnext/assets/doctype/asset/test_asset.py | 144 +++++++++--------- .../doctype/asset_category/asset_category.py | 18 ++- .../asset_category/test_asset_category.py | 20 ++- 6 files changed, 152 insertions(+), 109 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c9fd889b63f..155bfd4416a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -636,7 +636,8 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount / self.conversion_rate) }, item=item)) else: - cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + cwip_account = get_asset_account("capital_work_in_progress_account", + asset_category=item.asset_category,company=self.company) cwip_account_currency = get_account_currency(cwip_account) gl_entries.append(self.get_gl_dict({ diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4019815e19a..329433ebc10 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -939,7 +939,8 @@ def make_purchase_invoice(**args): "cost_center": args.cost_center or "_Test Cost Center - _TC", "project": args.project, "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "" + "rejected_serial_no": args.rejected_serial_no or "", + "asset_location": args.location or "" }) if args.get_taxes_and_charges: diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 719b8de92be..f03d1e43c42 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -470,29 +470,37 @@ class Asset(AccountsController): def validate_make_gl_entry(self): purchase_document = self.get_purchase_document() - asset_bought_with_invoice = purchase_document == self.purchase_invoice - fixed_asset_account, cwip_account = self.get_asset_accounts() - cwip_enabled = is_cwip_accounting_enabled(self.asset_category) - # check if expense already has been booked in case of cwip was enabled after purchasing asset - expense_booked = False - cwip_booked = False - - if asset_bought_with_invoice: - expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", - (purchase_document, fixed_asset_account), as_dict=1) - else: - cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", - (purchase_document, cwip_account), as_dict=1) - - if cwip_enabled and (expense_booked or not cwip_booked): - # if expense has already booked from invoice or cwip is booked from receipt + if not purchase_document: return False - elif not cwip_enabled and (not expense_booked or cwip_booked): - # if cwip is disabled but expense hasn't been booked yet - return True - elif cwip_enabled: - # default condition - return True + + asset_bought_with_invoice = (purchase_document == self.purchase_invoice) + fixed_asset_account = self.get_fixed_asset_account() + + cwip_enabled = is_cwip_accounting_enabled(self.asset_category) + cwip_account = self.get_cwip_account(cwip_enabled=cwip_enabled) + + query = """SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""" + if asset_bought_with_invoice: + # with invoice purchase either expense or cwip has been booked + expense_booked = frappe.db.sql(query, (purchase_document, fixed_asset_account), as_dict=1) + if expense_booked: + # if expense is already booked from invoice then do not make gl entries regardless of cwip enabled/disabled + return False + + cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1) + if cwip_booked: + # if cwip is booked from invoice then make gl entries regardless of cwip enabled/disabled + return True + else: + # with receipt purchase either cwip has been booked or no entries have been made + if not cwip_account: + # if cwip account isn't available do not make gl entries + return False + + cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1) + # if cwip is not booked from receipt then do not make gl entries + # if cwip is booked from receipt then make gl entries + return cwip_booked def get_purchase_document(self): asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') @@ -500,20 +508,25 @@ class Asset(AccountsController): return purchase_document - def get_asset_accounts(self): - fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, - asset_category = self.asset_category, company = self.company) + def get_fixed_asset_account(self): + return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + + def get_cwip_account(self, cwip_enabled=False): + cwip_account = None + try: + cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company) + except: + # if no cwip account found in category or company and "cwip is enabled" then raise else silently pass + if cwip_enabled: + raise - cwip_account = get_asset_account("capital_work_in_progress_account", - self.name, self.asset_category, self.company) - - return fixed_asset_account, cwip_account + return cwip_account def make_gl_entries(self): gl_entries = [] purchase_document = self.get_purchase_document() - fixed_asset_account, cwip_account = self.get_asset_accounts() + fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index d914dabc123..89e1864cb61 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset from erpnext.assets.doctype.asset.asset import make_sales_invoice from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice class TestAsset(unittest.TestCase): @@ -561,81 +562,6 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) - def test_gle_with_cwip_toggling(self): - # TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset - frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) - - pr = make_purchase_receipt(item_code="Macbook Pro", - qty=1, rate=5000, do_not_submit=True, location="Test Location") - pr.set('taxes', [{ - 'category': 'Total', - 'add_deduct_tax': 'Add', - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Service Tax - _TC', - 'description': '_Test Account Service Tax', - 'cost_center': 'Main - _TC', - 'rate': 5.0 - }, { - 'category': 'Valuation and Total', - 'add_deduct_tax': 'Add', - 'charge_type': 'On Net Total', - 'account_head': '_Test Account Shipping Charges - _TC', - 'description': '_Test Account Shipping Charges', - 'cost_center': 'Main - _TC', - 'rate': 5.0 - }]) - pr.submit() - expected_gle = ( - ("Asset Received But Not Billed - _TC", 0.0, 5250.0), - ("CWIP Account - _TC", 5250.0, 0.0) - ) - pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Receipt' and voucher_no = %s - order by account""", pr.name) - self.assertEqual(pr_gle, expected_gle) - - pi = make_invoice(pr.name) - pi.submit() - expected_gle = ( - ("_Test Account Service Tax - _TC", 250.0, 0.0), - ("_Test Account Shipping Charges - _TC", 250.0, 0.0), - ("Asset Received But Not Billed - _TC", 5250.0, 0.0), - ("Creditors - _TC", 0.0, 5500.0), - ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), - ) - pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no = %s - order by account""", pi.name) - self.assertEqual(pi_gle, expected_gle) - - asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') - asset_doc = frappe.get_doc('Asset', asset) - month_end_date = get_last_day(nowdate()) - asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) - asset_doc.append("finance_books", { - "expected_value_after_useful_life": 200, - "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date - }) - - # disable cwip and try submitting - frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) - asset_doc.submit() - # asset should have gl entries even if cwip is disabled - expected_gle = ( - ("_Test Fixed Asset - _TC", 5250.0, 0.0), - ("CWIP Account - _TC", 0.0, 5250.0) - ) - gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type='Asset' and voucher_no = %s - order by account""", asset_doc.name) - self.assertEqual(gle, expected_gle) - - frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) - def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") @@ -643,6 +569,74 @@ class TestAsset(unittest.TestCase): doc = make_invoice(pr.name) self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) + + def test_asset_cwip_toggling_cases(self): + cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting") + name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"]) + cwip_acc = "CWIP Account - _TC" + + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "") + frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", "") + + # case 0 -- PI with cwip disable, Asset with cwip disabled, No cwip account set + pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) + asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertFalse(gle) + + # case 1 -- PR with cwip disabled, Asset with cwip enabled + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) + asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertFalse(gle) + + # case 2 -- PR with cwip enabled, Asset with cwip disabled + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertTrue(gle) + + # case 3 -- PI with cwip disabled, Asset with cwip enabled + pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertFalse(gle) + + # case 4 -- PI with cwip enabled, Asset with cwip disabled + pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + asset_doc.available_for_use_date = nowdate() + asset_doc.calculate_depreciation = 0 + asset_doc.submit() + gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) + self.assertTrue(gle) + + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip) + frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) + frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc) def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 9a33fc14ac0..46620d56e98 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cint +from frappe.utils import cint, get_link_to_form from frappe.model.document import Document class AssetCategory(Document): @@ -13,6 +13,7 @@ class AssetCategory(Document): self.validate_finance_books() self.validate_account_types() self.validate_account_currency() + self.valide_cwip_account() def validate_finance_books(self): for d in self.finance_books: @@ -58,6 +59,21 @@ class AssetCategory(Document): frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), title=_("Invalid Account")) + + def valide_cwip_account(self): + if self.enable_cwip_accounting: + missing_cwip_accounts_for_company = [] + for d in self.accounts: + if (not d.capital_work_in_progress_account and + not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")): + missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name)) + + if missing_cwip_accounts_for_company: + msg = _("""To enable Capital Work in Progress Accounting, """) + msg += _("""you must select Capital Work in Progress Account in accounts table""") + msg += "

" + msg += _("You can also set default CWIP account in Company {}").format(", ".join(missing_cwip_accounts_for_company)) + frappe.throw(msg, title=_("Missing Account")) @frappe.whitelist() diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py index b32f9b50202..39b79d6c507 100644 --- a/erpnext/assets/doctype/asset_category/test_asset_category.py +++ b/erpnext/assets/doctype/asset_category/test_asset_category.py @@ -26,4 +26,22 @@ class TestAssetCategory(unittest.TestCase): asset_category.insert() except frappe.DuplicateEntryError: pass - \ No newline at end of file + + def test_cwip_accounting(self): + company_cwip_acc = frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account") + frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "") + + asset_category = frappe.new_doc("Asset Category") + asset_category.asset_category_name = "Computers" + asset_category.enable_cwip_accounting = 1 + + asset_category.total_number_of_depreciations = 3 + asset_category.frequency_of_depreciation = 3 + asset_category.append("accounts", { + "company_name": "_Test Company", + "fixed_asset_account": "_Test Fixed Asset - _TC", + "accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", + "depreciation_expense_account": "_Test Depreciations - _TC" + }) + + self.assertRaises(frappe.ValidationError, asset_category.insert) \ No newline at end of file From 79d069cbbb1ddfdbddd96a0763dc58a18d982346 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 12 Oct 2020 17:09:29 +0530 Subject: [PATCH 40/41] fix: last purchase rate in item prices report (#23507) * fix: last purchase rate in item prices report * fix: last purchase rate in item prices report * fix: last purchase rate in item prices report * chore: fetch last purchase rate from update stock purchase invoices --- .../stock/report/item_prices/item_prices.py | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index aa3ed92079c..12f32972039 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -77,38 +77,33 @@ def get_price_list(): return item_rate_map def get_last_purchase_rate(): - item_last_purchase_rate_map = {} - query = """select * from (select - result.item_code, - result.base_rate - from ( - (select - po_item.item_code, - po_item.item_name, - po.transaction_date as posting_date, - po_item.base_price_list_rate, - po_item.discount_percentage, - po_item.base_rate - from `tabPurchase Order` po, `tabPurchase Order Item` po_item - where po.name = po_item.parent and po.docstatus = 1) - union - (select - pr_item.item_code, - pr_item.item_name, - pr.posting_date, - pr_item.base_price_list_rate, - pr_item.discount_percentage, - pr_item.base_rate - from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item - where pr.name = pr_item.parent and pr.docstatus = 1) - ) result - order by result.item_code asc, result.posting_date desc) result_wrapper - group by item_code""" + query = """select * from ( + (select + po_item.item_code, + po.transaction_date as posting_date, + po_item.base_rate + from `tabPurchase Order` po, `tabPurchase Order Item` po_item + where po.name = po_item.parent and po.docstatus = 1) + union + (select + pr_item.item_code, + pr.posting_date, + pr_item.base_rate + from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item + where pr.name = pr_item.parent and pr.docstatus = 1) + union + (select + pi_item.item_code, + pi.posting_date, + pi_item.base_rate + from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pi_item + where pi.name = pi_item.parent and pi.docstatus = 1 and pi.update_stock = 1) + ) result order by result.item_code asc, result.posting_date asc""" for d in frappe.db.sql(query, as_dict=1): - item_last_purchase_rate_map.setdefault(d.item_code, d.base_rate) + item_last_purchase_rate_map[d.item_code] = d.base_rate return item_last_purchase_rate_map From 984fdde692331a3bd7f0c1e5eb7f06df149f3551 Mon Sep 17 00:00:00 2001 From: Anupam Date: Mon, 12 Oct 2020 18:06:58 +0530 Subject: [PATCH 41/41] fix: review fixes --- .../doctype/quality_procedure/quality_procedure.json | 5 +++-- .../doctype/quality_procedure/quality_procedure.py | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json index 1dc4660c44d..32a1ebcc0ca 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "format:PRC-{quality_procedure_name}", "creation": "2018-10-06 00:06:29.756804", @@ -17,7 +18,6 @@ ], "fields": [ { - "depends_on": "eval: doc.is_group == 0", "fieldname": "parent_quality_procedure", "fieldtype": "Link", "label": "Parent Procedure", @@ -71,7 +71,8 @@ } ], "is_tree": 1, - "modified": "2020-09-22 12:56:50.700777", + "links": [], + "modified": "2020-10-12 16:14:11.167537", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure", diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py index 1952e578673..797c26b64c2 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils.nestedset import NestedSet +from frappe.utils.nestedset import NestedSet, rebuild_tree from frappe import _ class QualityProcedure(NestedSet): @@ -42,6 +42,8 @@ class QualityProcedure(NestedSet): doc.save(ignore_permissions=True) def set_parent(self): + rebuild_tree('Quality Procedure', 'parent_quality_procedure') + for process in self.processes: # Set parent for only those children who don't have a parent parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure")