mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-15 03:01:22 +00:00
Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-36642
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold, throw
|
||||
from frappe import _, bold, qb, throw
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import (
|
||||
@@ -38,7 +38,12 @@ from erpnext.accounts.party import (
|
||||
get_party_gle_currency,
|
||||
validate_party_frozen_disabled,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
|
||||
from erpnext.accounts.utils import (
|
||||
create_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_fiscal_years,
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.buying.utils import update_last_purchase_rate
|
||||
from erpnext.controllers.print_settings import (
|
||||
set_print_templates_for_item_table,
|
||||
@@ -962,67 +967,133 @@ class AccountsController(TransactionBase):
|
||||
|
||||
d.exchange_gain_loss = difference
|
||||
|
||||
def make_exchange_gain_loss_gl_entries(self, gl_entries):
|
||||
if self.get("doctype") in ["Purchase Invoice", "Sales Invoice"]:
|
||||
for d in self.get("advances"):
|
||||
if d.exchange_gain_loss:
|
||||
is_purchase_invoice = self.get("doctype") == "Purchase Invoice"
|
||||
party = self.supplier if is_purchase_invoice else self.customer
|
||||
party_account = self.credit_to if is_purchase_invoice else self.debit_to
|
||||
party_type = "Supplier" if is_purchase_invoice else "Customer"
|
||||
def make_exchange_gain_loss_journal(self, args: dict = None) -> None:
|
||||
"""
|
||||
Make Exchange Gain/Loss journal for Invoices and Payments
|
||||
"""
|
||||
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
||||
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
||||
if self.docstatus == 1:
|
||||
if self.get("doctype") == "Journal Entry":
|
||||
# 'args' is populated with exchange gain/loss account and the amount to be booked.
|
||||
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
|
||||
# and below logic is only for such scenarios
|
||||
if args:
|
||||
for arg in args:
|
||||
# Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount`
|
||||
if (
|
||||
arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0
|
||||
) and arg.get("difference_account"):
|
||||
|
||||
gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
)
|
||||
if not gain_loss_account:
|
||||
frappe.throw(
|
||||
_("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company"))
|
||||
)
|
||||
account_currency = get_account_currency(gain_loss_account)
|
||||
if account_currency != self.company_currency:
|
||||
frappe.throw(
|
||||
_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency)
|
||||
)
|
||||
party_account = arg.get("account")
|
||||
gain_loss_account = arg.get("difference_account")
|
||||
difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss")
|
||||
if difference_amount > 0:
|
||||
dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit"
|
||||
else:
|
||||
dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit"
|
||||
|
||||
# for purchase
|
||||
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
||||
if not is_purchase_invoice:
|
||||
# just reverse for sales?
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": gain_loss_account,
|
||||
"account_currency": account_currency,
|
||||
"against": party,
|
||||
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
|
||||
dr_or_cr: abs(d.exchange_gain_loss),
|
||||
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
|
||||
"project": self.project,
|
||||
},
|
||||
item=d,
|
||||
je = create_gain_loss_journal(
|
||||
self.company,
|
||||
arg.get("party_type"),
|
||||
arg.get("party"),
|
||||
party_account,
|
||||
gain_loss_account,
|
||||
difference_amount,
|
||||
dr_or_cr,
|
||||
reverse_dr_or_cr,
|
||||
arg.get("against_voucher_type"),
|
||||
arg.get("against_voucher"),
|
||||
arg.get("idx"),
|
||||
self.doctype,
|
||||
self.name,
|
||||
arg.get("idx"),
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
get_link_to_form("Journal Entry", je)
|
||||
)
|
||||
)
|
||||
|
||||
if self.get("doctype") == "Payment Entry":
|
||||
# For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation
|
||||
gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0]
|
||||
booked = []
|
||||
if gain_loss_to_book:
|
||||
vtypes = [x.reference_doctype for x in gain_loss_to_book]
|
||||
vnames = [x.reference_name for x in gain_loss_to_book]
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
parents = (
|
||||
qb.from_(jea)
|
||||
.select(jea.parent)
|
||||
.where(
|
||||
(jea.reference_type == "Payment Entry")
|
||||
& (jea.reference_name == self.name)
|
||||
& (jea.docstatus == 1)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": party_account,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"against": gain_loss_account,
|
||||
dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate),
|
||||
dr_or_cr: abs(d.exchange_gain_loss),
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
},
|
||||
self.party_account_currency,
|
||||
item=self,
|
||||
booked = []
|
||||
if parents:
|
||||
booked = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(jea.reference_type, jea.reference_name, jea.reference_detail_no)
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (je.name.isin(parents))
|
||||
& (je.voucher_type == "Exchange Gain or Loss")
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
for d in gain_loss_to_book:
|
||||
# Filter out References for which Gain/Loss is already booked
|
||||
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
|
||||
|
||||
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
||||
|
||||
if d.reference_doctype == "Purchase Invoice":
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gain_loss_account = frappe.get_cached_value(
|
||||
"Company", self.company, "exchange_gain_loss_account"
|
||||
)
|
||||
|
||||
je = create_gain_loss_journal(
|
||||
self.company,
|
||||
self.party_type,
|
||||
self.party,
|
||||
party_account,
|
||||
gain_loss_account,
|
||||
d.exchange_gain_loss,
|
||||
dr_or_cr,
|
||||
reverse_dr_or_cr,
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
d.idx,
|
||||
self.doctype,
|
||||
self.name,
|
||||
d.idx,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
get_link_to_form("Journal Entry", je)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
@@ -1111,9 +1182,15 @@ class AccountsController(TransactionBase):
|
||||
reconcile_against_document(lst)
|
||||
|
||||
def on_cancel(self):
|
||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
)
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||
# Cancel Exchange Gain/Loss Journal before unlinking
|
||||
cancel_exchange_gain_loss_journal(self)
|
||||
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from erpnext.accounts.general_ledger import (
|
||||
make_reverse_gl_entries,
|
||||
process_gl_map,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
@@ -513,6 +513,7 @@ class StockController(AccountsController):
|
||||
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
||||
|
||||
def make_gl_entries_on_cancel(self):
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
if frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type=%s
|
||||
and voucher_no=%s""",
|
||||
|
||||
999
erpnext/controllers/tests/test_accounts_controller.py
Normal file
999
erpnext/controllers/tests/test_accounts_controller.py
Normal file
@@ -0,0 +1,999 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
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
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = customer_name
|
||||
customer.customer_type = "Individual"
|
||||
|
||||
if currency:
|
||||
customer.default_currency = currency
|
||||
customer.save()
|
||||
return customer.name
|
||||
else:
|
||||
return customer_name
|
||||
|
||||
|
||||
def make_supplier(supplier_name, currency=None):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = supplier_name
|
||||
supplier.supplier_type = "Individual"
|
||||
supplier.supplier_group = "All Supplier Groups"
|
||||
|
||||
if currency:
|
||||
supplier.default_currency = currency
|
||||
supplier.save()
|
||||
return supplier.name
|
||||
else:
|
||||
return supplier_name
|
||||
|
||||
|
||||
class TestAccountsController(FrappeTestCase):
|
||||
"""
|
||||
Test Exchange Gain/Loss booking on various scenarios.
|
||||
Test Cases are numbered for better organization
|
||||
|
||||
10 series - Sales Invoice against Payment Entries
|
||||
20 series - Sales Invoice against Journals
|
||||
30 series - Sales Invoice against Credit Notes
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_account()
|
||||
self.create_item()
|
||||
self.create_parties()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_company(self):
|
||||
company_name = "_Test Company"
|
||||
self.company_abbr = abbr = "_TC"
|
||||
if frappe.db.exists("Company", company_name):
|
||||
company = frappe.get_doc("Company", company_name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company_name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "Stores - " + abbr
|
||||
self.finished_warehouse = "Finished Goods - " + abbr
|
||||
self.income_account = "Sales - " + abbr
|
||||
self.expense_account = "Cost of Goods Sold - " + abbr
|
||||
self.debit_to = "Debtors - " + abbr
|
||||
self.debit_usd = "Debtors USD - " + abbr
|
||||
self.cash = "Cash - " + abbr
|
||||
self.creditors = "Creditors - " + abbr
|
||||
|
||||
def create_item(self):
|
||||
item = create_item(
|
||||
item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
def create_parties(self):
|
||||
self.create_customer()
|
||||
self.create_supplier()
|
||||
|
||||
def create_customer(self):
|
||||
self.customer = make_customer("_Test MC Customer USD", "USD")
|
||||
|
||||
def create_supplier(self):
|
||||
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
|
||||
|
||||
def create_sales_invoice(
|
||||
self,
|
||||
qty=1,
|
||||
rate=1,
|
||||
conversion_rate=80,
|
||||
posting_date=nowdate(),
|
||||
do_not_save=False,
|
||||
do_not_submit=False,
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_usd,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="USD",
|
||||
conversion_rate=conversion_rate,
|
||||
is_pos=0,
|
||||
is_return=0,
|
||||
return_against=None,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_payment_entry(
|
||||
self, amount=1, source_exc_rate=75, posting_date=nowdate(), customer=None
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=customer or self.customer,
|
||||
paid_from=self.debit_usd,
|
||||
paid_to=self.cash,
|
||||
paid_amount=amount,
|
||||
)
|
||||
payment.source_exchange_rate = source_exc_rate
|
||||
payment.received_amount = source_exc_rate * amount
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_payment_reconciliation(self):
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
return pr
|
||||
|
||||
def create_journal_entry(
|
||||
self,
|
||||
acc1=None,
|
||||
acc1_exc_rate=None,
|
||||
acc2_exc_rate=None,
|
||||
acc2=None,
|
||||
acc1_amount=0,
|
||||
acc2_amount=0,
|
||||
posting_date=None,
|
||||
cost_center=None,
|
||||
):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
je.company = self.company
|
||||
je.user_remark = "test"
|
||||
je.multi_currency = True
|
||||
if not cost_center:
|
||||
cost_center = self.cost_center
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": acc1,
|
||||
"exchange_rate": acc1_exc_rate or 1,
|
||||
"cost_center": cost_center,
|
||||
"debit_in_account_currency": acc1_amount if acc1_amount > 0 else 0,
|
||||
"credit_in_account_currency": abs(acc1_amount) if acc1_amount < 0 else 0,
|
||||
"debit": acc1_amount * acc1_exc_rate if acc1_amount > 0 else 0,
|
||||
"credit": abs(acc1_amount * acc1_exc_rate) if acc1_amount < 0 else 0,
|
||||
},
|
||||
{
|
||||
"account": acc2,
|
||||
"exchange_rate": acc2_exc_rate or 1,
|
||||
"cost_center": cost_center,
|
||||
"credit_in_account_currency": acc2_amount if acc2_amount > 0 else 0,
|
||||
"debit_in_account_currency": abs(acc2_amount) if acc2_amount < 0 else 0,
|
||||
"credit": acc2_amount * acc2_exc_rate if acc2_amount > 0 else 0,
|
||||
"debit": abs(acc2_amount * acc2_exc_rate) if acc2_amount < 0 else 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
return je
|
||||
|
||||
def get_journals_for(self, voucher_type: str, voucher_no: str) -> list:
|
||||
journals = []
|
||||
if voucher_type and voucher_no:
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1},
|
||||
fields=["parent"],
|
||||
)
|
||||
return journals
|
||||
|
||||
def assert_ledger_outstanding(
|
||||
self,
|
||||
voucher_type: str,
|
||||
voucher_no: str,
|
||||
outstanding: float,
|
||||
outstanding_in_account_currency: float,
|
||||
) -> None:
|
||||
"""
|
||||
Assert outstanding amount based on ledger on both company/base currency and account currency
|
||||
"""
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
current_outstanding = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Sum(ple.amount).as_("outstanding"),
|
||||
Sum(ple.amount_in_account_currency).as_("outstanding_in_account_currency"),
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == voucher_type)
|
||||
& (ple.against_voucher_no == voucher_no)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
.run(as_dict=True)[0]
|
||||
)
|
||||
self.assertEqual(outstanding, current_outstanding.outstanding)
|
||||
self.assertEqual(
|
||||
outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency
|
||||
)
|
||||
|
||||
def test_10_payment_against_sales_invoice(self):
|
||||
# Sales Invoice in Foreign Currency
|
||||
rate = 80
|
||||
rate_in_account_currency = 1
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency)
|
||||
|
||||
# Test payments with different exchange rates
|
||||
for exc_rate in [75.9, 83.1, 80.01]:
|
||||
with self.subTest(exc_rate=exc_rate):
|
||||
pe = self.create_payment_entry(amount=1, source_exc_rate=exc_rate).save()
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
|
||||
)
|
||||
pe = pe.save().submit()
|
||||
|
||||
# Outstanding in both currencies should be '0'
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_pe), 1)
|
||||
self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
|
||||
|
||||
# Cancel Payment
|
||||
pe.cancel()
|
||||
|
||||
# outstanding should be same as grand total
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, rate_in_account_currency)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been cancelled
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_pe, [])
|
||||
|
||||
def test_11_advance_against_sales_invoice(self):
|
||||
# Advance Payment
|
||||
adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
|
||||
adv.reload()
|
||||
|
||||
# Sales Invoices in different exchange rates
|
||||
for exc_rate in [75.9, 83.1, 80.01]:
|
||||
with self.subTest(exc_rate=exc_rate):
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
|
||||
advances = si.get_advance_entries()
|
||||
self.assertEqual(len(advances), 1)
|
||||
self.assertEqual(advances[0].reference_name, adv.name)
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"doctype": "Sales Invoice Advance",
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"reference_row": advances[0].reference_row,
|
||||
"advance_amount": 1,
|
||||
"allocated_amount": 1,
|
||||
"ref_exchange_rate": advances[0].exchange_rate,
|
||||
"remarks": advances[0].remarks,
|
||||
},
|
||||
)
|
||||
|
||||
si = si.save()
|
||||
si = si.submit()
|
||||
|
||||
# Outstanding in both currencies should be '0'
|
||||
adv.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
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.assertNotEqual(exc_je_for_si, [])
|
||||
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)
|
||||
|
||||
# Cancel Invoice
|
||||
si.cancel()
|
||||
|
||||
# Exchange 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(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_adv, [])
|
||||
|
||||
def test_12_partial_advance_and_payment_for_sales_invoice(self):
|
||||
"""
|
||||
Sales invoice with partial advance payment, and a normal payment reconciled
|
||||
"""
|
||||
# Partial Advance
|
||||
adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
|
||||
adv.reload()
|
||||
|
||||
# sales invoice with advance(partial amount)
|
||||
rate = 80
|
||||
rate_in_account_currency = 1
|
||||
si = self.create_sales_invoice(
|
||||
qty=2, conversion_rate=80, rate=rate_in_account_currency, do_not_submit=True
|
||||
)
|
||||
advances = si.get_advance_entries()
|
||||
self.assertEqual(len(advances), 1)
|
||||
self.assertEqual(advances[0].reference_name, adv.name)
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"doctype": "Sales Invoice Advance",
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": 1,
|
||||
"allocated_amount": 1,
|
||||
"ref_exchange_rate": advances[0].exchange_rate,
|
||||
"remarks": advances[0].remarks,
|
||||
},
|
||||
)
|
||||
si = si.save()
|
||||
si = si.submit()
|
||||
|
||||
# Outstanding should be there in both currencies
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1) # account currency
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created for the partial advance
|
||||
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.assertNotEqual(exc_je_for_si, [])
|
||||
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)
|
||||
|
||||
# Payment for remaining amount
|
||||
pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
|
||||
)
|
||||
pe = pe.save().submit()
|
||||
|
||||
# Outstanding in both currencies should be '0'
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created for the payment
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
# There should be 2 JE's now. One for the advance and one for the payment
|
||||
self.assertEqual(len(exc_je_for_si), 2)
|
||||
self.assertEqual(len(exc_je_for_pe), 1)
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
|
||||
|
||||
# Cancel Invoice
|
||||
si.reload()
|
||||
si.cancel()
|
||||
|
||||
# Exchange Gain/Loss Journal should been cancelled
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_pe, [])
|
||||
self.assertEqual(exc_je_for_adv, [])
|
||||
|
||||
def test_13_partial_advance_and_payment_for_invoice_with_cancellation(self):
|
||||
"""
|
||||
Invoice with partial advance payment, and a normal payment. Then cancel advance and payment.
|
||||
"""
|
||||
# Partial Advance
|
||||
adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
|
||||
adv.reload()
|
||||
|
||||
# invoice with advance(partial amount)
|
||||
si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1, do_not_submit=True)
|
||||
advances = si.get_advance_entries()
|
||||
self.assertEqual(len(advances), 1)
|
||||
self.assertEqual(advances[0].reference_name, adv.name)
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"doctype": "Sales Invoice Advance",
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": 1,
|
||||
"allocated_amount": 1,
|
||||
"ref_exchange_rate": advances[0].exchange_rate,
|
||||
"remarks": advances[0].remarks,
|
||||
},
|
||||
)
|
||||
si = si.save()
|
||||
si = si.submit()
|
||||
|
||||
# Outstanding should be there in both currencies
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1) # account currency
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created for the partial advance
|
||||
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.assertNotEqual(exc_je_for_si, [])
|
||||
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)
|
||||
|
||||
# Payment(remaining amount)
|
||||
pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
|
||||
)
|
||||
pe = pe.save().submit()
|
||||
|
||||
# Outstanding should be '0' in both currencies
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created for the payment
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
# There should be 2 JE's now. One for the advance and one for the payment
|
||||
self.assertEqual(len(exc_je_for_si), 2)
|
||||
self.assertEqual(len(exc_je_for_pe), 1)
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
|
||||
|
||||
adv.reload()
|
||||
adv.cancel()
|
||||
|
||||
# Outstanding should be there in both currencies, since advance is cancelled.
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1) # account currency
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
# Exchange Gain/Loss Journal for advance should been cancelled
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_pe), 1)
|
||||
self.assertEqual(exc_je_for_adv, [])
|
||||
|
||||
def test_14_same_payment_split_against_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
|
||||
# Payment
|
||||
pe = self.create_payment_entry(amount=2, source_exc_rate=75).save()
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
|
||||
)
|
||||
pe = pe.save().submit()
|
||||
|
||||
# There should be outstanding in both currencies
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_pe), 1)
|
||||
self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
|
||||
|
||||
# Reconcile the remaining amount
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = self.debit_usd
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
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)
|
||||
|
||||
# Exc gain/loss journal should have been creaetd for the reconciled amount
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertEqual(len(exc_je_for_si), 2)
|
||||
self.assertEqual(len(exc_je_for_pe), 2)
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_pe)
|
||||
|
||||
# There should be no outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Cancel Payment
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 2)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been cancelled
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_pe, [])
|
||||
|
||||
def test_20_journal_against_sales_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||
# Payment
|
||||
je = self.create_journal_entry(
|
||||
acc1=self.debit_usd,
|
||||
acc1_exc_rate=75,
|
||||
acc2=self.cash,
|
||||
acc1_amount=-1,
|
||||
acc2_amount=-75,
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
je.accounts[0].party_type = "Customer"
|
||||
je.accounts[0].party = self.customer
|
||||
je = je.save().submit()
|
||||
|
||||
# Reconcile the remaining amount
|
||||
pr = self.create_payment_reconciliation()
|
||||
# pr.receivable_payable_account = self.debit_usd
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
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)
|
||||
|
||||
# There should be no outstanding in both currencies
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_je = self.get_journals_for(je.doctype, je.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(
|
||||
len(exc_je_for_si), 2
|
||||
) # payment also has reference. so, there are 2 journals referencing invoice
|
||||
self.assertEqual(len(exc_je_for_je), 1)
|
||||
self.assertIn(exc_je_for_je[0], exc_je_for_si)
|
||||
|
||||
# Cancel Payment
|
||||
je.reload()
|
||||
je.cancel()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been cancelled
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_je = self.get_journals_for(je.doctype, je.name)
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_je, [])
|
||||
|
||||
def test_21_advance_journal_against_sales_invoice(self):
|
||||
# Advance Payment
|
||||
adv_exc_rate = 80
|
||||
adv = self.create_journal_entry(
|
||||
acc1=self.debit_usd,
|
||||
acc1_exc_rate=adv_exc_rate,
|
||||
acc2=self.cash,
|
||||
acc1_amount=-1,
|
||||
acc2_amount=adv_exc_rate * -1,
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
adv.accounts[0].party_type = "Customer"
|
||||
adv.accounts[0].party = self.customer
|
||||
adv.accounts[0].is_advance = "Yes"
|
||||
adv = adv.save().submit()
|
||||
adv.reload()
|
||||
|
||||
# Sales Invoices in different exchange rates
|
||||
for exc_rate in [75.9, 83.1]:
|
||||
with self.subTest(exc_rate=exc_rate):
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
|
||||
advances = si.get_advance_entries()
|
||||
self.assertEqual(len(advances), 1)
|
||||
self.assertEqual(advances[0].reference_name, adv.name)
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"doctype": "Sales Invoice Advance",
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"reference_row": advances[0].reference_row,
|
||||
"advance_amount": 1,
|
||||
"allocated_amount": 1,
|
||||
"ref_exchange_rate": advances[0].exchange_rate,
|
||||
"remarks": advances[0].remarks,
|
||||
},
|
||||
)
|
||||
|
||||
si = si.save()
|
||||
si = si.submit()
|
||||
|
||||
# Outstanding in both currencies should be '0'
|
||||
adv.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name]
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
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)
|
||||
|
||||
# Cancel Invoice
|
||||
si.cancel()
|
||||
|
||||
# Exchange 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(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_adv, [])
|
||||
|
||||
def test_22_partial_advance_and_payment_for_invoice_with_cancellation(self):
|
||||
"""
|
||||
Invoice with partial advance payment as Journal, and a normal payment. Then cancel advance and payment.
|
||||
"""
|
||||
# Partial Advance
|
||||
adv_exc_rate = 75
|
||||
adv = self.create_journal_entry(
|
||||
acc1=self.debit_usd,
|
||||
acc1_exc_rate=adv_exc_rate,
|
||||
acc2=self.cash,
|
||||
acc1_amount=-1,
|
||||
acc2_amount=adv_exc_rate * -1,
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
adv.accounts[0].party_type = "Customer"
|
||||
adv.accounts[0].party = self.customer
|
||||
adv.accounts[0].is_advance = "Yes"
|
||||
adv = adv.save().submit()
|
||||
adv.reload()
|
||||
|
||||
# invoice with advance(partial amount)
|
||||
si = self.create_sales_invoice(qty=3, conversion_rate=80, rate=1, do_not_submit=True)
|
||||
advances = si.get_advance_entries()
|
||||
self.assertEqual(len(advances), 1)
|
||||
self.assertEqual(advances[0].reference_name, adv.name)
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"doctype": "Sales Invoice Advance",
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"reference_row": advances[0].reference_row,
|
||||
"advance_amount": 1,
|
||||
"allocated_amount": 1,
|
||||
"ref_exchange_rate": advances[0].exchange_rate,
|
||||
"remarks": advances[0].remarks,
|
||||
},
|
||||
)
|
||||
|
||||
si = si.save()
|
||||
si = si.submit()
|
||||
|
||||
# Outstanding should be there in both currencies
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 2) # account currency
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created for the partial advance
|
||||
exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name]
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
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)
|
||||
|
||||
# Payment
|
||||
adv2_exc_rate = 83
|
||||
pay = self.create_journal_entry(
|
||||
acc1=self.debit_usd,
|
||||
acc1_exc_rate=adv2_exc_rate,
|
||||
acc2=self.cash,
|
||||
acc1_amount=-2,
|
||||
acc2_amount=adv2_exc_rate * -2,
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
pay.accounts[0].party_type = "Customer"
|
||||
pay.accounts[0].party = self.customer
|
||||
pay.accounts[0].is_advance = "Yes"
|
||||
pay = pay.save().submit()
|
||||
pay.reload()
|
||||
|
||||
# Reconcile the remaining amount
|
||||
pr = self.create_payment_reconciliation()
|
||||
# pr.receivable_payable_account = self.debit_usd
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
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)
|
||||
|
||||
# Outstanding should be '0' in both currencies
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created for the payment
|
||||
exc_je_for_si = [
|
||||
x
|
||||
for x in self.get_journals_for(si.doctype, si.name)
|
||||
if x.parent != adv.name and x.parent != pay.name
|
||||
]
|
||||
exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
# There should be 2 JE's now. One for the advance and one for the payment
|
||||
self.assertEqual(len(exc_je_for_si), 2)
|
||||
self.assertEqual(len(exc_je_for_pe), 1)
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
|
||||
|
||||
adv.reload()
|
||||
adv.cancel()
|
||||
|
||||
# Outstanding should be there in both currencies, since advance is cancelled.
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1) # account currency
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
exc_je_for_si = [
|
||||
x
|
||||
for x in self.get_journals_for(si.doctype, si.name)
|
||||
if x.parent != adv.name and x.parent != pay.name
|
||||
]
|
||||
exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name)
|
||||
exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
|
||||
# Exchange Gain/Loss Journal for advance should been cancelled
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_pe), 1)
|
||||
self.assertEqual(exc_je_for_adv, [])
|
||||
|
||||
def test_23_same_journal_split_against_single_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
|
||||
# Payment
|
||||
je = self.create_journal_entry(
|
||||
acc1=self.debit_usd,
|
||||
acc1_exc_rate=75,
|
||||
acc2=self.cash,
|
||||
acc1_amount=-2,
|
||||
acc2_amount=-150,
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
je.accounts[0].party_type = "Customer"
|
||||
je.accounts[0].party = self.customer
|
||||
je = je.save().submit()
|
||||
|
||||
# Reconcile the first half
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
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}))
|
||||
difference_amount = pr.calculate_difference_on_allocation_change(
|
||||
[x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1
|
||||
)
|
||||
pr.allocation[0].allocated_amount = 1
|
||||
pr.allocation[0].difference_amount = difference_amount
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
# There should be outstanding in both currencies
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
|
||||
exc_je_for_je = self.get_journals_for(je.doctype, je.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_je), 1)
|
||||
self.assertIn(exc_je_for_je[0], exc_je_for_si)
|
||||
|
||||
# reconcile remaining half
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
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.allocation[0].allocated_amount = 1
|
||||
pr.allocation[0].difference_amount = difference_amount
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
|
||||
exc_je_for_je = self.get_journals_for(je.doctype, je.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 2)
|
||||
self.assertEqual(len(exc_je_for_je), 2)
|
||||
self.assertIn(exc_je_for_je[0], exc_je_for_si)
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Cancel Payment
|
||||
je.reload()
|
||||
je.cancel()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 2)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been cancelled
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_je = self.get_journals_for(je.doctype, je.name)
|
||||
self.assertEqual(exc_je_for_si, [])
|
||||
self.assertEqual(exc_je_for_je, [])
|
||||
|
||||
def test_30_cr_note_against_sales_invoice(self):
|
||||
"""
|
||||
Reconciling Cr Note against Sales Invoice, both having different exchange rates
|
||||
"""
|
||||
# Invoice in Foreign currency
|
||||
si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
|
||||
|
||||
# Cr Note in Foreign currency of different exchange rate
|
||||
cr_note = self.create_sales_invoice(qty=-2, conversion_rate=75, rate=1, do_not_save=True)
|
||||
cr_note.is_return = 1
|
||||
cr_note.save().submit()
|
||||
|
||||
# Reconcile the first half
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
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}))
|
||||
difference_amount = pr.calculate_difference_on_allocation_change(
|
||||
[x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1
|
||||
)
|
||||
pr.allocation[0].allocated_amount = 1
|
||||
pr.allocation[0].difference_amount = difference_amount
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 2)
|
||||
self.assertEqual(len(exc_je_for_cr), 2)
|
||||
self.assertEqual(exc_je_for_cr, exc_je_for_si)
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
|
||||
cr_note.reload()
|
||||
cr_note.cancel()
|
||||
|
||||
# Exchange Gain/Loss Journal should've been created.
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 1)
|
||||
self.assertEqual(len(exc_je_for_cr), 0)
|
||||
|
||||
# The Credit Note JE is still active and is referencing the sales invoice
|
||||
# So, outstanding stays the same
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 1)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
|
||||
Reference in New Issue
Block a user