diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index 3c679fa215a..20ce7ca9a4c 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -21,11 +21,39 @@ class BankAccount(Document): def validate(self): self.validate_company() + self.validate_iban() def validate_company(self): if self.is_company_account and not self.company: frappe.throw(_("Company is manadatory for company account")) + def validate_iban(self): + ''' + Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN + ''' + # IBAN field is optional + if not self.iban: + return + + def encode_char(c): + # Position in the alphabet (A=1, B=2, ...) plus nine + return str(9 + ord(c) - 64) + + # remove whitespaces, upper case to get the right number from ord() + iban = ''.join(self.iban.split(' ')).upper() + + # Move country code and checksum from the start to the end + flipped = iban[4:] + iban[:4] + + # Encode characters as numbers + encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped] + + to_check = int(''.join(encoded)) + + if to_check % 97 != 1: + frappe.throw(_('IBAN is not valid')) + + @frappe.whitelist() def make_bank_account(doctype, docname): doc = frappe.new_doc("Bank Account") diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.py b/erpnext/accounts/doctype/bank_account/test_bank_account.py index 43a3298ec4a..f3bb086fa96 100644 --- a/erpnext/accounts/doctype/bank_account/test_bank_account.py +++ b/erpnext/accounts/doctype/bank_account/test_bank_account.py @@ -4,9 +4,46 @@ from __future__ import unicode_literals import frappe +from frappe import _ +from frappe import ValidationError import unittest # test_records = frappe.get_test_records('Bank Account') class TestBankAccount(unittest.TestCase): - pass + + def test_validate_iban(self): + valid_ibans = [ + 'GB82 WEST 1234 5698 7654 32', + 'DE91 1000 0000 0123 4567 89', + 'FR76 3000 6000 0112 3456 7890 189' + ] + + invalid_ibans = [ + # wrong checksum (3rd place) + 'GB72 WEST 1234 5698 7654 32', + 'DE81 1000 0000 0123 4567 89', + 'FR66 3000 6000 0112 3456 7890 189' + ] + + bank_account = frappe.get_doc({'doctype':'Bank Account'}) + + try: + bank_account.validate_iban() + except AttributeError: + msg = _('BankAccount.validate_iban() failed for empty IBAN') + self.fail(msg=msg) + + for iban in valid_ibans: + bank_account.iban = iban + try: + bank_account.validate_iban() + except ValidationError: + msg = _('BankAccount.validate_iban() failed for valid IBAN {}'.format(iban)) + self.fail(msg=msg) + + for not_iban in invalid_ibans: + bank_account.iban = not_iban + msg = _('BankAccount.validate_iban() accepted invalid IBAN {}'.format(not_iban)) + with self.assertRaises(ValidationError, msg=msg): + bank_account.validate_iban() diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 18ba8f9b5cb..3b41006f2b3 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -296,6 +296,12 @@ frappe.ui.form.on('Asset', { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); }, + gross_purchase_amount: function(frm) { + frm.doc.finance_books.forEach(d => { + frm.events.set_depreciation_rate(frm, d); + }) + }, + set_depreciation_rate: function(frm, row) { if (row.total_number_of_depreciations && row.frequency_of_depreciation) { frappe.call({ diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 8011038b1b1..72f5c627a71 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -101,7 +101,7 @@ class Asset(AccountsController): def set_depreciation_rate(self): for d in self.get("finance_books"): - d.rate_of_depreciation = self.get_depreciation_rate(d) + d.rate_of_depreciation = self.get_depreciation_rate(d, on_validate=True) def make_depreciation_schedule(self): depreciation_method = [d.depreciation_method for d in self.finance_books] @@ -125,7 +125,7 @@ class Asset(AccountsController): no_of_depreciations * cint(d.frequency_of_depreciation)) total_days = date_diff(end_date, self.available_for_use_date) - rate_per_day = value_after_depreciation / total_days + rate_per_day = (value_after_depreciation - d.get("expected_value_after_useful_life")) / total_days number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \ cint(self.number_of_depreciations_booked) @@ -291,8 +291,8 @@ class Asset(AccountsController): def validate_expected_value_after_useful_life(self): for row in self.get('finance_books'): - accumulated_depreciation_after_full_schedule = \ - max([d.accumulated_depreciation_amount for d in self.get("schedules") if d.finance_book_id == row.idx]) + accumulated_depreciation_after_full_schedule = max([d.accumulated_depreciation_amount + for d in self.get("schedules") if cint(d.finance_book_id) == row.idx]) asset_value_after_full_schedule = flt(flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule), @@ -403,7 +403,7 @@ class Asset(AccountsController): make_gl_entries(gl_entries) self.db_set('booked_fixed_asset', 1) - def get_depreciation_rate(self, args): + def get_depreciation_rate(self, args, on_validate=False): if isinstance(args, string_types): args = json.loads(args) @@ -420,7 +420,10 @@ class Asset(AccountsController): if args.get("depreciation_method") == 'Double Declining Balance': return 200.0 / args.get("total_number_of_depreciations") - if args.get("depreciation_method") == "Written Down Value" and not args.get("rate_of_depreciation"): + if args.get("depreciation_method") == "Written Down Value": + if args.get("rate_of_depreciation") and on_validate: + return args.get("rate_of_depreciation") + no_of_years = flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation"))) / 12 value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 985097b447d..ef85ffa1cb8 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -102,9 +102,9 @@ class TestAsset(unittest.TestCase): asset.save() self.assertEqual(asset.status, "Draft") expected_schedules = [ - ["2020-06-06", 163.93, 163.93], - ["2021-04-06", 49836.07, 50000.0], - ["2022-02-06", 40000.0, 90000.00] + ["2020-06-06", 147.54, 147.54], + ["2021-04-06", 44852.46, 45000.0], + ["2022-02-06", 45000.0, 90000.00] ] schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] @@ -130,8 +130,8 @@ class TestAsset(unittest.TestCase): self.assertEqual(asset.status, "Draft") asset.save() expected_schedules = [ - ["2020-06-06", 197.37, 40197.37], - ["2021-04-06", 49802.63, 90000.00] + ["2020-06-06", 164.47, 40164.47], + ["2021-04-06", 49835.53, 90000.00] ] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] for d in asset.get("schedules")] @@ -266,8 +266,8 @@ class TestAsset(unittest.TestCase): self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 0.0, 35699.15), - ("_Test Depreciations - _TC", 35699.15, 0.0) + ("_Test Accumulated Depreciations - _TC", 0.0, 32129.24), + ("_Test Depreciations - _TC", 32129.24, 0.0) ) gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 76205e6c823..ace4fbf9e30 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -22,6 +22,9 @@ erpnext.integrations.plaidLink = class plaidLink { frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration') .then(result => { if (result !== "disabled") { + if (result.plaid_env == undefined || result.plaid_public_key == undefined) { + frappe.throw(__("Please add valid Plaid api keys in site_config.json first")); + } me.plaid_env = result.plaid_env; me.plaid_public_key = result.plaid_public_key; me.client_name = result.client_name; diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 94134f1df23..d09f24a965f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -31,7 +31,7 @@ class PurchaseReceipt(BuyingController): 'target_parent_dt': 'Purchase Order', 'target_parent_field': 'per_received', 'target_ref_field': 'qty', - 'source_field': 'qty', + 'source_field': 'received_qty', 'percent_join_field': 'purchase_order', 'overflow_type': 'receipt' }, diff --git a/erpnext/stock/report/inactive_items/__init__.py b/erpnext/stock/report/inactive_items/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/inactive_items/inactive_items.js b/erpnext/stock/report/inactive_items/inactive_items.js new file mode 100644 index 00000000000..39dfd5c8c36 --- /dev/null +++ b/erpnext/stock/report/inactive_items/inactive_items.js @@ -0,0 +1,34 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Inactive Items"] = { + "filters": [ + { + fieldname: "item", + label: __("Item"), + fieldtype: "Link", + options: "Item" + }, + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group" + }, + { + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: "Sales Order\nSales Invoice", + default: "Sales Order" + }, + { + fieldname: "days", + label: __("Days Since Last order"), + fieldtype: "Select", + options: [30, 60, 90], + default: 30 + }, + ] +} diff --git a/erpnext/stock/report/inactive_items/inactive_items.json b/erpnext/stock/report/inactive_items/inactive_items.json new file mode 100644 index 00000000000..b9eb05ec050 --- /dev/null +++ b/erpnext/stock/report/inactive_items/inactive_items.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "creation": "2019-04-16 16:05:00.647308", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test Letter Head 1", + "modified": "2019-04-16 16:06:33.630043", + "modified_by": "Administrator", + "module": "Stock", + "name": "Inactive Items", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Sales Invoice", + "report_name": "Inactive Items", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/inactive_items/inactive_items.py b/erpnext/stock/report/inactive_items/inactive_items.py new file mode 100644 index 00000000000..8d879126da6 --- /dev/null +++ b/erpnext/stock/report/inactive_items/inactive_items.py @@ -0,0 +1,148 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate, add_days, today, cint +from frappe import _ + +def execute(filters=None): + + columns = get_columns() + data = get_data(filters) + return columns, data + +def get_columns(): + + columns = [ + { + "fieldname": "territory", + "fieldtype": "Link", + "label": _("Territory"), + "options": "Territory", + "width": 100 + }, + { + "fieldname": "item_group", + "fieldtype": "Link", + "label": _("Item Group"), + "options": "Item Group", + "width": 150 + }, + { + "fieldname": "item_name", + "fieldtype": "Link", + "options": "Item", + "label": "Item", + "width": 150 + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": _("Item Name"), + "width": 150 + }, + + { + "fieldname": "customer", + "fieldtype": "Link", + "label": _("Customer"), + "options": "Customer", + "width": 100 + }, + { + "fieldname": "last_order_date", + "fieldtype": "Date", + "label": _("Last Order Date"), + "width": 100 + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "label": _("Quantity"), + "width": 100 + }, + { + "fieldname": "days_since_last_order", + "fieldtype": "Int", + "label": _("Days Since Last Order"), + "width": 100 + }, + ] + + return columns + + +def get_data(filters): + + data = [] + items = get_items(filters) + sales_invoice_data = get_sales_details(filters) + + for item in items: + if sales_invoice_data.get(item.name): + item_obj = sales_invoice_data[item.name] + if item_obj.days_since_last_order > cint(filters['days']): + row = { + "territory": item_obj.territory, + "item_group": item_obj.item_group, + "item": item_obj.name, + "item_name": item_obj.item_name, + "customer": item_obj.customer, + "last_order_date": item_obj.last_order_date, + "qty": item_obj.qty, + "days_since_last_order": item_obj.days_since_last_order + } + data.append(row) + else: + row = { + "item_group": item.item_group, + "item": item.name, + "item_name": item.item_name + } + data.append(row) + + return data + + +def get_sales_details(filters): + + data = [] + item_details_map = {} + + date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date" + + sales_data = frappe.db.sql(""" + select s.territory, s.customer, si.item_group, si.item_name, si.qty, {date_field} as last_order_date, + DATEDIFF(CURDATE(), {date_field}) as days_since_last_order + from `tab{doctype}` s, `tab{doctype} Item` si + where s.name = si.parent and s.docstatus = 1 + group by si.name order by days_since_last_order """ #nosec + .format(date_field = date_field, doctype = filters['based_on']), as_dict=1) + + for d in sales_data: + item_details_map.setdefault(d.item_name, d) + + return item_details_map + +def get_items(filters): + + filters_dict = { + "disabled": 0, + "is_stock_item": 1 + } + + if filters.get("item_group"): + filters_dict.update({ + "item_group": filters["item_group"] + }) + + if filters.get("item"): + filters_dict.update({ + "name": filters["item"] + }) + + items = frappe.get_all("Item", fields=["name", "item_group", "item_name"], filters=filters_dict, order_by="name") + + return items +