From 66806ff1b067e9f0dee38456f3e33f777ad7f078 Mon Sep 17 00:00:00 2001 From: Poranut Chollavorn Date: Thu, 18 Jun 2020 13:43:15 +0000 Subject: [PATCH 01/79] 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/79] 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/79] 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 1ee7ea6adee6633dd651f91fc556a6e291321d72 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 15 Sep 2020 21:58:36 +0530 Subject: [PATCH 04/79] fix: update items with workflow travis fix (#23335) --- erpnext/selling/doctype/sales_order/test_sales_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ae0faf2128b..31c328fdf3e 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -425,9 +425,9 @@ class TestSalesOrder(unittest.TestCase): frappe.set_user("Administrator") workflow = make_sales_order_workflow() so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) + frappe.set_user("Administrator") apply_workflow(so, 'Approve') - frappe.set_user("Administrator") user = 'test@example.com' test_user = frappe.get_doc('User', user) test_user.add_roles("Sales User", "Test Junior Approver") @@ -1010,7 +1010,7 @@ def make_sales_order_workflow(): "is_active": 1, "send_email_alert": 0, }) - workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) + workflow.append('states', dict( state = 'Pending', allow_edit = 'Administrator' )) workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) workflow.append('transitions', dict( state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, From 526814c041cef90aa23fd5817ade2a2fdf7caf79 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Sep 2020 10:28:33 +0530 Subject: [PATCH 05/79] fix: Fetch and set Item Tax Template via Update Items --- .../purchase_order/test_purchase_order.py | 47 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 20 ++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 86e1e973abb..8427e66a868 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -202,6 +202,53 @@ class TestPurchaseOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) frappe.set_user("Administrator") + def test_update_child_with_tax_template(self): + tax_template = "_Test Account Excise Duty @ 10" + item = "_Test Item Home Desktop 100" + + if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item_doc = frappe.get_doc("Item", item) + item_doc.append("taxes", { + "item_tax_template": tax_template, + "valid_from": nowdate() + }) + item_doc.save() + else: + # update valid from + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}) + + po = create_purchase_order(item_code=item, qty=1, do_not_save=1) + + po.append("taxes", { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Purchase Taxes and Charges", + "rate": 10 + }) + po.insert() + po.submit() + + self.assertEqual(po.taxes[0].tax_amount, 50) + self.assertEqual(po.taxes[0].total, 550) + + items = json.dumps([ + {'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name}, + {'item_code' : item, 'rate' : 100, 'qty' : 1} # added item + ]) + update_child_qty_rate('Purchase Order', items, po.name) + + po.reload() + self.assertEqual(po.taxes[0].tax_amount, 60) + self.assertEqual(po.taxes[0].total, 660) + + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}) + def test_update_qty(self): po = create_purchase_order() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fc535074005..65d815a9094 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -20,7 +20,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_t from erpnext.exceptions import InvalidCurrency from six import text_type from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions -from erpnext.stock.get_item_details import get_item_warehouse +from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map from erpnext.stock.doctype.packed_item.packed_item import make_packing_list force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") @@ -1127,6 +1127,18 @@ def get_supplier_block_status(party_name): } return info +def set_child_tax_template_and_map(item, child_item, parent_doc): + args = { + 'item_code': item.item_code, + 'posting_date': parent_doc.transaction_date, + 'tax_category': parent_doc.get('tax_category'), + 'company': parent_doc.get('company') + } + + child_item.item_tax_template = _get_item_tax_template(args, item.taxes) + if child_item.get("item_tax_template"): + child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True) + def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Sales Order Item child item containing the default values @@ -1140,6 +1152,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = item.stock_uom + set_child_tax_template_and_map(item, child_item, p_doc) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") @@ -1162,6 +1175,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.uom = item.stock_uom child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation + set_child_tax_template_and_map(item, child_item, p_doc) return child_item def validate_and_delete_children(parent, data): @@ -1199,7 +1213,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil frappe.throw(_("You do not have permissions to {} items in a {}.") .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) - + def validate_workflow_conditions(doc): workflow = get_workflow_name(doc.doctype) if not workflow: @@ -1234,7 +1248,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') validate_and_delete_children(parent, data) From 31a887f045fafe327df54d311d254165baaa7796 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 18 Sep 2020 11:23:35 +0530 Subject: [PATCH 06/79] 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 d57a94e2f222adb5a6c5852738f613a68af0a07b Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 18 Sep 2020 13:26:46 +0530 Subject: [PATCH 07/79] fix: No handlefor Cost centers in Accounts Receivable --- .../report/accounts_receivable/accounts_receivable.js | 2 +- .../report/accounts_receivable/accounts_receivable.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index a49cc321495..65d13749d32 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -69,7 +69,7 @@ frappe.query_reports["Accounts Receivable"] = { filters: { 'company': company } - } + }; } }, { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 04fc33220d9..f6632fa2632 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -617,9 +617,19 @@ class ReceivablePayableReport(object): elif party_type_field=="supplier": self.add_supplier_filters(conditions, values) + if self.filters.cost_center: + self.get_cost_center_conditions(conditions) + self.add_accounting_dimensions_filters(conditions, values) return " and ".join(conditions), values + def get_cost_center_conditions(self, conditions): + lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) + cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})] + + cost_center_string = '", "'.join(cost_center_list) + conditions.append('cost_center in ("{0}")'.format(cost_center_string)) + def get_order_by_condition(self): if self.filters.get('group_by_party'): return "order by party, posting_date" From 3748cbb32cc000678db9b190fb1fb4bad163fc03 Mon Sep 17 00:00:00 2001 From: Saqib Date: Sun, 20 Sep 2020 19:31:46 +0530 Subject: [PATCH 08/79] fix: depreciation start date ux fixes (#23340) --- erpnext/assets/doctype/asset/asset.js | 16 +- erpnext/assets/doctype/asset/asset.py | 9 +- erpnext/assets/doctype/asset/test_asset.py | 18 +- .../asset_finance_book.json | 420 ++++-------------- .../asset_movement/test_asset_movement.py | 6 +- .../purchase_receipt/test_purchase_receipt.py | 3 +- 6 files changed, 113 insertions(+), 359 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index a53ff881777..04ef68357f2 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -252,13 +252,6 @@ frappe.ui.form.on('Asset', { }) }, - available_for_use_date: function(frm) { - $.each(frm.doc.finance_books || [], function(i, d) { - if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date; - }); - refresh_field("finance_books"); - }, - is_existing_asset: function(frm) { frm.trigger("toggle_reference_doc"); // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); @@ -437,6 +430,15 @@ frappe.ui.form.on('Asset Finance Book', { } frappe.flags.dont_change_rate = false; + }, + + depreciation_start_date: function(frm, cdt, cdn) { + const book = locals[cdt][cdn]; + if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) { + frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`)); + book.depreciation_start_date = ""; + frm.refresh_field("finance_books"); + } } }); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2a9e4db32b9..13fb2665982 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext, math, json from frappe import _ from six import string_types -from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days +from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day from frappe.model.document import Document from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset.depreciation \ @@ -84,6 +84,11 @@ class Asset(AccountsController): if not self.available_for_use_date: frappe.throw(_("Available for use date is required")) + for d in self.finance_books: + if d.depreciation_start_date == self.available_for_use_date: + frappe.throw(_("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format(d.idx), + title=_("Incorrect Date")) + def set_missing_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") @@ -308,7 +313,7 @@ class Asset(AccountsController): if not row.depreciation_start_date: if not self.available_for_use_date: frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) - row.depreciation_start_date = self.available_for_use_date + row.depreciation_start_date = get_last_day(self.available_for_use_date) if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index e8def53c070..d914dabc123 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -374,19 +374,18 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = nowdate() - asset.purchase_date = nowdate() + asset.available_for_use_date = '2020-01-01' + asset.purchase_date = '2020-01-01' asset.append("finance_books", { "expected_value_after_useful_life": 10000, "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": nowdate() + "total_number_of_depreciations": 10, + "frequency_of_depreciation": 1 }) asset.insert() asset.submit() - post_depreciation_entries(date=add_months(nowdate(), 10)) + post_depreciation_entries(date=add_months('2020-01-01', 4)) scrap_asset(asset.name) @@ -395,9 +394,9 @@ class TestAsset(unittest.TestCase): self.assertTrue(asset.journal_entry_for_scrap) expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) + ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0) ) gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` @@ -469,8 +468,7 @@ class TestAsset(unittest.TestCase): "expected_value_after_useful_life": 10000, "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) asset.insert() accumulated_depreciation_after_full_schedule = \ diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index c80f95e1555..79fcb957d4d 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -1,347 +1,99 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-05-08 14:44:37.095570", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2018-05-08 14:44:37.095570", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "finance_book", + "depreciation_method", + "total_number_of_depreciations", + "column_break_5", + "frequency_of_depreciation", + "depreciation_start_date", + "expected_value_after_useful_life", + "value_after_depreciation", + "rate_of_depreciation" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "finance_book", - "fieldtype": "Link", - "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": "Finance Book", - "length": 0, - "no_copy": 0, - "options": "Finance Book", - "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": "finance_book", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Finance Book", + "options": "Finance Book" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "depreciation_method", - "fieldtype": "Select", - "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": "Depreciation Method", - "length": 0, - "no_copy": 0, - "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", - "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, - "unique": 0 - }, + "fieldname": "depreciation_method", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Depreciation Method", + "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_number_of_depreciations", - "fieldtype": "Int", - "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": "Total Number of Depreciations", - "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, - "unique": 0 - }, + "fieldname": "total_number_of_depreciations", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Total Number of Depreciations", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_5", - "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, - "label": "", - "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_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "frequency_of_depreciation", - "fieldtype": "Int", - "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": "Frequency of Depreciation (Months)", - "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, - "unique": 0 - }, + "fieldname": "frequency_of_depreciation", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Frequency of Depreciation (Months)", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:parent.doctype == 'Asset'", - "fetch_if_empty": 0, - "fieldname": "depreciation_start_date", - "fieldtype": "Date", - "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": "Depreciation Start Date", - "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:parent.doctype == 'Asset'", + "fieldname": "depreciation_start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Depreciation Posting Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "depends_on": "eval:parent.doctype == 'Asset'", - "fetch_if_empty": 0, - "fieldname": "expected_value_after_useful_life", - "fieldtype": "Currency", - "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": "Expected Value After Useful Life", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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", + "depends_on": "eval:parent.doctype == 'Asset'", + "fieldname": "expected_value_after_useful_life", + "fieldtype": "Currency", + "label": "Expected Value After Useful Life", + "options": "Company:company:default_currency" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "value_after_depreciation", - "fieldtype": "Currency", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Value After Depreciation", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "value_after_depreciation", + "fieldtype": "Currency", + "hidden": 1, + "label": "Value After Depreciation", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.depreciation_method == 'Written Down Value'", - "description": "In Percentage", - "fetch_if_empty": 0, - "fieldname": "rate_of_depreciation", - "fieldtype": "Percent", - "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": "Rate of Depreciation", - "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.depreciation_method == 'Written Down Value'", + "description": "In Percentage", + "fieldname": "rate_of_depreciation", + "fieldtype": "Percent", + "label": "Rate of Depreciation" } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-04-09 19:45:14.523488", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Finance Book", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-16 12:11:30.631788", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Finance Book", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index c3755a3fb9a..cddee5fa0f1 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -32,8 +32,7 @@ class TestAssetMovement(unittest.TestCase): "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) if asset.docstatus == 0: @@ -82,8 +81,7 @@ class TestAssetMovement(unittest.TestCase): "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) if asset.docstatus == 0: asset.submit() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c9cda37c40b..7c244ea5023 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -471,8 +471,7 @@ class TestPurchaseReceipt(unittest.TestCase): "expected_value_after_useful_life": 10, "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 1, - "depreciation_start_date": frappe.utils.nowdate() + "frequency_of_depreciation": 1 }) asset.submit() From f5fd2cd4a2ab3c5645f539b4502715288dc9432c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Sep 2020 01:20:00 +0530 Subject: [PATCH 09/79] fix: stock reconciliation, incorrect serial nos fetched in the current serial no field --- .../stock_reconciliation/stock_reconciliation.py | 13 +++++++++---- erpnext/stock/stock_ledger.py | 8 +++++++- erpnext/stock/utils.py | 6 +----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 170a62c8846..ca59e67a676 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -165,9 +165,12 @@ class StockReconciliation(StockController): validate_is_stock_item(item_code, item.is_stock_item, verbose=0) # item should not be serialized - if item.has_serial_no and not row.serial_no and not item.serial_no_series: + if item.has_serial_no and not row.serial_no and not item.serial_no_series and flt(row.qty) > 0: raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code)) + if flt(row.qty) == 0 and row.serial_no: + 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)) @@ -235,7 +238,7 @@ class StockReconciliation(StockController): sl_entries = self.merge_similar_item_serial_nos(sl_entries) def issue_existing_serial_and_batch(self, sl_entries): - from erpnext.stock.stock_ledger import get_previous_sle + from erpnext.stock.stock_ledger import get_stock_ledger_entries for row in self.items: serial_nos = get_serial_nos(row.serial_no) or [] @@ -261,12 +264,14 @@ class StockReconciliation(StockController): for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) - previous_sle = get_previous_sle({ + previous_sle = get_stock_ledger_entries({ "item_code": row.item_code, "posting_date": self.posting_date, "posting_time": self.posting_time, "serial_no": serial_no - }) + }, "<", "desc", "limit 1") + + previous_sle = previous_sle and previous_sle[0] or {} if previous_sle and row.warehouse != previous_sle.get("warehouse"): # If serial no exists in different warehouse diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ba885c09487..5c4bba730e3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -460,7 +460,13 @@ def get_stock_ledger_entries(previous_sle, operator=None, conditions += " and " + previous_sle.get("warehouse_condition") if check_serial_no and previous_sle.get("serial_no"): - conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no")))) + serial_no = previous_sle.get("serial_no") + conditions += """ and ( + serial_no = '{0}' + OR serial_no like '{0}\n%%' + OR serial_no like '%%\n{0}' + OR serial_no like '%%\n{0}\n%%' + ) and actual_qty > 0""".format(serial_no) if not previous_sle.get("posting_date"): previous_sle["posting_date"] = "1900-01-01" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 95ecb7fecd7..db39bae8a63 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -96,11 +96,7 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None if with_valuation_rate: if with_serial_no: - serial_nos = last_entry.get("serial_no") - - if (serial_nos and - len(get_serial_nos_data(serial_nos)) < last_entry.qty_after_transaction): - serial_nos = get_serial_nos_data_after_transactions(args) + serial_nos = get_serial_nos_data_after_transactions(args) return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos) if last_entry else (0.0, 0.0, 0.0)) From e71e4cf8b5014767013f9acf0e34943f6ea7b531 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Sep 2020 12:26:35 +0530 Subject: [PATCH 10/79] refactor: enabled no copy property for Supplier Invoice Date to avoid due date validation --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index fbd4dee4d66..637aa80f8a6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -356,6 +356,7 @@ "fieldname": "bill_date", "fieldtype": "Date", "label": "Supplier Invoice Date", + "no_copy": 1, "oldfieldname": "bill_date", "oldfieldtype": "Date", "print_hide": 1 @@ -1307,7 +1308,7 @@ "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, - "modified": "2020-08-20 11:08:19.611710", + "modified": "2020-09-21 12:22:09.164068", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", From af585aae28239515640d3c6fc0145b7c7a7bd472 Mon Sep 17 00:00:00 2001 From: Rohan Date: Mon, 21 Sep 2020 16:08:55 +0530 Subject: [PATCH 11/79] fix: use Plaid's new API (v12) (#23317) * fix: use Plaid's new API * fix: requested changes --- .../bank_reconciliation.js | 38 ++++---- .../doctype/plaid_settings/plaid_connector.py | 97 ++++++++++--------- .../doctype/plaid_settings/plaid_settings.js | 57 +++++------ .../plaid_settings/plaid_settings.json | 11 +-- .../doctype/plaid_settings/plaid_settings.py | 50 ++++++---- .../plaid_settings/test_plaid_settings.py | 21 ++-- .../v12_0/move_plaid_settings_to_doctype.py | 5 +- requirements.txt | 2 +- 8 files changed, 149 insertions(+), 132 deletions(-) diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js index efc76f9158b..97035278754 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js @@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation { check_plaid_status() { const me = this; frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { - if (r && r.enabled == "1") { + if (r && r.enabled === "1") { me.plaid_status = "active" } else { me.plaid_status = "inactive" @@ -139,7 +139,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { } make() { - const me = this; + const me = this; new frappe.ui.FileUploader({ method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement', allow_multiple: 0, @@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync { init_config() { const me = this; - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration') - .then(result => { - me.plaid_env = result.plaid_env; - me.plaid_public_key = result.plaid_public_key; - me.client_name = result.client_name; - me.sync_transactions() - }) + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration') + .then(result => { + me.plaid_env = result.plaid_env; + me.client_name = result.client_name; + me.link_token = result.link_token; + me.sync_transactions(); + }) } sync_transactions() { const me = this; - frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => { + frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => { frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', { - bank: v['bank'], + bank: r.bank, bank_account: me.parent.bank_account, freeze: true }) .then((result) => { - let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized") + let result_title = (result && result.length > 0) + ? __("{0} bank transaction(s) created", [result.length]) + : __("This bank account is already synchronized"); + let result_msg = ` -
-
${result_title}
-
` +
+
${result_title}
+
` + this.parent.$main_section.append(result_msg) - frappe.show_alert({message:__("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator:'green'}); + frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' }); }) }) } @@ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { }) frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments', - {bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")} + { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") } ).then((result) => { me.make_dialog(result) }) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 532e19cffd9..4e23e6c9c30 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -2,81 +2,84 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -from frappe import _ -from frappe.utils.password import get_decrypted_password -from plaid import Client -from plaid.errors import APIError, ItemError +import plaid +import requests +from plaid.errors import APIError, ItemError, InvalidRequestError import frappe -import requests +from frappe import _ + class PlaidConnector(): def __init__(self, access_token=None): - - plaid_settings = frappe.get_single("Plaid Settings") - - self.config = { - "plaid_client_id": plaid_settings.plaid_client_id, - "plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'), - "plaid_public_key": plaid_settings.plaid_public_key, - "plaid_env": plaid_settings.plaid_env - } - - self.client = Client(client_id=self.config.get("plaid_client_id"), - secret=self.config.get("plaid_secret"), - public_key=self.config.get("plaid_public_key"), - environment=self.config.get("plaid_env") - ) - self.access_token = access_token + self.settings = frappe.get_single("Plaid Settings") + self.products = ["auth", "transactions"] + self.client_name = frappe.local.site + self.client = plaid.Client( + client_id=self.settings.plaid_client_id, + secret=self.settings.get_password("plaid_secret"), + environment=self.settings.plaid_env, + api_version="2019-05-29" + ) def get_access_token(self, public_token): if public_token is None: frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error")) - response = self.client.Item.public_token.exchange(public_token) - access_token = response['access_token'] - + access_token = response["access_token"] return access_token + def get_link_token(self): + token_request = { + "client_name": self.client_name, + "client_id": self.settings.plaid_client_id, + "secret": self.settings.plaid_secret, + "products": self.products, + # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) + "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", + "country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"], + "user": { + "client_user_id": frappe.generate_hash(frappe.session.user, length=32) + } + } + + try: + response = self.client.LinkToken.create(token_request) + except InvalidRequestError as e: + frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error")) + frappe.msgprint(_("Please check your Plaid client ID and secret values")) + except APIError as e: + frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) + frappe.throw(_(str(e)), title=_("Authentication Failed")) + else: + return response["link_token"] + def auth(self): try: self.client.Auth.get(self.access_token) - print("Authentication successful.....") except ItemError as e: - if e.code == 'ITEM_LOGIN_REQUIRED': - pass - else: + if e.code == "ITEM_LOGIN_REQUIRED": pass except APIError as e: - if e.code == 'PLANNED_MAINTENANCE': - pass - else: + if e.code == "PLANNED_MAINTENANCE": pass except requests.Timeout: pass except Exception as e: - print(e) frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) - frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'}) + frappe.throw(_(str(e)), title=_("Authentication Failed")) def get_transactions(self, start_date, end_date, account_id=None): + self.auth() + account_ids = list(account_id) if account_id else None + try: - self.auth() - if account_id: - account_ids = [account_id] - - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids) - - else: - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date) - - transactions = response['transactions'] - - while len(transactions) < response['total_transactions']: + response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids) + transactions = response["transactions"] + while len(transactions) < response["total_transactions"]: response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) - transactions.extend(response['transactions']) + transactions.extend(response["transactions"]) return transactions except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 0ffbb877ea7..22a4004955f 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -4,14 +4,14 @@ frappe.provide("erpnext.integrations"); frappe.ui.form.on('Plaid Settings', { - enabled: function(frm) { + enabled: function (frm) { frm.toggle_reqd('plaid_client_id', frm.doc.enabled); frm.toggle_reqd('plaid_secret', frm.doc.enabled); - frm.toggle_reqd('plaid_public_key', frm.doc.enabled); frm.toggle_reqd('plaid_env', frm.doc.enabled); }, - refresh: function(frm) { - if(frm.doc.enabled) { + + refresh: function (frm) { + if (frm.doc.enabled) { frm.add_custom_button('Link a new bank account', () => { new erpnext.integrations.plaidLink(frm); }); @@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', { erpnext.integrations.plaidLink = class plaidLink { constructor(parent) { this.frm = parent; - this.product = ["transactions", "auth"]; this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; this.init_config(); } - init_config() { - const me = this; - me.plaid_env = me.frm.doc.plaid_env; - me.plaid_public_key = me.frm.doc.plaid_public_key; - me.client_name = frappe.boot.sitename; - me.init_plaid(); + async init_config() { + this.product = ["auth", "transactions"]; + this.plaid_env = this.frm.doc.plaid_env; + this.client_name = frappe.boot.sitename; + this.token = await this.frm.call("get_link_token").then(resp => resp.message); + this.init_plaid(); } init_plaid() { @@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink { } onScriptLoaded(me) { - me.linkHandler = window.Plaid.create({ + me.linkHandler = Plaid.create({ clientName: me.client_name, + product: me.product, env: me.plaid_env, - key: me.plaid_public_key, - onSuccess: me.plaid_success, - product: me.product + token: me.token, + onSuccess: me.plaid_success }); } onScriptError(error) { - frappe.msgprint('There was an issue loading the link-initialize.js script'); + frappe.msgprint("There was an issue connecting to Plaid's authentication server"); frappe.msgprint(error); } @@ -87,21 +86,25 @@ erpnext.integrations.plaidLink = class plaidLink { const me = this; frappe.prompt({ - fieldtype:"Link", + fieldtype: "Link", options: "Company", - label:__("Company"), - fieldname:"company", - reqd:1 + label: __("Company"), + fieldname: "company", + reqd: 1 }, (data) => { me.company = data.company; - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response}) - .then((result) => { - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response, - bank: result, company: me.company}); - }) - .then(() => { - frappe.show_alert({message:__("Bank accounts added"), indicator:'green'}); + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', { + token: token, + response: response + }).then((result) => { + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', { + response: response, + bank: result, + company: me.company }); + }).then(() => { + frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' }); + }); }, __("Select a company"), __("Continue")); } }; diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json index d8203d7390f..27062172239 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2018-10-25 10:02:48.656165", "doctype": "DocType", "editable_grid": 1, @@ -12,7 +11,6 @@ "plaid_client_id", "plaid_secret", "column_break_7", - "plaid_public_key", "plaid_env" ], "fields": [ @@ -41,12 +39,6 @@ "in_list_view": 1, "label": "Plaid Secret" }, - { - "fieldname": "plaid_public_key", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Plaid Public Key" - }, { "fieldname": "plaid_env", "fieldtype": "Select", @@ -69,8 +61,7 @@ } ], "issingle": 1, - "links": [], - "modified": "2020-02-07 15:21:11.616231", + "modified": "2020-09-12 02:31:44.542385", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Plaid Settings", diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 81fb9843f61..3afccf95b8e 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -2,30 +2,36 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import json -from frappe import _ -from frappe.model.document import Document + +import frappe from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector -from frappe.utils import getdate, formatdate, today, add_months +from frappe import _ from frappe.desk.doctype.tag.tag import add_tag +from frappe.model.document import Document +from frappe.utils import add_months, formatdate, getdate, today + class PlaidSettings(Document): - pass + @staticmethod + def get_link_token(): + plaid = PlaidConnector() + return plaid.get_link_token() + @frappe.whitelist() -def plaid_configuration(): +def get_plaid_configuration(): if frappe.db.get_single_value("Plaid Settings", "enabled"): plaid_settings = frappe.get_single("Plaid Settings") return { - "plaid_public_key": plaid_settings.plaid_public_key, "plaid_env": plaid_settings.plaid_env, + "link_token": plaid_settings.get_link_token(), "client_name": frappe.local.site } - else: - return "disabled" + + return "disabled" + @frappe.whitelist() def add_institution(token, response): @@ -33,6 +39,7 @@ def add_institution(token, response): plaid = PlaidConnector() access_token = plaid.get_access_token(token) + bank = None if not frappe.db.exists("Bank", response["institution"]["name"]): try: @@ -44,7 +51,6 @@ def add_institution(token, response): bank.insert() except Exception: frappe.throw(frappe.get_traceback()) - else: bank = frappe.get_doc("Bank", response["institution"]["name"]) bank.plaid_access_token = access_token @@ -52,6 +58,7 @@ def add_institution(token, response): return bank + @frappe.whitelist() def add_bank_accounts(response, bank, company): try: @@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company): new_account.insert() result.append(new_account.name) - except frappe.UniqueValidationError: - frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name)) + frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"])) except Exception: frappe.throw(frappe.get_traceback()) @@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company): return result + def add_account_type(account_type): try: frappe.get_doc({ @@ -122,10 +129,11 @@ def add_account_subtype(account_subtype): except Exception: frappe.throw(frappe.get_traceback()) + @frappe.whitelist() def sync_transactions(bank, bank_account): - ''' Sync transactions based on the last integration date as the start date, after sync is completed - add the transaction date of the oldest transaction as the last integration date ''' + """Sync transactions based on the last integration date as the start date, after sync is completed + add the transaction date of the oldest transaction as the last integration date.""" last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date") if last_transaction_date: @@ -148,10 +156,10 @@ def sync_transactions(bank, bank_account): len(result), bank_account, start_date, end_date)) frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date) - except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) + def get_transactions(bank, bank_account=None, start_date=None, end_date=None): access_token = None @@ -169,6 +177,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): return transactions + def new_bank_transaction(transaction): result = [] @@ -183,8 +192,8 @@ def new_bank_transaction(transaction): status = "Pending" if transaction["pending"] == "True" else "Settled" + tags = [] try: - tags = [] tags += transaction["category"] tags += ["Plaid Cat. {}".format(transaction["category_id"])] except KeyError: @@ -217,6 +226,7 @@ def new_bank_transaction(transaction): return result + def automatic_synchronization(): settings = frappe.get_doc("Plaid Settings", "Plaid Settings") @@ -224,4 +234,8 @@ def automatic_synchronization(): plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) for plaid_account in plaid_accounts: - frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name) + frappe.enqueue( + "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", + bank=plaid_account.bank, + bank_account=plaid_account.name + ) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py index 29e8fa4fec8..9f0bb92f537 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import unittest -import frappe -from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts import json -from frappe.utils.response import json_handler +import unittest + +import frappe from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account +from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import ( + add_account_subtype, add_account_type, add_bank_accounts, + new_bank_transaction, get_plaid_configuration) +from frappe.utils.response import json_handler + class TestPlaidSettings(unittest.TestCase): def setUp(self): @@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase): def test_plaid_disabled(self): frappe.db.set_value("Plaid Settings", None, "enabled", 0) - self.assertTrue(plaid_configuration() == "disabled") + self.assertTrue(get_plaid_configuration() == "disabled") def test_add_account_type(self): add_account_type("brokerage") @@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase): 'mask': '0000', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'name': 'Plaid Checking' - }], + }], 'institution': { 'institution_id': 'ins_6', 'name': 'Citi' @@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase): 'mask': '0000', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'name': 'Plaid Checking' - }], + }], 'institution': { 'institution_id': 'ins_6', 'name': 'Citi' @@ -152,4 +155,4 @@ class TestPlaidSettings(unittest.TestCase): new_bank_transaction(transactions) - self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1) \ No newline at end of file + self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1) diff --git a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py index 8e60d33f850..d2bcb12070c 100644 --- a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py +++ b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py @@ -4,17 +4,16 @@ from __future__ import unicode_literals import frappe + def execute(): frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings") plaid_settings = frappe.get_single("Plaid Settings") if plaid_settings.enabled: - if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \ - and frappe.conf.plaid_public_key and frappe.conf.plaid_secret): + if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret): plaid_settings.enabled = 0 else: plaid_settings.update({ "plaid_client_id": frappe.conf.plaid_client_id, - "plaid_public_key": frappe.conf.plaid_public_key, "plaid_env": frappe.conf.plaid_env, "plaid_secret": frappe.conf.plaid_secret }) diff --git a/requirements.txt b/requirements.txt index c277545fab5..f807fa6c29d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ frappe gocardless-pro==1.11.0 googlemaps==3.1.1 pandas==0.24.2 -plaid-python==3.4.0 +plaid-python==6.0.0 PyGithub==1.44.1 python-stdnum==1.12 Unidecode==1.1.1 From 19365d6d0754c0455ade3392a88c97dab7c4d9d3 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 21 Sep 2020 18:04:34 +0530 Subject: [PATCH 12/79] fix(plaid): do not send null list in account ids (#23375) --- .../doctype/plaid_settings/plaid_connector.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 4e23e6c9c30..a033a2a722d 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -46,7 +46,7 @@ class PlaidConnector(): try: response = self.client.LinkToken.create(token_request) - except InvalidRequestError as e: + except InvalidRequestError: frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error")) frappe.msgprint(_("Please check your Plaid client ID and secret values")) except APIError as e: @@ -72,10 +72,16 @@ class PlaidConnector(): def get_transactions(self, start_date, end_date, account_id=None): self.auth() - account_ids = list(account_id) if account_id else None + kwargs = dict( + access_token=self.access_token, + start_date=start_date, + end_date=end_date + ) + if account_id: + kwargs.update(dict(account_ids=[account_id])) try: - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids) + response = self.client.Transactions.get(**kwargs) transactions = response["transactions"] while len(transactions) < response["total_transactions"]: response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) From 088e1e66ac69b0e112edce6b6799a87cc44d852e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 21 Sep 2020 18:26:52 +0530 Subject: [PATCH 13/79] fix: online pos print not working --- .../selling/page/point_of_sale/point_of_sale.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 30790f0a987..f1728b7afda 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -449,8 +449,7 @@ erpnext.pos.PointOfSale = class PointOfSale { $(this.frm.msgbox.body).find('.btn-primary').on('click', () => { this.frm.msgbox.hide(); - const frm = this.events.get_frm(); - frm.doc = this.doc; + const frm = this.frm; frm.print_preview.lang_code = frm.doc.language; frm.print_preview.printit(true); }); @@ -688,8 +687,8 @@ erpnext.pos.PointOfSale = class PointOfSale { if(this.frm.doc.docstatus != 1 ){ await this.frm.save(); } - const frm = this.events.get_frm(); - frm.doc = this.doc; + + const frm = this.frm; frm.print_preview.lang_code = frm.doc.language; frm.print_preview.printit(true); }); @@ -948,8 +947,12 @@ class POSCart { } }, onchange: () => { - this.events.on_customer_change(this.customer_field.get_value()); - this.events.get_loyalty_details(); + let customer = this.customer_field.get_value(); + frappe.db.get_value("Customer", customer, "language", (r) => { + this.frm.doc.language = r ? r.language : "en-US"; + this.events.on_customer_change(customer); + this.events.get_loyalty_details(); + }); } }, parent: this.wrapper.find('.customer-field'), From b53a84b07f333e6352295201e19e465d64b810fc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 22 Sep 2020 10:36:52 +0530 Subject: [PATCH 14/79] fix: incorrect consumed qty if raw material with batch --- erpnext/controllers/buying_controller.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 8ed76421894..6e05a312352 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -289,7 +289,7 @@ class BuyingController(StockController): title=_("Limit Crossed")) transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) - backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) + # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) for raw_material in transferred_raw_materials + non_stock_items: rm_item_key = (raw_material.rm_item_code, item.purchase_order) @@ -321,6 +321,8 @@ class BuyingController(StockController): set_serial_nos(raw_material, consumed_serial_nos, qty) if raw_material.batch_nos: + backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {}) + batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) for batch_data in batches_qty: @@ -884,7 +886,8 @@ def get_backflushed_subcontracted_raw_materials(purchase_orders): backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({ "qty": 0.0, "serial_no": [], - "batch_no": [] + "batch_no": [], + "consumed_batch": {} })) row = backflushed_raw_materials_map.get(pr_key) @@ -894,6 +897,12 @@ def get_backflushed_subcontracted_raw_materials(purchase_orders): if data.get(field): row[field].append(data.get(field)) + if data.get("batch_no"): + if data.get("batch_no") in row.consumed_batch: + row.consumed_batch[data.get("batch_no")] += data.consumed_qty + else: + row.consumed_batch[data.get("batch_no")] = data.consumed_qty + return backflushed_raw_materials_map def get_supplied_items(item_code, purchase_receipt, references): @@ -1038,14 +1047,12 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item): return backflushed_batch_qty_map -def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map, po): +def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po): # Returns available batches to be backflushed based on requirements transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) if not transferred_batches: transferred_batches = transferred_batch_qty_map.get((item_code, po), {}) - backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {}) - available_batches = [] for (batch, transferred_qty) in transferred_batches.items(): From ba271317c7466c8abd8a0addedaaadc0acc04f36 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 22 Sep 2020 13:45:15 +0530 Subject: [PATCH 15/79] fix: adding filters validation Batch-Wise Balance History --- .../batch_wise_balance_history/batch_wise_balance_history.js | 4 +++- .../batch_wise_balance_history/batch_wise_balance_history.py | 3 +++ 2 files changed, 6 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 b23c908e07a..84e95e27ca0 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 @@ -9,13 +9,15 @@ frappe.query_reports["Batch-Wise Balance History"] = { "fieldtype": "Date", "width": "80", "default": frappe.sys_defaults.year_start_date, + "reqd": 1 }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", "width": "80", - "default": frappe.datetime.get_today() + "default": frappe.datetime.get_today(), + "reqd": 1 } ] } \ 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 7f7835f74ee..ec2ef35bb41 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 @@ -9,6 +9,9 @@ from frappe.utils import flt, cint, getdate def execute(filters=None): if not filters: filters = {} + if filters.from_date > filters.to_date: + frappe.throw(_("From Date must be before To Date")) + float_precision = cint(frappe.db.get_default("float_precision")) or 3 columns = get_columns(filters) From 9a7e433a9d97190e44615a291f7137b420a0ede4 Mon Sep 17 00:00:00 2001 From: Anupam Date: Tue, 22 Sep 2020 13:00:09 +0530 Subject: [PATCH 16/79] 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 a78cd20e315f3aa0e49e1a17fdaf12b4b8a6ba3a Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 22 Sep 2020 15:04:03 +0530 Subject: [PATCH 17/79] fix: ignore permission while creating supplier scorecard period in supplier scorecard --- erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py | 1 + .../supplier_scorecard_period/supplier_scorecard_period.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py index af109ba2848..e956afdf749 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py @@ -178,6 +178,7 @@ def make_all_scorecards(docname): period_card = make_supplier_scorecard(docname, None) period_card.start_date = start_date period_card.end_date = end_date + period_card.insert(ignore_permissions=True) period_card.submit() scp_count = scp_count + 1 if start_date < first_start_date: diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py index 737ddd6ddd5..35a78cb98fc 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -106,7 +106,7 @@ def make_supplier_scorecard(source_name, target_doc=None): "doctype": "Supplier Scorecard Scoring Criteria", "postprocess": update_criteria_fields, } - }, target_doc, post_process) + }, target_doc, post_process, ignore_permissions=True) return doc From 38000aacc537e4ad2a3a2a996a2c9ea53326dbb3 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 22 Sep 2020 16:51:58 +0530 Subject: [PATCH 18/79] fix: warehouse address filtered based on warehouse (#23381) Co-authored-by: Marica --- .../stock/doctype/stock_entry/stock_entry.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 529602f2b9c..518f87fbe2a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -24,6 +24,24 @@ frappe.ui.form.on('Stock Entry', { } }); + frm.set_query('source_warehouse_address', function() { + return { + filters: { + link_doctype: 'Warehouse', + link_name: frm.doc.from_warehouse + } + } + }); + + frm.set_query('target_warehouse_address', function() { + return { + filters: { + link_doctype: 'Warehouse', + link_name: frm.doc.to_warehouse + } + } + }); + frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => { if (r.sample_retention_warehouse) { var filters = [ From b948cf204caa629c067c6d29145b49a27eddc5bc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 22 Sep 2020 19:47:33 +0530 Subject: [PATCH 19/79] fix: Download Required Materials not working for production plan --- .../doctype/production_plan/production_plan.js | 6 ++++-- .../doctype/production_plan/production_plan.py | 9 ++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 2b168d1d76d..cb8d3a02068 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -159,6 +159,7 @@ frappe.ui.form.on('Production Plan', { get_sales_orders: function(frm) { frappe.call({ method: "get_open_sales_orders", + freeze: true, doc: frm.doc, callback: function(r) { refresh_field("sales_orders"); @@ -169,6 +170,7 @@ frappe.ui.form.on('Production Plan', { get_material_request: function(frm) { frappe.call({ method: "get_pending_material_requests", + freeze: true, doc: frm.doc, callback: function() { refresh_field('material_requests'); @@ -188,7 +190,7 @@ frappe.ui.form.on('Production Plan', { }, get_items_for_mr: function(frm) { - const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', + const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type']; frappe.call({ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", @@ -219,7 +221,7 @@ frappe.ui.form.on('Production Plan', { download_materials_required: function(frm) { let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; - open_url_post(frappe.request.url, { cmd: get_template_url, production_plan: frm.doc.name }); + open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); }, show_progress: function(frm) { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 02dfabe6f70..aa80dcfed24 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -422,14 +422,13 @@ class ProductionPlan(Document): msgprint(_("No material request created")) @frappe.whitelist() -def download_raw_materials(production_plan): - doc = frappe.get_doc('Production Plan', production_plan) - doc.check_permission() - +def download_raw_materials(doc): item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse', 'projected Qty', 'Actual Qty']] - doc = doc.as_dict() + if isinstance(doc, string_types): + doc = frappe._dict(json.loads(doc)) + for d in get_items_for_material_requests(doc, ignore_existing_ordered_qty=True): item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('quantity'), d.get('warehouse'), d.get('projected_qty'), d.get('actual_qty')]) From aad4bea366f281bdca5998f189a79f86249167b9 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 23 Sep 2020 13:01:30 +0530 Subject: [PATCH 20/79] fix: failed workflow condition error message in update items (#23392) --- erpnext/controllers/accounts_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 65d815a9094..bc90011bf12 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1231,7 +1231,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil transitions.append(transition.as_dict()) if not transitions: - frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions")) + frappe.throw( + _("You are not allowed to update as per the conditions set in {} Workflow.").format(get_link_to_form("Workflow", workflow)), + title=_("Insufficient Permissions") + ) def get_new_child_item(item_row): new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults From f31e358df90af5f38f4efd8ac0aab607a1d3c258 Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 23 Sep 2020 19:14:31 +0530 Subject: [PATCH 21/79] fix: leave application status fix --- erpnext/hr/doctype/leave_application/leave_application.json | 4 ++-- erpnext/hr/doctype/leave_application/leave_application.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json index 9cc9c87f7f8..ee85a8437ee 100644 --- a/erpnext/hr/doctype/leave_application/leave_application.json +++ b/erpnext/hr/doctype/leave_application/leave_application.json @@ -166,7 +166,6 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "default": "Open", "fieldname": "status", "fieldtype": "Select", @@ -245,9 +244,10 @@ ], "icon": "fa fa-calendar", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, "max_attachments": 3, - "modified": "2020-08-13 17:22:44.832397", + "modified": "2020-09-23 19:11:58.806837", "modified_by": "Administrator", "module": "HR", "name": "Leave Application", diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 6c42c4752b7..0b45a1d8291 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -56,7 +56,7 @@ class LeaveApplication(Document): def on_cancel(self): self.create_leave_ledger_entry(submit=False) - self.status = "Cancelled" + self.db_set("status", "Cancelled") # notify leave applier about cancellation self.notify_employee() self.cancel_attendance() From 9e3764ab62efad97c5a04a14a5160f6bc38861b3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 23 Sep 2020 19:35:49 +0530 Subject: [PATCH 22/79] refactor: book loss amount in the COGS instead of stock received but not billed --- 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 be2453373e4..42cc473b18d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -207,6 +207,7 @@ 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") @@ -288,7 +289,7 @@ class PurchaseReceipt(BuyingController): if self.is_return or flt(d.item_tax_amount): loss_account = expenses_included_in_valuation else: - loss_account = stock_rbnb + loss_account = cogs_account gl_entries.append(self.get_gl_dict({ "account": loss_account, From abd4b04cd95e655e8d7a8ec1347083c09ebc47d8 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 24 Sep 2020 12:07:43 +0530 Subject: [PATCH 23/79] fix: Check Company in Payment Entry before selecting values (#23420) --- .../doctype/payment_entry/payment_entry.js | 18 ++++++++++++++++-- erpnext/public/js/controllers/accounts.js | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 70c485133f0..3883637e363 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', { setup: function(frm) { frm.set_query("paid_from", function() { + frm.events.validate_company(frm); + var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; - return { filters: { "account_type": ["in", account_types], @@ -23,13 +24,16 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("party_type", function() { + frm.events.validate_company(frm); return{ filters: { "name": ["in", Object.keys(frappe.boot.party_account_types)], } } }); + frm.set_query("party_bank_account", function() { return { filters: { @@ -39,6 +43,7 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("bank_account", function() { return { filters: { @@ -46,6 +51,7 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("contact_person", function() { if (frm.doc.party) { return { @@ -57,10 +63,12 @@ frappe.ui.form.on('Payment Entry', { }; } }); + frm.set_query("paid_to", function() { + frm.events.validate_company(frm); + var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; - return { filters: { "account_type": ["in", account_types], @@ -149,6 +157,12 @@ frappe.ui.form.on('Payment Entry', { frm.events.show_general_ledger(frm); }, + validate_company: (frm) => { + if (!frm.doc.company){ + frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}); + } + }, + company: function(frm) { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index f4eaad58dae..6e97d811fc1 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -120,7 +120,7 @@ frappe.ui.form.on('Salary Structure', { var get_payment_mode_account = function(frm, mode_of_payment, callback) { if(!frm.doc.company) { - frappe.throw(__("Please select the Company first")); + frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}); } if(!mode_of_payment) { From b61361f9422b3ffdd9834961f3a8b0f1c9226b0b Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 24 Sep 2020 15:08:52 +0530 Subject: [PATCH 24/79] chore: make asset movement transaction date match with purchase date & time (#23424) --- erpnext/assets/doctype/asset/asset.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 13fb2665982..719b8de92be 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext, math, json from frappe import _ from six import string_types -from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day +from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day, get_datetime from frappe.model.document import Document from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset.depreciation \ @@ -154,6 +154,10 @@ class Asset(AccountsController): def make_asset_movement(self): reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice' reference_docname = self.purchase_receipt or self.purchase_invoice + transaction_date = getdate(self.purchase_date) + if reference_docname: + posting_date, posting_time = frappe.db.get_value(reference_doctype, reference_docname, ["posting_date", "posting_time"]) + transaction_date = get_datetime("{} {}".format(posting_date, posting_time)) assets = [{ 'asset': self.name, 'asset_name': self.asset_name, @@ -165,7 +169,7 @@ class Asset(AccountsController): 'assets': assets, 'purpose': 'Receipt', 'company': self.company, - 'transaction_date': getdate(self.purchase_date), + 'transaction_date': transaction_date, 'reference_doctype': reference_doctype, 'reference_name': reference_docname }).insert() From 1caab44c29401f2015ac5ed8cc99d3f541f8a450 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Thu, 24 Sep 2020 15:43:18 +0530 Subject: [PATCH 25/79] fix:dash board for pre-release test (#23426) --- erpnext/hr/doctype/leave_application/leave_application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 0b45a1d8291..cac4f33a237 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -446,7 +446,7 @@ def get_leave_details(employee, date): leave_allocation[d] = { "total_leaves": total_allocated_leaves, - "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken), + "expired_leaves": max(total_allocated_leaves - (remaining_leaves + leaves_taken), 0), "leaves_taken": leaves_taken, "pending_leaves": leaves_pending, "remaining_leaves": remaining_leaves} From 92c1f38c3399a6f6a7de4c21ff3dea7326171737 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 24 Sep 2020 17:08:22 +0530 Subject: [PATCH 26/79] fix: set stock uom in tem while creating material request from stock entry --- erpnext/stock/doctype/stock_entry/stock_entry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 529602f2b9c..e1a7e3bb768 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -139,6 +139,7 @@ frappe.ui.form.on('Stock Entry', { mr_item.item_code = item.item_code; mr_item.item_name = item.item_name; mr_item.uom = item.uom; + mr_item.stock_uom = item.stock_uom; mr_item.conversion_factor = item.conversion_factor; mr_item.item_group = item.item_group; mr_item.description = item.description; From 560f513d6987c9731b38f969ce4d9e9bb76089cf Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 25 Sep 2020 01:12:31 +0530 Subject: [PATCH 27/79] 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 28/79] 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 29/79] 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 30/79] 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 31/79] 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 32/79] 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 33/79] 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 34/79] 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 35/79] 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 36/79] 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 37/79] 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 38/79] 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 b4c2816343296afe4d3af02a4f646517e3f95585 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 3 Oct 2020 17:25:26 +0200 Subject: [PATCH 39/79] fix: check fiscal year --- erpnext/regional/report/datev/datev.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a2d74b889ad..815b5ed4d29 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -16,6 +16,7 @@ from csv import QUOTE_NONNUMERIC import frappe from frappe import _ +from erpnext.accounts.utils import get_fiscal_year import pandas as pd @@ -30,20 +31,33 @@ def execute(filters=None): def validate(filters): """Make sure all mandatory filters and settings are present.""" - if not filters.get('company'): + company = filters.get('company') + if not company: frappe.throw(_('Company is a mandatory filter.')) - if not filters.get('from_date'): + from_date = filters.get('from_date') + if not from_date: frappe.throw(_('From Date is a mandatory filter.')) - if not filters.get('to_date'): + to_date = filters.get('to_date') + if not to_date: frappe.throw(_('To Date is a mandatory filter.')) + validate_fiscal_year(from_date, to_date, company) + try: frappe.get_doc('DATEV Settings', filters.get('company')) except frappe.DoesNotExistError: frappe.throw(_('Please create DATEV Settings for Company {}.').format(filters.get('company'))) + +def validate_fiscal_year(from_date, to_date, company): + from_fiscal_year = get_fiscal_year(date=from_date, company=company) + to_fiscal_year = get_fiscal_year(date=to_date, company=company) + if from_fiscal_year != to_fiscal_year: + frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date)) + + def get_columns(): """Return the list of columns that will be shown in query report.""" columns = [ @@ -231,7 +245,7 @@ def get_datev_csv(data, filters): # L = Tax client number (Mandantennummer) frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "", # M = Start of the fiscal year (Wirtschaftsjahresbeginn) - frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), + frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"), # N = Length of account numbers (Sachkontenlänge) "4", # O = Transaction batch start date (YYYYMMDD) @@ -507,6 +521,9 @@ def download_datev_csv(filters=None): filters = json.loads(filters) validate(filters) + fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=filters.get('company')) + filters['fiscal_year_start'] = fiscal_year[1] + data = get_gl_entries(filters, as_dict=1) frappe.response['result'] = get_datev_csv(data, filters) From e1f1d5a67dacbc01310229a10ecbf2dcb567c2c4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 3 Oct 2020 18:28:43 +0200 Subject: [PATCH 40/79] fix: fiscal year can be shorter than 12 months --- .../doctype/fiscal_year/fiscal_year.json | 431 +++++------------- .../doctype/fiscal_year/fiscal_year.py | 5 + .../doctype/fiscal_year/test_records.json | 7 + 3 files changed, 115 insertions(+), 328 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index 4ca9f6b96fb..882955cd292 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -1,347 +1,122 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:year", - "beta": 0, - "creation": "2013-01-22 16:50:25", - "custom": 0, - "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "autoname": "field:year", + "creation": "2013-01-22 16:50:25", + "description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "year", + "disabled", + "is_short_year", + "year_start_date", + "year_end_date", + "companies", + "auto_created" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "For e.g. 2012, 2012-13", - "fieldname": "year", - "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": "Year Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "year", - "oldfieldtype": "Data", - "permlevel": 0, - "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, - "unique": 0 - }, + "description": "For e.g. 2012, 2012-13", + "fieldname": "year", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Year Name", + "oldfieldname": "year", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "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": "Disabled", - "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": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "year_start_date", - "fieldtype": "Date", - "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": "Year Start Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "year_start_date", - "oldfieldtype": "Date", - "permlevel": 0, - "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, - "unique": 0 - }, + "fieldname": "year_start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Year Start Date", + "no_copy": 1, + "oldfieldname": "year_start_date", + "oldfieldtype": "Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "year_end_date", - "fieldtype": "Date", - "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": "Year End Date", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "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, - "unique": 0 - }, + "fieldname": "year_end_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Year End Date", + "no_copy": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "companies", - "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": "Companies", - "length": 0, - "no_copy": 0, - "options": "Fiscal Year Company", - "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": "companies", + "fieldtype": "Table", + "label": "Companies", + "options": "Fiscal Year Company" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "auto_created", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Auto Created", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "0", + "fieldname": "auto_created", + "fieldtype": "Check", + "hidden": 1, + "label": "Auto Created", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "description": "Less than 12 months.", + "fieldname": "is_short_year", + "fieldtype": "Check", + "label": "Is Short Year", + "set_only_once": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-calendar", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-04-25 14:21:41.273354", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Fiscal Year", - "owner": "Administrator", + ], + "icon": "fa fa-calendar", + "idx": 1, + "modified": "2020-10-03 18:22:04.161315", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Fiscal Year", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Sales User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Purchase User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Accounts User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Stock User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Stock User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Employee", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Employee" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_field": "name", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "show_name_in_global_search": 1, + "sort_field": "name", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index d80bc7fad10..29c96bdae1e 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -36,6 +36,11 @@ class FiscalYear(Document): frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.")) def validate_dates(self): + if self.is_short_year: + # Fiscal Year can be shorter than one year, in some jurisdictions + # under certain circumstances. For example, in the USA and Germany. + return + if getdate(self.year_start_date) > getdate(self.year_end_date): frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"), FiscalYearIncorrectDate) diff --git a/erpnext/accounts/doctype/fiscal_year/test_records.json b/erpnext/accounts/doctype/fiscal_year/test_records.json index d5723ca62ba..47be54f026f 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_records.json +++ b/erpnext/accounts/doctype/fiscal_year/test_records.json @@ -58,5 +58,12 @@ "year": "_Test Fiscal Year 2021", "year_end_date": "2021-12-31", "year_start_date": "2021-01-01" + }, + { + "doctype": "Fiscal Year", + "year": "_Test Short Fiscal Year 2021", + "is_short_year": 1, + "year_end_date": "2021-12-31", + "year_start_date": "2021-04-01" } ] From 8aad02f822c03f1fff5283945b3de01d239bb93d Mon Sep 17 00:00:00 2001 From: Saqib Date: Sun, 4 Oct 2020 18:13:58 +0530 Subject: [PATCH 41/79] 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 42/79] 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 43/79] 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 44/79] 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 45/79] 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 46/79] 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 47/79] 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 48/79] 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 49/79] 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 50/79] 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 51/79] 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 0e6e7e95bae1af3264e1b8bd3e97d7f2859644f4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 8 Oct 2020 15:30:40 +0530 Subject: [PATCH 52/79] fix: Taxable value in GSTR 3B report --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index b02c4bc7333..25c30acba76 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -158,7 +158,7 @@ class GSTR3BReport(Document): self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"]) self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"]) - self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y") + self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas", "Registered Regular"], reverse_charge="Y") self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2) self.set_itc_details(itc_details) @@ -197,7 +197,7 @@ class GSTR3BReport(Document): if d["ty"] == 'ISRC': reverse_charge = "Y" itc_type = 'All Other ITC' - gst_category = ['Unregistered', 'Overseas'] + gst_category = ['Unregistered', 'Overseas', 'Registered Regular'] else: reverse_charge = "N" @@ -253,7 +253,7 @@ class GSTR3BReport(Document): def get_total_taxable_value(self, doctype, reverse_charge): return frappe._dict(frappe.db.sql(""" - select gst_category, sum(net_total) as total + select gst_category, sum(base_net_total) as total from `tab{doctype}` where docstatus = 1 and month(posting_date) = %s and year(posting_date) = %s and reverse_charge = %s From eef5c5dad403563cd100b7a598c67737e1be5436 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 15 Jul 2020 16:37:21 +0530 Subject: [PATCH 53/79] 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 54/79] 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 55/79] 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 56/79] 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 57/79] 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 58/79] 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 a784e13ac30bc424e5114c79f85c62bff77ce4c7 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 1 Oct 2020 16:20:12 +0530 Subject: [PATCH 59/79] fix: Payment Schedule not fetching --- .../accounts/doctype/subscription/subscription.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index bc34816e375..4ec2e65b2a3 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -263,13 +263,14 @@ class Subscription(Document): invoice.set_taxes() # Due date - invoice.append( - 'payment_schedule', - { - 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)), - 'invoice_portion': 100 - } - ) + if self.days_until_due: + invoice.append( + 'payment_schedule', + { + 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)), + 'invoice_portion': 100 + } + ) # Discounts if self.additional_discount_percentage: From dc68b445960020f09c96b71c01a6dadc1b191d84 Mon Sep 17 00:00:00 2001 From: Anupam Date: Mon, 12 Oct 2020 11:53:19 +0530 Subject: [PATCH 60/79] 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 61/79] 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 62/79] 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 63/79] 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 64/79] 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 65/79] 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 66/79] 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") From 007069d4958d715728dcaff390001cc9cd2dc844 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 13 Oct 2020 02:37:55 +0530 Subject: [PATCH 67/79] fix: Convert dates to datetime.date before comparing in Holiday List --- erpnext/hr/doctype/holiday_list/holiday_list.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index 8c7b6f723f9..bfd882e532a 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -1,3 +1,4 @@ + # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt @@ -31,7 +32,7 @@ class HolidayList(Document): def validate_days(self): - if self.from_date > self.to_date: + if getdate(self.from_date) > getdate(self.to_date): throw(_("To Date cannot be before From Date")) for day in self.get("holidays"): From 39988c9319f191ed028213696577c14d93c06383 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 13 Oct 2020 12:36:48 +0530 Subject: [PATCH 68/79] fix: subscription test case (#23617) --- erpnext/accounts/doctype/subscription/test_subscription.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index e38de252699..e11c0c39701 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -210,6 +210,7 @@ class TestSubscription(unittest.TestCase): subscription.customer = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) subscription.start = '2018-01-01' + subscription.days_until_due = 1 subscription.insert() subscription.process() # generate first invoice From 8f60d25064cc1f3f333e0efc3f71211448d100de Mon Sep 17 00:00:00 2001 From: aakvatech <35020381+aakvatech@users.noreply.github.com> Date: Tue, 13 Oct 2020 10:26:33 +0300 Subject: [PATCH 69/79] =?UTF-8?q?feat:=20Add=20company=20and=20correct=20f?= =?UTF-8?q?ilter=20in=20bank=20statement=20reconciliation=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … report filters If you have multi company scenario and many bank accounts that are no longer active, then it becomes difficult in bank statement reconciliation report to filter the account to reconcile and it also shows disabled accounts so futher confusion is created. https://github.com/frappe/erpnext/issues/23613 --- .../bank_reconciliation_statement.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js index 57fe4b05be4..8f028496cd5 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js @@ -3,6 +3,14 @@ frappe.query_reports["Bank Reconciliation Statement"] = { "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "reqd": 1, + "default": frappe.defaults.get_user_default("Company") + }, { "fieldname":"account", "label": __("Bank Account"), @@ -12,11 +20,14 @@ frappe.query_reports["Bank Reconciliation Statement"] = { locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "", "reqd": 1, "get_query": function() { + var company = frappe.query_report.get_filter_value('company') return { "query": "erpnext.controllers.queries.get_account_list", "filters": [ ['Account', 'account_type', 'in', 'Bank, Cash'], ['Account', 'is_group', '=', 0], + ['Account', 'disabled', '=', 0], + ['Account', 'company', '=', company], ] } } @@ -34,4 +45,4 @@ frappe.query_reports["Bank Reconciliation Statement"] = { "fieldtype": "Check" }, ] -} \ No newline at end of file +} From e2b7ac2b0e6feea737d3afff613914bc31fae75c Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 13 Oct 2020 11:39:18 +0200 Subject: [PATCH 70/79] fix: make account number length configurable (#23496) * fix: make account number length configurable * Update datev.py Co-authored-by: Nabin Hait --- .../doctype/datev_settings/datev_settings.json | 10 +++++++++- erpnext/regional/report/datev/datev.py | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json index caed7367dc1..d319324a68e 100644 --- a/erpnext/regional/doctype/datev_settings/datev_settings.json +++ b/erpnext/regional/doctype/datev_settings/datev_settings.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "client", + "account_number_length", "column_break_2", "client_number", "section_break_4", @@ -57,9 +58,16 @@ { "fieldname": "column_break_6", "fieldtype": "Column Break" + }, + { + "default": "4", + "fieldname": "account_number_length", + "fieldtype": "Int", + "label": "Account Number Length", + "reqd": 1 } ], - "modified": "2020-04-15 12:59:57.786506", + "modified": "2020-10-03 16:52:35.903867", "modified_by": "Administrator", "module": "Regional", "name": "DATEV Settings", diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 815b5ed4d29..9f2e86bab36 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -247,7 +247,7 @@ def get_datev_csv(data, filters): # M = Start of the fiscal year (Wirtschaftsjahresbeginn) frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"), # N = Length of account numbers (Sachkontenlänge) - "4", + str(filters.get('account_number_length', 4)), # O = Transaction batch start date (YYYYMMDD) frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"), # P = Transaction batch end date (YYYYMMDD) @@ -521,6 +521,9 @@ def download_datev_csv(filters=None): filters = json.loads(filters) validate(filters) + + filters['account_number_length'] = frappe.get_value('DATEV Settings', filters.get('company'), 'account_number_length') + fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=filters.get('company')) filters['fiscal_year_start'] = fiscal_year[1] From 4def9ed1c646b8ba9ddb20cbf528adb6016d8400 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 13 Oct 2020 14:34:12 +0530 Subject: [PATCH 71/79] fix: incorrect supplied qty error --- .../purchase_order/test_purchase_order.py | 53 +++++++++++++++++++ .../purchase_receipt/test_purchase_receipt.py | 4 +- .../stock/doctype/stock_entry/stock_entry.py | 5 +- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 8427e66a868..f568a182ed7 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -780,6 +780,59 @@ class TestPurchaseOrder(unittest.TestCase): update_backflush_based_on("BOM") + def test_supplied_qty_against_subcontracted_po(self): + item_code = "_Test Subcontracted FG Item 5" + make_item('Sub Contracted Raw Material 4', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1 + }) + + make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"]) + + update_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 250 + po = create_purchase_order(item_code=item_code, qty=order_qty, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True) + + # Add same subcontracted items multiple times + po.append("items", { + "item_code": item_code, + "qty": order_qty, + "schedule_date": add_days(nowdate(), 1), + "warehouse": "_Test Warehouse - _TC" + }) + + po.set_missing_values() + po.submit() + + # Material receipt entry for the raw materials which will be send to supplier + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100) + + rm_items = [ + { + "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item", + "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name + }, + { + "item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item", + "qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name + }, + ] + + # Raw Materials transfer entry from stores to supplier's warehouse + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.submit() + + po_doc = frappe.get_doc("Purchase Order", po.name) + for row in po_doc.supplied_items: + # Valid that whether transferred quantity is matching with supplied qty or not in the purchase order + self.assertEqual(row.supplied_qty, 250.0) + + update_backflush_based_on("BOM") + def test_advance_payment_entry_unlink_against_purchase_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry frappe.db.set_value("Accounts Settings", "Accounts Settings", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 7c244ea5023..d0208d01eda 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -613,9 +613,9 @@ class TestPurchaseReceipt(unittest.TestCase): rm_items = [ {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", - "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, + "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name}, {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item", - "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"} + "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name} ] rm_item_string = json.dumps(rm_items) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 961f5f45325..6abdba5ca89 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1255,8 +1255,9 @@ class StockEntry(StockController): FROM `tabStock Entry Detail` sed, `tabStock Entry` se WHERE - (pos.name = sed.po_detail OR sed.subcontracted_item = pos.main_item_code) - AND sed.docstatus = 1 AND se.name = sed.parent and se.purchase_order = %(po)s + pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code + AND pos.parent = se.purchase_order AND sed.docstatus = 1 + AND se.name = sed.parent and se.purchase_order = %(po)s ), 0) WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order}) From 3f5c4932b4ea0509ee32e23024767cbf16dc92b4 Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 14 Oct 2020 15:37:09 +0530 Subject: [PATCH 72/79] fix: added filter show in website for filtering product --- erpnext/portal/product_configurator/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 336b9319c1d..404460412e8 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -12,13 +12,15 @@ def get_field_filter_data(): for f in fields: doctype = f.get_link_doctype() - # apply enable/disable filter + # apply enable/disable/show_in_website filter meta = frappe.get_meta(doctype) filters = {} if meta.has_field('enabled'): filters['enabled'] = 1 if meta.has_field('disabled'): filters['disabled'] = 0 + if meta.has_field('show_in_website'): + filters['show_in_website'] = 1 values = [d.name for d in frappe.get_all(doctype, filters)] filter_data.append([f, values]) From 940c5f2d4b96472a92341007dee76185a52b636e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 14:14:27 +0530 Subject: [PATCH 73/79] fix: serial no field is blank in stock reconciliation --- .../doctype/stock_reconciliation/stock_reconciliation.js | 4 ++++ .../doctype/stock_reconciliation/stock_reconciliation.py | 2 ++ .../doctype/stock_reconciliation/test_stock_reconciliation.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 0475ea7a2ec..bf8a59b2722 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -109,6 +109,10 @@ frappe.ui.form.on("Stock Reconciliation", { frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos); + + if (frm.doc.purpose == "Stock Reconciliation") { + frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos); + } } }); } diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 003403a2f8a..f502dbad8bb 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -68,6 +68,8 @@ class StockReconciliation(StockController): if item_dict.get("serial_nos"): item.current_serial_no = item_dict.get("serial_nos") + if self.purpose == "Stock Reconciliation": + item.serial_no = item.current_serial_no item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 8b073ec5ab4..27908016407 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -131,7 +131,7 @@ class TestStockReconciliation(unittest.TestCase): to_delete_records.append(sr.name) sr = create_stock_reconciliation(item_code=serial_item_code, - warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos)) + warehouse = serial_warehouse, qty=5, rate=300) # print(sr.name) serial_nos1 = get_serial_nos(sr.items[0].serial_no) From 7a079d73dc2f4a73f8793a859331c8953a46650b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 21:54:53 +0530 Subject: [PATCH 74/79] fix: remove repetative code --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 08553675f4c..8538b00fd6e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -208,7 +208,6 @@ class PurchaseReceipt(BuyingController): 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") From a2ab2272404e12661fcb7a768ceca3a7fddfa237 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 19:00:02 +0530 Subject: [PATCH 75/79] fix: manually set serial nos override with current available serial nos --- .../doctype/stock_reconciliation/stock_reconciliation.js | 7 ++++++- .../doctype/stock_reconciliation/stock_reconciliation.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index bf8a59b2722..0b6610cf85b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -110,7 +110,7 @@ frappe.ui.form.on("Stock Reconciliation", { frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty); frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos); - if (frm.doc.purpose == "Stock Reconciliation") { + if (frm.doc.purpose == "Stock Reconciliation" && !d.serial_no) { frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos); } } @@ -187,6 +187,11 @@ frappe.ui.form.on("Stock Reconciliation Item", { frappe.model.set_value(cdt, cdn, "batch_no", ""); } + if (child.serial_no) { + frappe.model.set_value(cdt, cdn, "serial_no", ""); + frappe.model.set_value(cdt, cdn, "current_serial_no", ""); + } + frm.events.set_valuation_rate_and_qty(frm, cdt, cdn); }, diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index f502dbad8bb..2b4780437f4 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -68,7 +68,7 @@ class StockReconciliation(StockController): if item_dict.get("serial_nos"): item.current_serial_no = item_dict.get("serial_nos") - if self.purpose == "Stock Reconciliation": + if self.purpose == "Stock Reconciliation" and not item.serial_no: item.serial_no = item.current_serial_no item.current_qty = item_dict.get("qty") From 3df4f226d5ae27c3e50ac90a56342c63c3f1ce78 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Oct 2020 15:47:21 +0530 Subject: [PATCH 76/79] fix: overproduction, not allowed to transfer extra materials --- .../stock/doctype/stock_entry/stock_entry.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 274ab1d166b..b61b8e37197 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -844,6 +844,8 @@ class StockEntry(StockController): frappe.throw(_("Posting date and posting time is mandatory")) self.set_work_order_details() + self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings", + "backflush_raw_materials_based_on") if self.bom_no: @@ -857,14 +859,14 @@ class StockEntry(StockController): item["to_warehouse"] = self.pro_doc.wip_warehouse self.add_to_stock_entry_detail(item_dict) - elif (self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") - and not self.pro_doc.skip_transfer and frappe.db.get_single_value("Manufacturing Settings", - "backflush_raw_materials_based_on")== "Material Transferred for Manufacture"): + elif (self.work_order and (self.purpose == "Manufacture" + or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer + and self.flags.backflush_based_on == "Material Transferred for Manufacture"): self.get_transfered_raw_materials() - elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \ - frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "BOM" and \ - frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1: + elif (self.work_order and (self.purpose == "Manufacture" or + self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM" + and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1): self.get_unconsumed_raw_materials() else: if not self.fg_completed_qty: @@ -1108,7 +1110,6 @@ class StockEntry(StockController): for d in backflushed_materials.get(item.item_code): if d.get(item.warehouse): if (qty > req_qty): - qty = req_qty qty-= d.get(item.warehouse) if qty > 0: @@ -1133,11 +1134,22 @@ class StockEntry(StockController): """ item_dict = self.get_pro_order_required_items() max_qty = flt(self.pro_doc.qty) + + allow_overproduction = False + overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings", + "overproduction_percentage_for_work_order")) + + to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty) + transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100) + + if transfer_limit_qty >= to_transfer_qty: + allow_overproduction = True + for item, item_details in iteritems(item_dict): pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty) desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty - if desire_to_transfer <= pending_to_issue: + if desire_to_transfer <= pending_to_issue or allow_overproduction: item_dict[item]["qty"] = desire_to_transfer elif pending_to_issue > 0: item_dict[item]["qty"] = pending_to_issue From c1ffe0990b471aff03664d8d2a9c6d0e83a2fe35 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 22 Oct 2020 17:04:38 +0530 Subject: [PATCH 77/79] Update items add taxes v12 pre release (#23706) * fix: Add Taxes if missing via Update Items * chore: PO Test for adding tax row via Update Items * chore: SO Test for adding tax row via Update Items --- .../purchase_order/test_purchase_order.py | 50 ++++++++-- erpnext/controllers/accounts_controller.py | 27 ++++++ .../doctype/sales_order/test_sales_order.py | 91 ++++++++++++++++++- erpnext/stock/get_item_details.py | 5 + 4 files changed, 166 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index f568a182ed7..cb745d6dba8 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -203,9 +203,39 @@ class TestPurchaseOrder(unittest.TestCase): frappe.set_user("Administrator") def test_update_child_with_tax_template(self): + """ + Test Action: Create a PO with one item having its tax account head already in the PO. + Add the same item + new item with tax template via Update Items. + Expected result: First Item's tax row is updated. New tax row is added for second Item. + """ + if not frappe.db.exists("Item", "Test Item with Tax"): + make_item("Test Item with Tax", { + 'is_stock_item': 1, + }) + + if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): + frappe.get_doc({ + 'doctype': 'Item Tax Template', + 'title': 'Test Update Items Template', + 'company': '_Test Company', + 'taxes': [ + { + 'tax_type': "_Test Account Service Tax - _TC", + 'tax_rate': 10, + } + ] + }).insert() + + new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") + + new_item_with_tax.append("taxes", { + "item_tax_template": "Test Update Items Template", + "valid_from": nowdate() + }) + new_item_with_tax.save() + tax_template = "_Test Account Excise Duty @ 10" item = "_Test Item Home Desktop 100" - if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): item_doc = frappe.get_doc("Item", item) item_doc.append("taxes", { @@ -237,17 +267,25 @@ class TestPurchaseOrder(unittest.TestCase): items = json.dumps([ {'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name}, - {'item_code' : item, 'rate' : 100, 'qty' : 1} # added item + {'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO + {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO ]) update_child_qty_rate('Purchase Order', items, po.name) po.reload() - self.assertEqual(po.taxes[0].tax_amount, 60) - self.assertEqual(po.taxes[0].total, 660) + self.assertEqual(po.taxes[0].tax_amount, 70) + self.assertEqual(po.taxes[0].total, 770) + self.assertEqual(po.taxes[1].account_head, "_Test Account Service Tax - _TC") + self.assertEqual(po.taxes[1].tax_amount, 70) + self.assertEqual(po.taxes[1].total, 840) + # teardown frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL - where parent = %(item)s and item_tax_template = %(tax)s""", - {"item": item, "tax": tax_template}) + where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) + po.cancel() + po.delete() + new_item_with_tax.delete() + frappe.get_doc("Item Tax Template", "Test Update Items Template").delete() def test_update_qty(self): po = create_purchase_order() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 12b8f697cde..31045a97671 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1139,6 +1139,31 @@ def set_child_tax_template_and_map(item, child_item, parent_doc): if child_item.get("item_tax_template"): child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True) +def add_taxes_from_tax_template(child_item, parent_doc): + add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template") + + if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template: + tax_map = json.loads(child_item.get("item_tax_rate")) + for tax_type in tax_map: + tax_rate = flt(tax_map[tax_type]) + taxes = parent_doc.get('taxes') or [] + # add new row for tax head only if missing + found = any(tax.account_head == tax_type for tax in taxes) + if not found: + tax_row = parent_doc.append("taxes", {}) + tax_row.update({ + "description" : str(tax_type).split(' - ')[0], + "charge_type" : "On Net Total", + "account_head" : tax_type, + "rate" : tax_rate + }) + if parent_doc.doctype == "Purchase Order": + tax_row.update({ + "category" : "Total", + "add_deduct_tax" : "Add" + }) + tax_row.db_insert() + def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Sales Order Item child item containing the default values @@ -1153,6 +1178,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.uom = item.stock_uom set_child_tax_template_and_map(item, child_item, p_doc) + add_taxes_from_tax_template(child_item, p_doc) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") @@ -1176,6 +1202,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation set_child_tax_template_and_map(item, child_item, p_doc) + add_taxes_from_tax_template(child_item, p_doc) return child_item def validate_and_delete_children(parent, data): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 55458a51e98..fcde0d502e9 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -402,7 +402,7 @@ 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 @@ -492,6 +492,95 @@ class TestSalesOrder(unittest.TestCase): so.reload() self.assertEqual(so.packed_items[0].qty, 4) + def test_update_child_with_tax_template(self): + """ + Test Action: Create a SO with one item having its tax account head already in the SO. + Add the same item + new item with tax template via Update Items. + Expected result: First Item's tax row is updated. New tax row is added for second Item. + """ + if not frappe.db.exists("Item", "Test Item with Tax"): + make_item("Test Item with Tax", { + 'is_stock_item': 1, + }) + + if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): + frappe.get_doc({ + 'doctype': 'Item Tax Template', + 'title': 'Test Update Items Template', + 'company': '_Test Company', + 'taxes': [ + { + 'tax_type': "_Test Account Service Tax - _TC", + 'tax_rate': 10, + } + ] + }).insert() + + new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") + + new_item_with_tax.append("taxes", { + "item_tax_template": "Test Update Items Template", + "valid_from": nowdate() + }) + new_item_with_tax.save() + + tax_template = "_Test Account Excise Duty @ 10" + item = "_Test Item Home Desktop 100" + if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item_doc = frappe.get_doc("Item", item) + item_doc.append("taxes", { + "item_tax_template": tax_template, + "valid_from": nowdate() + }) + item_doc.save() + else: + # update valid from + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}) + + so = make_sales_order(item_code=item, qty=1, do_not_save=1) + + so.append("taxes", { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 10 + }) + so.insert() + so.submit() + + self.assertEqual(so.taxes[0].tax_amount, 10) + self.assertEqual(so.taxes[0].total, 110) + + old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse") + frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC") + + items = json.dumps([ + {'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name}, + {'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO + {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO + ]) + update_child_qty_rate('Sales Order', items, so.name) + + so.reload() + self.assertEqual(so.taxes[0].tax_amount, 40) + self.assertEqual(so.taxes[0].total, 440) + self.assertEqual(so.taxes[1].account_head, "_Test Account Service Tax - _TC") + self.assertEqual(so.taxes[1].tax_amount, 40) + self.assertEqual(so.taxes[1].total, 480) + + # teardown + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) + so.cancel() + so.delete() + new_item_with_tax.delete() + frappe.get_doc("Item Tax Template", "Test Update Items Template").delete() + frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value) + def test_warehouse_user(self): frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com") diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 50c8c4e53f4..b1e38b340c5 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -385,6 +385,11 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults={}): else: warehouse = args.get('warehouse') + if not warehouse: + default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse") + if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company: + return default_warehouse + return warehouse def update_barcode_value(out): From f74aa28752d09745bcb83a01ef866df77e57e41c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 22 Oct 2020 17:52:16 +0530 Subject: [PATCH 78/79] chore: Added change log (#23707) --- erpnext/change_log/v12/v12_13_0.md | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 erpnext/change_log/v12/v12_13_0.md diff --git a/erpnext/change_log/v12/v12_13_0.md b/erpnext/change_log/v12/v12_13_0.md new file mode 100644 index 00000000000..73054e90cc8 --- /dev/null +++ b/erpnext/change_log/v12/v12_13_0.md @@ -0,0 +1,51 @@ +## ERPNext v12.13.0 Release Note + +### Fixes and Enhancements + +- Warehouse address filtered based on warehouse ([#23381](https://github.com/frappe/erpnext/pull/23381)) +- COGS validation in the purchase receipt ([#23536](https://github.com/frappe/erpnext/pull/23536)) +- Item Tax Updating via `Update Items` in SO/PO ([#23338](https://github.com/frappe/erpnext/pull/23338)) +- Pricing rule selector is wrong ([#22328](https://github.com/frappe/erpnext/pull/22328)) +- Adding filters validation Batch-Wise Balance History ([#23396](https://github.com/frappe/erpnext/pull/23396)) +- Add company and correct filter in bank statement reconciliation report filters ([#23618](https://github.com/frappe/erpnext/pull/23618)) +- Incorrect consumed qty if raw material with batch ([#23389](https://github.com/frappe/erpnext/pull/23389)) +- Balance serial nos in stock ledger report ([#23520](https://github.com/frappe/erpnext/pull/23520)) +- Use Plaid's new API (v12) ([#23317](https://github.com/frappe/erpnext/pull/23317)) +- Cost Center filter in accounts receivable and payable report ([#23356](https://github.com/frappe/erpnext/pull/23356)) +- Incorrect operation time calculation for batch size ([#23480](https://github.com/frappe/erpnext/pull/23480)) +- Serial no field is blank in stock reconciliation ([#23648](https://github.com/frappe/erpnext/pull/23648)) +- Manually set serial nos override with current available serial nos ([#23657](https://github.com/frappe/erpnext/pull/23657)) +- Can't save item price after adding child table ([#23594](https://github.com/frappe/erpnext/pull/23594)) +- Naming series - cannot reset current value to zero ([#23505](https://github.com/frappe/erpnext/pull/23505)) +- TDS calculation, skip invoices with "Apply Tax Withholding Amount" has disabled ([#23463](https://github.com/frappe/erpnext/pull/23463)) +- Taxes not getting fetched from Opportunity to Quotation ([#23354](https://github.com/frappe/erpnext/pull/23354)) +- Stock reconciliation, incorrect serial nos fetched in the current serial no field ([#23366](https://github.com/frappe/erpnext/pull/23366)) +- Cannot create asset if cwip disabled and account not set ([#23584](https://github.com/frappe/erpnext/pull/23584)) +- Leave application status fix ([#23411](https://github.com/frappe/erpnext/pull/23411)) +- Not able to do overproduction ([#23600](https://github.com/frappe/erpnext/pull/23600)) +- Incorrect supplied qty error ([#23619](https://github.com/frappe/erpnext/pull/23619)) +- Download Required Materials not working for production plan ([#23403](https://github.com/frappe/erpnext/pull/23403)) +- Make account number length configurable ([#23496](https://github.com/frappe/erpnext/pull/23496)) +- Include item_code in items result to allow adding product info in custom templates ([#23440](https://github.com/frappe/erpnext/pull/23440)) +- Mode of payment getting overwritten by default mode of payment for returns ([#23599](https://github.com/frappe/erpnext/pull/23599)) +- Show total row in print format of financial statement ([#23565](https://github.com/frappe/erpnext/pull/23565)) +- Performance issue while adding template item in the cart ([#23508](https://github.com/frappe/erpnext/pull/23508)) +- Display item name instead of item code in offline POS ([#23451](https://github.com/frappe/erpnext/pull/23451)) +- Set stock UOM in item while creating material request from stock entry ([#23430](https://github.com/frappe/erpnext/pull/23430)) +- Book loss amount in the COGS instead of stock received but not billed ([#23412](https://github.com/frappe/erpnext/pull/23412)) +- Depreciation start date ux fixes ([#23340](https://github.com/frappe/erpnext/pull/23340)) +- Payment Schedule not fetching ([#23477](https://github.com/frappe/erpnext/pull/23477)) +- Taxable value in GSTR 3B report ([#23552](https://github.com/frappe/erpnext/pull/23552)) +- Negative stock error while submitting stock reco for batch item ([#23564](https://github.com/frappe/erpnext/pull/23564)) +- Check only Read and Write Permission in Update Items ([#23458](https://github.com/frappe/erpnext/pull/23458)) +- Handle Blank from/to range in Numeric Item Attribute ([#23484](https://github.com/frappe/erpnext/pull/23484)) +- Added new filters in the Batch-wise balance history report ([#23522](https://github.com/frappe/erpnext/pull/23522)) +- Added filter show in website for filtering product ([#23638](https://github.com/frappe/erpnext/pull/23638)) +- Enabled no copy property for Supplier Invoice Date to avoid due date validation ([#23367](https://github.com/frappe/erpnext/pull/23367)) +- Showing a negative balance in expired leaves ([#23426](https://github.com/frappe/erpnext/pull/23426)) +- Validate if removed item attributes exist in variants ([#23591](https://github.com/frappe/erpnext/pull/23591)) +- Longer timeout for company replace abbreviation ([#23442](https://github.com/frappe/erpnext/pull/23442)) +- Online pos print not working ([#23377](https://github.com/frappe/erpnext/pull/23377)) +- Last purchase rate in item prices report ([#23507](https://github.com/frappe/erpnext/pull/23507)) +- Do not consider opening entries for TDS calculation ([#23598](https://github.com/frappe/erpnext/pull/23598)) +- Project value is missing from procurement-tracker ([#23551](https://github.com/frappe/erpnext/pull/23551)) From acf22c475ea9f843335c080e82f53f5108557ac6 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 22 Oct 2020 18:48:33 +0550 Subject: [PATCH 79/79] bumped to version 12.13.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 69c3121653f..85293c15899 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '12.12.1' +__version__ = '12.13.0' def get_default_company(user=None): '''Get default company for user'''