Skip to content

Commit c1ba569

Browse files
authored
Merge branch 'develop' into party-import-tool-integrated-with-data-import-tool
2 parents 62be241 + fe09f45 commit c1ba569

146 files changed

Lines changed: 4847 additions & 1469 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

erpnext/accounts/custom/address.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,6 @@ def get_shipping_address(company: str, address: str | None = None):
7171
if address:
7272
address_as_dict = address[0]
7373
name, address_template = get_address_templates(address_as_dict)
74-
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict)
74+
return address_as_dict.get("name"), frappe.render_template(
75+
address_template, address_as_dict, restrict_globals=True
76+
)

erpnext/accounts/doctype/account/account.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,8 @@ def get_company_default_account_fields():
730730
"default_discount_account": "Default Payment Discount Account",
731731
"unrealized_profit_loss_account": "Unrealized Profit / Loss Account",
732732
"exchange_gain_loss_account": "Exchange Gain / Loss Account",
733+
"exchange_gain_account": "Exchange Gain Account",
734+
"exchange_loss_account": "Exchange Loss Account",
733735
"unrealized_exchange_gain_loss_account": "Unrealized Exchange Gain / Loss Account",
734736
"round_off_account": "Round Off Account",
735737
"default_deferred_revenue_account": "Default Deferred Revenue Account",

erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@
179179
},
180180
"Impairment": {
181181
"account_category": "Operating Expenses"
182+
},
183+
"Exchange Loss": {
184+
"account_category": "Operating Expenses"
182185
}
183186
},
184187
"root_type": "Expense"
@@ -196,6 +199,10 @@
196199
"account_type": "Income Account"
197200
},
198201
"Indirect Income": {
202+
"Exchange Gain": {
203+
"account_type": "Income Account",
204+
"account_category": "Other Operating Income"
205+
},
199206
"account_type": "Income Account",
200207
"is_group": 1
201208
},

erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ def get():
138138
_("Gain/Loss on Asset Disposal"): {"account_category": "Other Operating Income"},
139139
_("Impairment"): {"account_category": "Operating Expenses"},
140140
_("Tax Expense"): {"account_category": "Tax Expense"},
141+
_("Exchange Loss"): {"account_category": "Operating Expenses"},
141142
},
142143
"root_type": "Expense",
143144
},
@@ -149,6 +150,7 @@ def get():
149150
_("Indirect Income"): {
150151
_("Interest Income"): {"account_category": "Investment Income"},
151152
_("Interest on Fixed Deposits"): {"account_category": "Investment Income"},
153+
_("Exchange Gain"): {"account_category": "Other Operating Income"},
152154
"is_group": 1,
153155
},
154156
"root_type": "Income",

erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ def get():
233233
},
234234
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
235235
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
236+
_("Exchange Loss"): {"account_number": "5226", "account_category": "Operating Expenses"},
236237
"account_number": "5200",
237238
},
238239
"root_type": "Expense",
@@ -250,6 +251,10 @@ def get():
250251
"account_number": "4220",
251252
"account_category": "Investment Income",
252253
},
254+
_("Exchange Gain"): {
255+
"account_number": "4230",
256+
"account_category": "Other Operating Income",
257+
},
253258
"is_group": 1,
254259
"account_number": "4200",
255260
},

erpnext/accounts/doctype/journal_entry/services/reference_validator.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def _validate_invoices(self) -> None:
184184
continue
185185
invoice = frappe.get_doc(reference_type, reference_name)
186186
self._validate_invoice_outstanding(invoice, total, reference_type, reference_name)
187+
self._validate_block_invoice(invoice)
187188

188189
def _validate_invoice_outstanding(self, invoice, total, reference_type, reference_name) -> None:
189190
"""Payment booked against an invoice cannot exceed its outstanding amount."""
@@ -197,3 +198,15 @@ def _validate_invoice_outstanding(self, invoice, total, reference_type, referenc
197198
reference_type, reference_name, invoice.outstanding_amount
198199
)
199200
)
201+
202+
def _validate_block_invoice(self, invoice):
203+
"""Payment cannnot be booked against blocked Purchase Invoices"""
204+
if invoice.doctype != "Purchase Invoice":
205+
return
206+
207+
if invoice.invoice_is_blocked():
208+
frappe.throw(
209+
_("{0} {1} is blocked and on hold until {2}.").format(
210+
invoice.doctype, invoice.name, invoice.release_date
211+
)
212+
)

erpnext/accounts/doctype/journal_entry/test_journal_entry.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# License: GNU General Public License v3. See license.txt
33

44
import frappe
5-
from frappe.utils import flt, nowdate
5+
from frappe.utils import add_days, flt, nowdate
66

77
from erpnext.accounts.doctype.account.test_account import get_inventory_account
88
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
@@ -748,6 +748,69 @@ def test_validate_reference_doc_populates_reference_side_effects(self):
748748
self.assertEqual(jv.reference_types[invoice.name], "Sales Invoice")
749749
self.assertEqual(jv.reference_accounts[invoice.name], "Debtors - _TC")
750750

751+
def make_jv_against_purchase_invoice(self, invoice, amount=100):
752+
jv = make_journal_entry("Creditors - _TC", "_Test Cash - _TC", amount, save=False)
753+
jv.accounts[0].party_type = "Supplier"
754+
jv.accounts[0].party = invoice.supplier
755+
jv.accounts[0].reference_type = "Purchase Invoice"
756+
jv.accounts[0].reference_name = invoice.name
757+
return jv
758+
759+
def test_jv_against_purchase_invoice_respects_hold_state(self):
760+
"""Payment can be booked against a Purchase Invoice only while it is not on hold."""
761+
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
762+
763+
release_date = add_days(nowdate(), 10)
764+
765+
def never_held():
766+
return make_purchase_invoice()
767+
768+
def held_until_a_future_date():
769+
invoice = make_purchase_invoice()
770+
invoice.block_invoice(hold_comment="Waiting for the goods", release_date=release_date)
771+
return invoice
772+
773+
def held_without_a_release_date():
774+
invoice = make_purchase_invoice()
775+
invoice.block_invoice(hold_comment="Under dispute")
776+
return invoice
777+
778+
def held_until_a_date_that_has_passed():
779+
invoice = held_until_a_future_date()
780+
frappe.db.set_value("Purchase Invoice", invoice.name, "release_date", add_days(nowdate(), -1))
781+
return invoice
782+
783+
def unblocked_again():
784+
invoice = held_until_a_future_date()
785+
invoice.unblock_invoice()
786+
return invoice
787+
788+
for build_invoice in (held_until_a_future_date, held_without_a_release_date):
789+
with self.subTest(build_invoice.__name__):
790+
jv = self.make_jv_against_purchase_invoice(build_invoice())
791+
self.assertRaisesRegex(frappe.ValidationError, "is blocked and on hold until", jv.insert)
792+
793+
for build_invoice in (never_held, held_until_a_date_that_has_passed, unblocked_again):
794+
with self.subTest(build_invoice.__name__):
795+
invoice = build_invoice()
796+
jv = self.make_jv_against_purchase_invoice(invoice)
797+
jv.insert()
798+
self.assertEqual(jv.reference_types[invoice.name], "Purchase Invoice")
799+
800+
def test_jv_against_blocked_sales_invoice_reference_is_not_checked(self):
801+
"""A Sales Invoice has no hold state, so the check must skip it rather than fail."""
802+
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
803+
804+
invoice = create_sales_invoice(rate=500)
805+
jv = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 100, save=False)
806+
jv.accounts[1].party_type = "Customer"
807+
jv.accounts[1].party = "_Test Customer"
808+
jv.accounts[1].reference_type = "Sales Invoice"
809+
jv.accounts[1].reference_name = invoice.name
810+
jv.insert()
811+
812+
self.assertEqual(jv.reference_types[invoice.name], "Sales Invoice")
813+
751814
def test_get_balance_places_difference_on_blank_row(self):
752815
"""Characterize: get_balance puts the unbalanced difference on an amountless row."""
753816
jv = frappe.new_doc("Journal Entry")

erpnext/accounts/doctype/payment_entry/test_payment_entry.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,61 @@ def test_payment_entry_exchange_gain_loss(self):
950950
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
951951
self.assertEqual(outstanding_amount, 0)
952952

953+
def test_exchange_gain_loss_split_accounts(self):
954+
gain_account = create_account(
955+
account_name="_Test Exchange Gain",
956+
parent_account="Indirect Expenses - _TC",
957+
company="_Test Company",
958+
)
959+
loss_account = create_account(
960+
account_name="_Test Exchange Loss",
961+
parent_account="Indirect Expenses - _TC",
962+
company="_Test Company",
963+
)
964+
frappe.db.set_value("Company", "_Test Company", "exchange_gain_account", gain_account)
965+
frappe.db.set_value("Company", "_Test Company", "exchange_loss_account", loss_account)
966+
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "exchange_gain_account", "")
967+
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "exchange_loss_account", "")
968+
969+
si_gain = create_sales_invoice(
970+
customer="_Test Customer USD",
971+
debit_to="_Test Receivable USD - _TC",
972+
currency="USD",
973+
conversion_rate=50,
974+
)
975+
pe_gain = get_payment_entry("Sales Invoice", si_gain.name, bank_account="_Test Bank USD - _TC")
976+
pe_gain.reference_no = "1"
977+
pe_gain.reference_date = "2016-01-01"
978+
pe_gain.source_exchange_rate = 55
979+
pe_gain.save()
980+
self.assertEqual(pe_gain.references[0].exchange_gain_loss, 500)
981+
pe_gain.submit()
982+
983+
self.assertEqual(self.get_gain_loss_journal_account(pe_gain.name), gain_account)
984+
985+
si_loss = create_sales_invoice(
986+
customer="_Test Customer USD",
987+
debit_to="_Test Receivable USD - _TC",
988+
currency="USD",
989+
conversion_rate=55,
990+
)
991+
pe_loss = get_payment_entry("Sales Invoice", si_loss.name, bank_account="_Test Bank USD - _TC")
992+
pe_loss.reference_no = "2"
993+
pe_loss.reference_date = "2016-01-01"
994+
pe_loss.source_exchange_rate = 50
995+
pe_loss.save()
996+
self.assertEqual(pe_loss.references[0].exchange_gain_loss, -500)
997+
pe_loss.submit()
998+
999+
self.assertEqual(self.get_gain_loss_journal_account(pe_loss.name), loss_account)
1000+
1001+
def get_gain_loss_journal_account(self, payment_entry_name: str) -> str | None:
1002+
return frappe.db.get_value(
1003+
"Journal Entry Account",
1004+
{"reference_type": "Payment Entry", "reference_name": payment_entry_name, "docstatus": 1},
1005+
"account",
1006+
)
1007+
9531008
def test_payment_entry_against_sales_invoice_with_cost_centre(self):
9541009
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
9551010

erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
is_any_doc_running,
1919
)
2020
from erpnext.accounts.services.advances import get_advance_payment_entries_for_regional
21+
from erpnext.accounts.services.exchange_gain_loss import get_exchange_gain_loss_account
2122
from erpnext.accounts.utils import (
2223
QueryPaymentLedger,
2324
create_gain_loss_journal,
@@ -485,9 +486,6 @@ def allocate_entries(self, args: dict):
485486
"Accounts Settings", "exchange_gain_loss_posting_date", cache=True
486487
)
487488
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
488-
default_exchange_gain_loss_account = frappe.get_cached_value(
489-
"Company", self.company, "exchange_gain_loss_account"
490-
)
491489

492490
entries = []
493491
for pay in args.get("payments"):
@@ -507,7 +505,10 @@ def allocate_entries(self, args: dict):
507505
pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name"))
508506

509507
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
510-
res.difference_account = default_exchange_gain_loss_account
508+
is_gain = (
509+
res.difference_amount > 0 if self.party_type == "Customer" else res.difference_amount < 0
510+
)
511+
res.difference_account = get_exchange_gain_loss_account(self.company, is_gain)
511512
res.exchange_rate = inv.get("exchange_rate")
512513
res.update({"gain_loss_posting_date": pay.get("posting_date")})
513514
if not pay.get("is_advance"):

0 commit comments

Comments
 (0)