Merge pull request #41997 from frappe/mergify/bp/version-15-hotfix/pr-41844

refactor: allow foreign currency advance accounts (backport #41844)
This commit is contained in:
ruthra kumar
2024-06-23 05:43:10 +05:30
committed by GitHub
11 changed files with 436 additions and 65 deletions

View File

@@ -76,7 +76,6 @@ class PaymentEntry(AccountsController):
self.setup_party_account_field()
self.set_missing_values()
self.set_liability_account()
self.validate_advance_account_currency()
self.set_missing_ref_details(force=True)
self.validate_payment_type()
self.validate_party_details()
@@ -163,22 +162,6 @@ class PaymentEntry(AccountsController):
alert=True,
)
def validate_advance_account_currency(self):
if self.book_advance_payments_in_separate_party_account is True:
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
if self.payment_type == "Receive" and self.paid_from_account_currency != company_currency:
frappe.throw(
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
frappe.bold(self.paid_from), frappe.bold(self.paid_from_account_currency)
)
)
if self.payment_type == "Pay" and self.paid_to_account_currency != company_currency:
frappe.throw(
_("Booking advances in foreign currency account: {0} ({1}) is not yet supported.").format(
frappe.bold(self.paid_to), frappe.bold(self.paid_to_account_currency)
)
)
def on_cancel(self):
self.ignore_linked_doctypes = (
"GL Entry",
@@ -1266,7 +1249,7 @@ class PaymentEntry(AccountsController):
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
args_dict["account"] = account
args_dict[dr_or_cr] = invoice.allocated_amount
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
args_dict.update(
{
@@ -1285,7 +1268,7 @@ class PaymentEntry(AccountsController):
args_dict[dr_or_cr + "_in_account_currency"] = 0
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
args_dict["account"] = self.party_account
args_dict[dr_or_cr] = invoice.allocated_amount
args_dict[dr_or_cr] = self.calculate_base_allocated_amount_for_reference(invoice)
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
args_dict.update(
{

View File

@@ -476,7 +476,7 @@ class TestSubscription(FrappeTestCase):
start_date="2021-01-01",
submit_invoice=0,
generate_new_invoices_past_due_date=1,
party="_Test Subscription Customer",
party="_Test Subscription Customer John Doe",
)
# create invoices for the first two moths
@@ -565,10 +565,16 @@ def create_parties():
if not frappe.db.exists("Customer", "_Test Subscription Customer"):
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Subscription Customer"
customer.billing_currency = "USD"
customer.default_currency = "USD"
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer John Doe"):
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Subscription Customer John Doe"
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable - _TC"})
customer.insert()
def reset_settings():
settings = frappe.get_single("Subscription Settings")

View File

@@ -138,6 +138,7 @@ class Supplier(TransactionBase):
validate_party_accounts(self)
self.validate_internal_supplier()
self.add_role_for_user()
self.validate_currency_for_receivable_payable_and_advance_account()
@frappe.whitelist()
def get_supplier_group_details(self):

View File

@@ -35,6 +35,7 @@
"doctype": "Supplier",
"supplier_name": "_Test Supplier USD",
"supplier_group": "_Test Supplier Group",
"default_currency": "USD",
"accounts": [{
"company": "_Test Company",
"account": "_Test Payable USD - _TC"

View File

@@ -1430,10 +1430,13 @@ class AccountsController(TransactionBase):
if d.exchange_gain_loss and (
(d.reference_doctype, d.reference_name, str(d.idx)) not in booked
):
if self.payment_type == "Receive":
party_account = self.paid_from
elif self.payment_type == "Pay":
party_account = self.paid_to
if self.book_advance_payments_in_separate_party_account:
party_account = d.account
else:
if self.payment_type == "Receive":
party_account = self.paid_from
elif self.payment_type == "Pay":
party_account = self.paid_to
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"

View File

@@ -10,6 +10,7 @@ from frappe.utils import add_days, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.stock.doctype.item.test_item import create_item
@@ -55,6 +56,7 @@ class TestAccountsController(FrappeTestCase):
40 series - Company default Cost center is unset
50 series - Journals against Journals
60 series - Journals against Payment Entries
70 series - Advances in Separate party account. Both Party and Advance account are in Foreign currency.
90 series - Dimension inheritence
"""
@@ -114,47 +116,102 @@ class TestAccountsController(FrappeTestCase):
self.supplier = make_supplier("_Test MC Supplier USD", "USD")
def create_account(self):
account_name = "Debtors USD"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Receivable - " + self.company_abbr
acc.company = self.company
acc.account_currency = "USD"
acc.account_type = "Receivable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.debtors_usd = acc.name
accounts = [
frappe._dict(
{
"attribute_name": "debtors_usd",
"name": "Debtors USD",
"account_type": "Receivable",
"account_currency": "USD",
"parent_account": "Accounts Receivable - " + self.company_abbr,
}
),
frappe._dict(
{
"attribute_name": "creditors_usd",
"name": "Creditors USD",
"account_type": "Payable",
"account_currency": "USD",
"parent_account": "Accounts Payable - " + self.company_abbr,
}
),
# Advance accounts under Asset and Liability header
frappe._dict(
{
"attribute_name": "advance_received_usd",
"name": "Advance Received USD",
"account_type": "Receivable",
"account_currency": "USD",
"parent_account": "Current Liabilities - " + self.company_abbr,
}
),
frappe._dict(
{
"attribute_name": "advance_paid_usd",
"name": "Advance Paid USD",
"account_type": "Payable",
"account_currency": "USD",
"parent_account": "Current Assets - " + self.company_abbr,
}
),
]
account_name = "Creditors USD"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Payable - " + self.company_abbr
acc.company = self.company
acc.account_currency = "USD"
acc.account_type = "Payable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.creditors_usd = acc.name
for x in accounts:
if not frappe.db.get_value("Account", filters={"account_name": x.name, "company": self.company}):
acc = frappe.new_doc("Account")
acc.account_name = x.name
acc.parent_account = x.parent_account
acc.company = self.company
acc.account_currency = x.account_currency
acc.account_type = x.account_type
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": x.name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
setattr(self, x.attribute_name, acc.name)
def setup_advance_accounts_in_party_master(self):
company = frappe.get_doc("Company", self.company)
company.book_advance_payments_in_separate_party_account = 1
company.save()
customer = frappe.get_doc("Customer", self.customer)
customer.append(
"accounts",
{
"company": self.company,
"account": self.debtors_usd,
"advance_account": self.advance_received_usd,
},
)
customer.save()
supplier = frappe.get_doc("Supplier", self.supplier)
supplier.append(
"accounts",
{
"company": self.company,
"account": self.creditors_usd,
"advance_account": self.advance_paid_usd,
},
)
supplier.save()
def remove_advance_accounts_from_party_master(self):
company = frappe.get_doc("Company", self.company)
company.book_advance_payments_in_separate_party_account = 0
company.save()
customer = frappe.get_doc("Customer", self.customer)
customer.accounts = []
customer.save()
supplier = frappe.get_doc("Supplier", self.supplier)
supplier.accounts = []
supplier.save()
def create_sales_invoice(
self,
@@ -218,6 +275,48 @@ class TestAccountsController(FrappeTestCase):
payment.posting_date = posting_date
return payment
def create_purchase_invoice(
self,
qty=1,
rate=1,
conversion_rate=80,
posting_date=None,
do_not_save=False,
do_not_submit=False,
):
"""
Helper function to populate default values in purchase invoice
"""
if posting_date is None:
posting_date = nowdate()
pinv = make_purchase_invoice(
posting_date=posting_date,
qty=qty,
rate=rate,
company=self.company,
supplier=self.supplier,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
parent_cost_center=self.cost_center,
update_stock=0,
currency="USD",
conversion_rate=conversion_rate,
is_pos=0,
is_return=0,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=True,
)
pinv.credit_to = self.creditors_usd
if not do_not_save:
pinv.save()
if not do_not_submit:
pinv.submit()
return pinv
def clear_old_entries(self):
doctype_list = [
"GL Entry",
@@ -1698,3 +1797,123 @@ class TestAccountsController(FrappeTestCase):
# Exchange Gain/Loss Journal should've been cancelled
exc_je_for_je1 = self.get_journals_for(je1.doctype, je1.name)
self.assertEqual(exc_je_for_je1, [])
def test_70_advance_payment_against_sales_invoice_in_foreign_currency(self):
"""
Customer advance booked under Liability
"""
self.setup_advance_accounts_in_party_master()
adv = self.create_payment_entry(amount=1, source_exc_rate=83)
adv.save() # explicit 'save' is needed to trigger set_liability_account()
self.assertEqual(adv.paid_from, self.advance_received_usd)
adv.submit()
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1, do_not_submit=True)
si.debit_to = self.debtors_usd
si.save().submit()
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
pr = self.create_payment_reconciliation()
pr.receivable_payable_account = self.debtors_usd
pr.default_advance_account = self.advance_received_usd
pr.get_unreconciled_entries()
self.assertEqual(pr.invoices[0].invoice_number, si.name)
self.assertEqual(pr.payments[0].reference_name, adv.name)
# Allocate and Reconcile
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
# Exc Gain/Loss journal should've been creatad
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
self.assertEqual(len(exc_je_for_si), 1)
self.assertEqual(len(exc_je_for_adv), 1)
self.assertEqual(exc_je_for_si, exc_je_for_adv)
adv.reload()
adv.cancel()
si.reload()
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
# Exc Gain/Loss journal should've been cancelled
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
self.assertEqual(len(exc_je_for_si), 0)
self.assertEqual(len(exc_je_for_adv), 0)
self.remove_advance_accounts_from_party_master()
def test_71_advance_payment_against_purchase_invoice_in_foreign_currency(self):
"""
Supplier advance booked under Asset
"""
self.setup_advance_accounts_in_party_master()
usd_amount = 1
inr_amount = 85
exc_rate = 85
adv = create_payment_entry(
company=self.company,
payment_type="Pay",
party_type="Supplier",
party=self.supplier,
paid_from=self.cash,
paid_to=self.advance_paid_usd,
paid_amount=inr_amount,
)
adv.source_exchange_rate = 1
adv.target_exchange_rate = exc_rate
adv.received_amount = usd_amount
adv.paid_amount = exc_rate * usd_amount
adv.posting_date = nowdate()
adv.save()
# Make sure that advance account is still set
self.assertEqual(adv.paid_to, self.advance_paid_usd)
adv.submit()
pi = self.create_purchase_invoice(qty=1, conversion_rate=83, rate=1)
self.assertEqual(pi.credit_to, self.creditors_usd)
self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0)
pr = self.create_payment_reconciliation()
pr.party_type = "Supplier"
pr.party = self.supplier
pr.receivable_payable_account = self.creditors_usd
pr.default_advance_account = self.advance_paid_usd
pr.get_unreconciled_entries()
self.assertEqual(pr.invoices[0].invoice_number, pi.name)
self.assertEqual(pr.payments[0].reference_name, adv.name)
# Allocate and Reconcile
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
self.assert_ledger_outstanding(pi.doctype, pi.name, 0.0, 0.0)
# Exc Gain/Loss journal should've been creatad
exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name)
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
self.assertEqual(len(exc_je_for_pi), 1)
self.assertEqual(len(exc_je_for_adv), 1)
self.assertEqual(exc_je_for_pi, exc_je_for_adv)
adv.reload()
adv.cancel()
pi.reload()
self.assert_ledger_outstanding(pi.doctype, pi.name, 83.0, 1.0)
# Exc Gain/Loss journal should've been cancelled
exc_je_for_pi = self.get_journals_for(pi.doctype, pi.name)
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
self.assertEqual(len(exc_je_for_pi), 0)
self.assertEqual(len(exc_je_for_adv), 0)
self.remove_advance_accounts_from_party_master()

View File

@@ -144,6 +144,7 @@ class Customer(TransactionBase):
self.validate_default_bank_account()
self.validate_internal_customer()
self.add_role_for_user()
self.validate_currency_for_receivable_payable_and_advance_account()
# set loyalty program tier
if frappe.db.exists("Customer", self.name):

View File

@@ -47,6 +47,7 @@
"customer_type": "Individual",
"doctype": "Customer",
"territory": "_Test Territory",
"default_currency": "USD",
"accounts": [{
"company": "_Test Company",
"account": "_Test Receivable USD - _TC"

View File

@@ -38,6 +38,53 @@ class CustomerGroup(NestedSet):
def validate(self):
if not self.parent_customer_group:
self.parent_customer_group = get_root_of("Customer Group")
self.validate_currency_for_receivable_and_advance_account()
def validate_currency_for_receivable_and_advance_account(self):
for x in self.accounts:
company_default_currency = frappe.get_cached_value("Company", x.company, "default_currency")
receivable_account_currency = None
advance_account_currency = None
if x.account:
receivable_account_currency = frappe.get_cached_value(
"Account", x.account, "account_currency"
)
if x.advance_account:
advance_account_currency = frappe.get_cached_value(
"Account", x.advance_account, "account_currency"
)
if receivable_account_currency and receivable_account_currency != company_default_currency:
frappe.throw(
_("Receivable Account: {0} must be in Company default currency: {1}").format(
frappe.bold(x.account),
frappe.bold(company_default_currency),
)
)
if advance_account_currency and advance_account_currency != company_default_currency:
frappe.throw(
_("Advance Account: {0} must be in Company default currency: {1}").format(
frappe.bold(x.advance_account), frappe.bold(company_default_currency)
)
)
if (
receivable_account_currency
and advance_account_currency
and receivable_account_currency != advance_account_currency
):
frappe.throw(
_(
"Both Receivable Account: {0} and Advance Account: {1} must be of same currency for company: {2}"
).format(
frappe.bold(x.account),
frappe.bold(x.advance_account),
frappe.bold(x.company),
)
)
def on_update(self):
self.validate_name_with_customer()

View File

@@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.utils.nestedset import NestedSet, get_root_of
@@ -32,6 +33,51 @@ class SupplierGroup(NestedSet):
def validate(self):
if not self.parent_supplier_group:
self.parent_supplier_group = get_root_of("Supplier Group")
self.validate_currency_for_payable_and_advance_account()
def validate_currency_for_payable_and_advance_account(self):
for x in self.accounts:
company_default_currency = frappe.get_cached_value("Company", x.company, "default_currency")
payable_account_currency = None
advance_account_currency = None
if x.account:
payable_account_currency = frappe.get_cached_value("Account", x.account, "account_currency")
if x.advance_account:
advance_account_currency = frappe.get_cached_value(
"Account", x.advance_account, "account_currency"
)
if payable_account_currency and payable_account_currency != company_default_currency:
frappe.throw(
_("Payable Account: {0} must be in Company default currency: {1}").format(
frappe.bold(x.account),
frappe.bold(company_default_currency),
)
)
if advance_account_currency and advance_account_currency != company_default_currency:
frappe.throw(
_("Advance Account: {0} must be in Company default currency: {1}").format(
frappe.bold(x.advance_account), frappe.bold(company_default_currency)
)
)
if (
payable_account_currency
and advance_account_currency
and payable_account_currency != advance_account_currency
):
frappe.throw(
_(
"Both Payable Account: {0} and Advance Account: {1} must be of same currency for company: {2}"
).format(
frappe.bold(x.account),
frappe.bold(x.advance_account),
frappe.bold(x.company),
)
)
def on_update(self):
NestedSet.on_update(self)

View File

@@ -168,6 +168,69 @@ class TransactionBase(StatusUpdater):
if len(child_table_values) > 1:
self.set(default_field, None)
def validate_currency_for_receivable_payable_and_advance_account(self):
if self.doctype in ["Customer", "Supplier"]:
account_type = "Receivable" if self.doctype == "Customer" else "Payable"
for x in self.accounts:
company_default_currency = frappe.get_cached_value("Company", x.company, "default_currency")
receivable_payable_account_currency = None
advance_account_currency = None
if x.account:
receivable_payable_account_currency = frappe.get_cached_value(
"Account", x.account, "account_currency"
)
if x.advance_account:
advance_account_currency = frappe.get_cached_value(
"Account", x.advance_account, "account_currency"
)
if receivable_payable_account_currency and (
receivable_payable_account_currency != self.default_currency
and receivable_payable_account_currency != company_default_currency
):
frappe.throw(
_(
"{0} Account: {1} ({2}) must be in either customer billing currency: {3} or Company default currency: {4}"
).format(
account_type,
frappe.bold(x.account),
frappe.bold(receivable_payable_account_currency),
frappe.bold(self.default_currency),
frappe.bold(company_default_currency),
)
)
if advance_account_currency and (
advance_account_currency != self.default_currency
and advance_account_currency != company_default_currency
):
frappe.throw(
_(
"Advance Account: {0} must be in either customer billing currency: {1} or Company default currency: {2}"
).format(
frappe.bold(x.advance_account),
frappe.bold(self.default_currency),
frappe.bold(company_default_currency),
)
)
if (
receivable_payable_account_currency
and advance_account_currency
and receivable_payable_account_currency != advance_account_currency
):
frappe.throw(
_(
"Both {0} Account: {1} and Advance Account: {2} must be of same currency for company: {3}"
).format(
account_type,
frappe.bold(x.account),
frappe.bold(x.advance_account),
frappe.bold(x.company),
)
)
def delete_events(ref_type, ref_name):
events = (