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''' 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", 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" } ] diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 09feb7d97bc..27541670a89 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -31,13 +31,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_name = [] + pricing_rules = [] + for p in pricing_rules_all: + if p['name'] not in pricing_rules_name: + pricing_rules_name.append(p['name']) + pricing_rules.append(p) rules = [] @@ -323,9 +329,10 @@ def apply_internal_priority(pricing_rules, field_set, args): filtered_rules = [] for field in field_set: if args.get(field): - # filter function always returns a filter object even if empty - # list conversion is necessary to check for an empty result - filtered_rules = list(filter(lambda x: x.get(field)==args.get(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 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/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': [ { 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: 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 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) 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..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] @@ -139,9 +140,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) @@ -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): 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": "", 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/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, 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" 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 +} 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=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 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)) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ca62ec78cce..31045a97671 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,43 @@ 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 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 @@ -1140,6 +1177,8 @@ 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) + 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.") @@ -1162,6 +1201,8 @@ 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) + add_taxes_from_tax_template(child_item, p_doc) return child_item def validate_and_delete_children(parent, data): @@ -1270,19 +1311,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/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) 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/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 532e19cffd9..a033a2a722d 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -2,81 +2,90 @@ # 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: + 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() + 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: - 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(**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)) - 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/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"): diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json index 9271943a348..ee85a8437ee 100644 --- a/erpnext/hr/doctype/leave_application/leave_application.json +++ b/erpnext/hr/doctype/leave_application/leave_application.json @@ -244,9 +244,10 @@ ], "icon": "fa fa-calendar", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, "max_attachments": 3, - "modified": "2020-09-23 18:53:11.608446", + "modified": "2020-09-23 19:11:58.806837", "modified_by": "Administrator", "module": "HR", "name": "Leave Application", 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..d82a4dd9fe8 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_operation_time_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.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])); 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() 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/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index c8d3203cb70..c3fa6d797cb 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -14,13 +14,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]) @@ -378,7 +380,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` 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) { 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/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..32a1ebcc0ca 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json @@ -21,8 +21,7 @@ "fieldname": "parent_quality_procedure", "fieldtype": "Link", "label": "Parent Procedure", - "options": "Quality Procedure", - "read_only": 1 + "options": "Quality Procedure" }, { "default": "0", @@ -73,7 +72,7 @@ ], "is_tree": 1, "links": [], - "modified": "2020-06-17 17:25:03.434953", + "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") 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/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 diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a2d74b889ad..9f2e86bab36 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,9 +245,9 @@ 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", + 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) @@ -507,6 +521,12 @@ 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] + data = get_gl_entries(filters, as_dict=1) frappe.response['result'] = get_datev_csv(data, filters) 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(" ") 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) 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')); } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ae0faf2128b..fcde0d502e9 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) @@ -401,6 +403,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) @@ -425,9 +443,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") @@ -474,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") @@ -1010,7 +1117,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, 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) 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")) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index deace33f343..e6634d29fe1 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) @@ -1001,8 +1072,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 +1081,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 +1101,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 +1140,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"): 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: 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) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 42cc473b18d..8538b00fd6e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -207,7 +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") + stock_rbnb_currency = get_account_currency(stock_rbnb) landed_cost_entries = get_item_account_wise_additional_cost(self.name) expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") @@ -243,7 +243,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"], @@ -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({ diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c9cda37c40b..d0208d01eda 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() @@ -614,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.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 2d32999c5c5..ef1cbda3fe5 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 = [ @@ -139,6 +157,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; diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 86e4b3f8b3d..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 @@ -1255,8 +1267,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}) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 0475ea7a2ec..0b6610cf85b 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" && !d.serial_no) { + frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos); + } } }); } @@ -183,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 ca59e67a676..2b4780437f4 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" and not item.serial_no: + item.serial_no = item.current_serial_no item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") @@ -172,8 +174,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 +194,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 +221,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 +253,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..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) @@ -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/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): 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..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,19 +3,70 @@ 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"), "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 + }, + { + "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 7f7835f74ee..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 @@ -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) @@ -50,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 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 diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 99d816c4a24..953939bccb8 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 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} ] 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: 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