From 4945b94950f0c762a313e9e9b28d7a3f701c894c Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Wed, 30 Sep 2015 16:41:15 +0530 Subject: [PATCH] [fix] added validation to match account currency with existing gle and test case --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 14 +++--- .../journal_entry/test_journal_entry.py | 49 ++++++++++++++++--- .../purchase_invoice/test_purchase_invoice.py | 28 +++++------ .../sales_invoice/test_sales_invoice.py | 43 ++++++++-------- erpnext/accounts/party.py | 20 ++++++++ erpnext/controllers/accounts_controller.py | 6 +-- erpnext/exceptions.py | 7 +++ .../selling/doctype/customer/test_customer.py | 2 +- .../doctype/sales_order/test_sales_order.py | 2 + erpnext/setup/doctype/company/company.py | 10 ++-- 10 files changed, 122 insertions(+), 59 deletions(-) create mode 100644 erpnext/exceptions.py diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 5551ab7e5d..3927b8e61d 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -6,12 +6,10 @@ import frappe from frappe import _ from frappe.utils import flt, fmt_money, getdate, formatdate from frappe.model.document import Document -from erpnext.accounts.party import get_party_account_currency +from erpnext.accounts.party import validate_party_gle_currency, get_party_account_currency from erpnext.accounts.utils import get_account_currency - -class CustomerFrozen(frappe.ValidationError): pass -class InvalidCurrency(frappe.ValidationError): pass -class InvalidAccountCurrency(frappe.ValidationError): pass +from erpnext.setup.doctype.company.company import get_company_currency +from erpnext.exceptions import InvalidAccountCurrency, CustomerFrozen class GLEntry(Document): def validate(self): @@ -103,16 +101,16 @@ class GLEntry(Document): frappe.throw("{0} {1} is frozen".format(self.party_type, self.party), CustomerFrozen) def validate_currency(self): - company_currency = frappe.db.get_value("Company", self.company, "default_currency") + company_currency = get_company_currency(self.company) account_currency = get_account_currency(self.account) if not self.account_currency: self.account_currency = company_currency + if account_currency != self.account_currency: frappe.throw(_("Accounting Entry for {0} can only be made in currency: {1}") .format(self.account, (account_currency or company_currency)), InvalidAccountCurrency) - if self.party_type and self.party: party_account_currency = get_party_account_currency(self.party_type, self.party, self.company) @@ -120,6 +118,8 @@ class GLEntry(Document): frappe.throw(_("Accounting Entry for {0}: {1} can only be made in currency: {2}") .format(self.party_type, self.party, party_account_currency), InvalidAccountCurrency) + validate_party_gle_currency(self.party_type, self.party, self.company) + def validate_balance_type(account, adv_adj=False): if not adv_adj and account: balance_must_be = frappe.db.get_value("Account", account, "balance_must_be") diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index 753e0126c9..a4d6406fba 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import unittest, frappe from frappe.utils import flt from erpnext.accounts.utils import get_actual_expense, BudgetError, get_fiscal_year +from erpnext.exceptions import InvalidAccountCurrency class TestJournalEntry(unittest.TestCase): @@ -166,15 +167,15 @@ class TestJournalEntry(unittest.TestCase): existing_expense = self.get_actual_expense(posting_date) make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True) - + def test_multi_currency(self): jv = make_journal_entry("_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False) - + jv.get("accounts")[1].credit_in_account_currency = 5000 jv.submit() - - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + + gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s order by account asc""", jv.name, as_dict=1) @@ -197,12 +198,10 @@ class TestJournalEntry(unittest.TestCase): "credit_in_account_currency": 5000 } } - + for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): for i, gle in enumerate(gl_entries): self.assertEquals(expected_values[gle.account][field], gle[field]) - - # cancel jv.cancel() @@ -212,6 +211,40 @@ class TestJournalEntry(unittest.TestCase): self.assertFalse(gle) + def test_disallow_change_in_account_currency_for_a_party(self): + # create jv in USD + jv = make_journal_entry("_Test Bank USD - _TC", + "_Test Receivable USD - _TC", 100, save=False) + + jv.accounts[1].update({ + "party_type": "Customer", + "party": "_Test Customer USD" + }) + + jv.submit() + + # create jv in USD, but account currency in INR + jv = make_journal_entry("_Test Bank - _TC", + "_Test Receivable - _TC", 100, save=False) + + jv.accounts[1].update({ + "party_type": "Customer", + "party": "_Test Customer USD" + }) + + self.assertRaises(InvalidAccountCurrency, jv.submit) + + # back in USD + jv = make_journal_entry("_Test Bank USD - _TC", + "_Test Receivable USD - _TC", 100, save=False) + + jv.accounts[1].update({ + "party_type": "Customer", + "party": "_Test Customer USD" + }) + + jv.submit() + def make_journal_entry(account1, account2, amount, cost_center=None, exchange_rate=1, save=True, submit=False): jv = frappe.new_doc("Journal Entry") jv.posting_date = "2013-02-14" @@ -231,7 +264,7 @@ def make_journal_entry(account1, account2, amount, cost_center=None, exchange_ra "cost_center": cost_center, "credit_in_account_currency": amount if amount > 0 else 0, "debit_in_account_currency": abs(amount) if amount < 0 else 0, - exchange_rate: exchange_rate + "exchange_rate": exchange_rate } ]) if save or submit: diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index b39f30bbdc..67286db202 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -10,7 +10,7 @@ from frappe.utils import cint import frappe.defaults from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \ test_records as pr_test_records -from erpnext.controllers.accounts_controller import InvalidCurrency +from erpnext.exceptions import InvalidCurrency test_dependencies = ["Item", "Cost Center"] test_ignore = ["Serial No"] @@ -219,7 +219,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi.load_from_db() self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_type='Purchase Invoice' + where reference_type='Purchase Invoice' and reference_name=%s and debit_in_account_currency=300""", pi.name)) self.assertEqual(pi.outstanding_amount, 1212.30) @@ -237,17 +237,17 @@ class TestPurchaseInvoice(unittest.TestCase): existing_purchase_cost = frappe.db.sql("""select sum(ifnull(base_net_amount, 0)) from `tabPurchase Invoice Item` where project_name = '_Test Project' and docstatus=1""") existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 - + pi = make_purchase_invoice(currency="USD", conversion_rate=60, project_name="_Test Project") - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost + 15000) pi1 = make_purchase_invoice(qty=10, project_name="_Test Project") - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost + 15500) pi1.cancel() - self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), + self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost + 15000) pi.cancel() @@ -278,14 +278,14 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEquals(expected_values[gle.account][1], gle.credit) set_perpetual_inventory(0) - + def test_multi_currency_gle(self): set_perpetual_inventory(0) - - pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", + + pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", currency="USD", conversion_rate=50) - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s order by account asc""", pi.name, as_dict=1) @@ -308,16 +308,16 @@ class TestPurchaseInvoice(unittest.TestCase): "credit_in_account_currency": 0 } } - + for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): for i, gle in enumerate(gl_entries): self.assertEquals(expected_values[gle.account][field], gle[field]) - - + + # Check for valid currency pi1 = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", do_not_save=True) - + self.assertRaises(InvalidCurrency, pi1.save) # cancel diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 449f98d8f2..0eae7cbcfb 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -7,8 +7,7 @@ import unittest, copy from frappe.utils import nowdate, add_days, flt from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.controllers.accounts_controller import InvalidCurrency -from erpnext.accounts.doctype.gl_entry.gl_entry import InvalidAccountCurrency +from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency class TestSalesInvoice(unittest.TestCase): def make(self): @@ -842,13 +841,13 @@ class TestSalesInvoice(unittest.TestCase): self.assertEquals(si.total_taxes_and_charges, 234.44) self.assertEquals(si.base_grand_total, 859.44) self.assertEquals(si.grand_total, 859.44) - + def test_multi_currency_gle(self): set_perpetual_inventory(0) - si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", + si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", currency="USD", conversion_rate=50) - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s order by account asc""", si.name, as_dict=1) @@ -871,7 +870,7 @@ class TestSalesInvoice(unittest.TestCase): "credit_in_account_currency": 5000 } } - + for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): for i, gle in enumerate(gl_entries): self.assertEquals(expected_values[gle.account][field], gle[field]) @@ -883,38 +882,38 @@ class TestSalesInvoice(unittest.TestCase): where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) self.assertFalse(gle) - + def test_invalid_currency(self): # Customer currency = USD - + # Transaction currency cannot be INR - si1 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", + si1 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", do_not_save=True) - + self.assertRaises(InvalidCurrency, si1.save) - + # Transaction currency cannot be EUR - si2 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", + si2 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", currency="EUR", conversion_rate=80, do_not_save=True) - + self.assertRaises(InvalidCurrency, si2.save) - + # Transaction currency only allowed in USD - si3 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", + si3 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", currency="USD", conversion_rate=50) - + # Party Account currency must be in USD, as there is existing GLE with USD - si4 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC", + si4 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC", currency="USD", conversion_rate=50, do_not_submit=True) - + self.assertRaises(InvalidAccountCurrency, si4.submit) - + # Party Account currency must be in USD, force customer currency as there is no GLE - + si3.cancel() - si5 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC", + si5 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC", currency="USD", conversion_rate=50, do_not_submit=True) - + self.assertRaises(InvalidAccountCurrency, si5.submit) def create_sales_invoice(**args): diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 8181febd58..1eaf8a7931 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -10,6 +10,7 @@ from frappe.defaults import get_user_permissions from frappe.utils import add_days, getdate, formatdate, get_first_day, date_diff from erpnext.utilities.doctype.address.address import get_address_display from erpnext.utilities.doctype.contact.contact import get_contact_details +from erpnext.exceptions import InvalidAccountCurrency class DuplicatePartyAccountError(frappe.ValidationError): pass @@ -191,6 +192,25 @@ def get_party_account_currency(party_type, party, company): return frappe.local_cache("party_account_currency", (party_type, party, company), generator) +def get_party_gle_currency(party_type, party, company): + def generator(): + existing_gle_currency = frappe.db.sql("""select account_currency from `tabGL Entry` + where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s + limit 1""", { "company": company, "party_type": party_type, "party": party }) + + return existing_gle_currency[0][0] if existing_gle_currency else None + + return frappe.local_cache("party_gle_currency", (party_type, party, company), generator) + +def validate_party_gle_currency(party_type, party, company): + """Validate party account currency with existing GL Entry's currency""" + party_account_currency = get_party_account_currency(party_type, party, company) + existing_gle_currency = get_party_gle_currency(party_type, party, company) + + if existing_gle_currency and party_account_currency != existing_gle_currency: + frappe.throw(_("Accounting Entry for {0}: {1} can only be made in currency: {2}") + .format(party_type, party, existing_gle_currency), InvalidAccountCurrency) + def validate_party_accounts(doc): companies = [] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4f3e6c84e9..a92d070780 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -10,13 +10,11 @@ from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year, get_ac from erpnext.utilities.transaction_base import TransactionBase from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document from erpnext.controllers.sales_and_purchase_return import validate_return -from erpnext.accounts.party import get_party_account_currency +from erpnext.accounts.party import get_party_account_currency, validate_party_gle_currency +from erpnext.exceptions import CustomerFrozen, InvalidCurrency force_item_fields = ("item_group", "barcode", "brand", "stock_uom") -class CustomerFrozen(frappe.ValidationError): pass -class InvalidCurrency(frappe.ValidationError): pass - class AccountsController(TransactionBase): def __init__(self, arg1, arg2=None): super(AccountsController, self).__init__(arg1, arg2) diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py new file mode 100644 index 0000000000..c339edf239 --- /dev/null +++ b/erpnext/exceptions.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +# accounts +class CustomerFrozen(frappe.ValidationError): pass +class InvalidAccountCurrency(frappe.ValidationError): pass +class InvalidCurrency(frappe.ValidationError): pass diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 4bd02d5112..f18194f3ab 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -7,7 +7,7 @@ import frappe import unittest from frappe.test_runner import make_test_records -from erpnext.controllers.accounts_controller import CustomerFrozen +from erpnext.exceptions import CustomerFrozen test_ignore = ["Price List"] diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ccb792f111..ba5f025cfe 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -7,6 +7,8 @@ import frappe.permissions import unittest from erpnext.selling.doctype.sales_order.sales_order \ import make_material_request, make_delivery_note, make_sales_invoice, WarehouseRequired +from erpnext.accounts.doctype.journal_entry.test_journal_entry \ + import make_journal_entry from frappe.tests.test_permissions import set_user_permission_doctypes diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 785e7aa6fa..d337805344 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -46,13 +46,13 @@ class Company(Document): if for_company != self.name: frappe.throw(_("Account {0} does not belong to company: {1}") .format(self.get(field), self.name)) - + def validate_currency(self): self.previous_default_currency = frappe.db.get_value("Company", self.name, "default_currency") if self.default_currency and self.previous_default_currency and \ self.default_currency != self.previous_default_currency and \ self.check_if_transactions_exist(): - frappe.throw(_("Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency.")) + frappe.throw(_("Cannot change company's default currency, because there are existing transactions. Transactions must be cancelled to change the default currency.")) def on_update(self): if not frappe.db.sql("""select name from tabAccount @@ -208,7 +208,7 @@ class Company(Document): # clear default accounts, warehouses from item if warehouses: - + for f in ["default_warehouse", "website_warehouse"]: frappe.db.sql("""update tabItem set %s=NULL where %s in (%s)""" % (f, f, ', '.join(['%s']*len(warehouses))), tuple(warehouses)) @@ -257,3 +257,7 @@ def get_name_with_abbr(name, company): parts.append(company_abbr) return " - ".join(parts) + +def get_company_currency(company): + return frappe.local_cache("company_currency", company, + lambda: frappe.db.get_value("Company", company, "default_currency"))