mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-06 23:17:57 +00:00
Compare commits
209 Commits
mergify/bp
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16f13c75de | ||
|
|
609164fb9a | ||
|
|
993ba4cf45 | ||
|
|
bb081e46d7 | ||
|
|
6985f0efc3 | ||
|
|
302ff49b7f | ||
|
|
ee2b65806b | ||
|
|
ba459204b0 | ||
|
|
7d4785784b | ||
|
|
35b503932d | ||
|
|
736a776d3d | ||
|
|
11b9b1adc5 | ||
|
|
9f6bc7fe49 | ||
|
|
7e63f1d220 | ||
|
|
3d8502f408 | ||
|
|
c3e869c701 | ||
|
|
aee03417de | ||
|
|
14d8b87c8e | ||
|
|
2a86a1fb98 | ||
|
|
28180ccaa4 | ||
|
|
7bf17372b0 | ||
|
|
a5b881ea74 | ||
|
|
ac40b46a6d | ||
|
|
69682cb064 | ||
|
|
8b543e5503 | ||
|
|
c4f90c3b34 | ||
|
|
244dce5098 | ||
|
|
194ab87fef | ||
|
|
e0299e1cbd | ||
|
|
9c970acbda | ||
|
|
06dde659c2 | ||
|
|
9fc17e0e3a | ||
|
|
535f8657ed | ||
|
|
c77781a14f | ||
|
|
c9d22386ed | ||
|
|
2e7c3207c4 | ||
|
|
0aef591f5d | ||
|
|
39049948b8 | ||
|
|
39b9d798d9 | ||
|
|
bebb8ae1ea | ||
|
|
1c8266af39 | ||
|
|
73729f6ab0 | ||
|
|
a287201011 | ||
|
|
a4e2fbdcf9 | ||
|
|
21dc0a0b1a | ||
|
|
98626aaa6c | ||
|
|
86a8015cea | ||
|
|
8330b349d2 | ||
|
|
29197af11a | ||
|
|
a00a3868ed | ||
|
|
335dcc976c | ||
|
|
6972f161b8 | ||
|
|
79b8505972 | ||
|
|
fe7b797e5f | ||
|
|
df329964dd | ||
|
|
1845d12951 | ||
|
|
67c2ab4c9f | ||
|
|
405e1ab6d3 | ||
|
|
cf066edd7f | ||
|
|
96e2e356b6 | ||
|
|
0fef2d4b02 | ||
|
|
b265b82f0b | ||
|
|
c92a06d77d | ||
|
|
c0236191aa | ||
|
|
5f083d55b5 | ||
|
|
56da3bd2e4 | ||
|
|
f27077a45f | ||
|
|
38b51df17e | ||
|
|
f145e6267b | ||
|
|
09d9c0ddd3 | ||
|
|
e4b5507446 | ||
|
|
a028d856bc | ||
|
|
e3e4680ad2 | ||
|
|
276406bc1c | ||
|
|
156e46ccb0 | ||
|
|
07a5aba2aa | ||
|
|
4ce8d9af6a | ||
|
|
0474b8595b | ||
|
|
ca8677a0ff | ||
|
|
03e5467ba2 | ||
|
|
e960f8217b | ||
|
|
a81e807a70 | ||
|
|
3b4ee30dd7 | ||
|
|
521ebc25aa | ||
|
|
1291df9a63 | ||
|
|
70d7ceb2f2 | ||
|
|
512419eee7 | ||
|
|
c6be380e83 | ||
|
|
200496254b | ||
|
|
2596ef202b | ||
|
|
ac71969512 | ||
|
|
734a7b8be9 | ||
|
|
aa9c4555fd | ||
|
|
7e5be50997 | ||
|
|
03e3a693ff | ||
|
|
d4ae2f89b2 | ||
|
|
17c24a4168 | ||
|
|
c41824c4d0 | ||
|
|
3ad611966e | ||
|
|
ddc97df31a | ||
|
|
c21ebafaa5 | ||
|
|
17276a2c0c | ||
|
|
4ecdd1fd0e | ||
|
|
7e093d08a9 | ||
|
|
955e8714ee | ||
|
|
1a262483a4 | ||
|
|
eead27560c | ||
|
|
b658330881 | ||
|
|
4f968f5c65 | ||
|
|
838183941a | ||
|
|
92d86eb30b | ||
|
|
efeda90cad | ||
|
|
47d4319f83 | ||
|
|
eb5a9db749 | ||
|
|
a2ccb5fa87 | ||
|
|
b56dbe98cf | ||
|
|
cf5a2d6351 | ||
|
|
564de01463 | ||
|
|
ac8637d5a0 | ||
|
|
7c10775bc8 | ||
|
|
49a96f4306 | ||
|
|
161ed5290e | ||
|
|
a0d3b931f3 | ||
|
|
11b82ba008 | ||
|
|
1b73170e8c | ||
|
|
59c46a1789 | ||
|
|
24e0e3505d | ||
|
|
5824b5effd | ||
|
|
fba16efc07 | ||
|
|
9aeb21d0c8 | ||
|
|
000135a3d4 | ||
|
|
260574719e | ||
|
|
ead1ce2742 | ||
|
|
991413608b | ||
|
|
fe74e0888b | ||
|
|
ceff8c92fd | ||
|
|
d8babf66ae | ||
|
|
8dbbcf5ffb | ||
|
|
fab9c4d7df | ||
|
|
2383051b74 | ||
|
|
d2d3294f02 | ||
|
|
62037301ee | ||
|
|
b860f3d31d | ||
|
|
b8c72c05bb | ||
|
|
f1eda7c4ec | ||
|
|
83eafe118e | ||
|
|
0b178b9449 | ||
|
|
1e7f374d6e | ||
|
|
fcb86023cb | ||
|
|
2dba591d37 | ||
|
|
385a2beaf9 | ||
|
|
cc2cc812cc | ||
|
|
a1c3c60fca | ||
|
|
bbbd693c1c | ||
|
|
f42f59a6b2 | ||
|
|
886cec797c | ||
|
|
2ea2c5c11e | ||
|
|
b0f59ebf79 | ||
|
|
35fee187b6 | ||
|
|
3e2fb85ae6 | ||
|
|
0cb5b571b0 | ||
|
|
3131cf335e | ||
|
|
785845a425 | ||
|
|
fa3ee91414 | ||
|
|
f8fa8bdda7 | ||
|
|
69d509a098 | ||
|
|
5a9fb3db1f | ||
|
|
8c8bfd4277 | ||
|
|
47f0507643 | ||
|
|
cb68c784fe | ||
|
|
b1d3d39a11 | ||
|
|
31d9fc5367 | ||
|
|
80a38732f9 | ||
|
|
c228d1a05a | ||
|
|
6bbba727a5 | ||
|
|
0149bc633c | ||
|
|
574198bceb | ||
|
|
edd3383f7d | ||
|
|
2384b37305 | ||
|
|
c71dd00cc3 | ||
|
|
bd18ce7326 | ||
|
|
9a85e1a811 | ||
|
|
ad1b77f280 | ||
|
|
99d89b207e | ||
|
|
66f60c64bd | ||
|
|
9f8abd585a | ||
|
|
1f937a7c76 | ||
|
|
9234e27a70 | ||
|
|
6ba476a3cd | ||
|
|
5f4c1f331d | ||
|
|
0ad348d714 | ||
|
|
7f1240e2eb | ||
|
|
6806c5e977 | ||
|
|
4dd428de41 | ||
|
|
77a9cf6398 | ||
|
|
c45ea53889 | ||
|
|
cab262c147 | ||
|
|
00fd1d2f26 | ||
|
|
7f55f421ab | ||
|
|
781c377588 | ||
|
|
7318c6007d | ||
|
|
e8288a2f63 | ||
|
|
afb067ce50 | ||
|
|
e36cc5641c | ||
|
|
a98eb60a27 | ||
|
|
49bb095152 | ||
|
|
4d3ddeae8d | ||
|
|
e5affb16c7 | ||
|
|
770d6dd8e2 |
@@ -10,8 +10,10 @@ from frappe.contacts.doctype.address.address import (
|
||||
class ERPNextAddress(Address):
|
||||
def validate(self):
|
||||
self.validate_reference()
|
||||
self.update_compnay_address()
|
||||
super().validate()
|
||||
self.update_company_address()
|
||||
|
||||
if hasattr(super(), "validate"):
|
||||
super().validate()
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
@@ -20,7 +22,7 @@ class ERPNextAddress(Address):
|
||||
|
||||
return super().link_address()
|
||||
|
||||
def update_compnay_address(self):
|
||||
def update_company_address(self):
|
||||
for link in self.get("links"):
|
||||
if link.link_doctype == "Company":
|
||||
self.is_your_company_address = 1
|
||||
@@ -38,6 +40,10 @@ class ERPNextAddress(Address):
|
||||
"""
|
||||
After Address is updated, update the related 'Primary Address' on Customer.
|
||||
"""
|
||||
|
||||
if hasattr(super(), "on_update"):
|
||||
super().on_update()
|
||||
|
||||
address_display = get_address_display(self.as_dict())
|
||||
filters = {"customer_primary_address": self.name}
|
||||
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"reporting_currency_exchange_rate",
|
||||
"debit_in_reporting_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"account_currency",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
@@ -124,12 +127,30 @@
|
||||
"fieldname": "is_period_closing_voucher_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Period Closing Voucher Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:56.710541",
|
||||
"modified": "2025-08-22 19:13:50.400404",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Closing Balance",
|
||||
@@ -158,7 +179,8 @@
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import cint, cstr, flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.exceptions import ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class AccountClosingBalance(Document):
|
||||
@@ -26,12 +29,15 @@ class AccountClosingBalance(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
finance_book: DF.Link | None
|
||||
is_period_closing_voucher_entry: DF.Check
|
||||
period_closing_voucher: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -55,6 +61,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
"closing_date": closing_date,
|
||||
}
|
||||
)
|
||||
set_amount_in_reporting_currency(cle, company, closing_date)
|
||||
cle.flags.ignore_permissions = True
|
||||
cle.flags.ignore_links = True
|
||||
cle.submit()
|
||||
@@ -144,3 +151,29 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
||||
entries = query.run(as_dict=1)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def set_amount_in_reporting_currency(cle, company, closing_date):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
|
||||
reporting_currency_exchange_rate = get_exchange_rate(default_currency, reporting_currency, closing_date)
|
||||
if not reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, closing_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
debit_in_reporting_currency = flt(cle.get("debit", 0) * reporting_currency_exchange_rate)
|
||||
credit_in_reporting_currency = flt(cle.get("credit", 0) * reporting_currency_exchange_rate)
|
||||
|
||||
cle.update(
|
||||
{
|
||||
"reporting_currency_exchange_rate": reporting_currency_exchange_rate,
|
||||
"debit_in_reporting_currency": debit_in_reporting_currency,
|
||||
"credit_in_reporting_currency": credit_in_reporting_currency,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
@@ -95,6 +96,13 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "IBAN",
|
||||
"length": 30
|
||||
"length": 34,
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@@ -208,6 +209,7 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
@@ -250,7 +252,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -282,9 +284,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "bank,account",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ class BankAccount(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
@@ -72,35 +71,6 @@ class BankAccount(Document):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_iban(self):
|
||||
"""
|
||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||
"""
|
||||
# IBAN field is optional
|
||||
if not self.iban:
|
||||
return
|
||||
|
||||
def encode_char(c):
|
||||
# Position in the alphabet (A=1, B=2, ...) plus nine
|
||||
return str(9 + ord(c) - 64)
|
||||
|
||||
# remove whitespaces, upper case to get the right number from ord()
|
||||
iban = "".join(self.iban.split(" ")).upper()
|
||||
|
||||
# Move country code and checksum from the start to the end
|
||||
flipped = iban[4:] + iban[:4]
|
||||
|
||||
# Encode characters as numbers
|
||||
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||
|
||||
try:
|
||||
to_check = int("".join(encoded))
|
||||
except ValueError:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -8,38 +8,4 @@ from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankAccount(IntegrationTestCase):
|
||||
def test_validate_iban(self):
|
||||
valid_ibans = [
|
||||
"GB82 WEST 1234 5698 7654 32",
|
||||
"DE91 1000 0000 0123 4567 89",
|
||||
"FR76 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
invalid_ibans = [
|
||||
# wrong checksum (3rd place)
|
||||
"GB72 WEST 1234 5698 7654 32",
|
||||
"DE81 1000 0000 0123 4567 89",
|
||||
"FR66 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
bank_account = frappe.get_doc({"doctype": "Bank Account"})
|
||||
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except AttributeError:
|
||||
msg = "BankAccount.validate_iban() failed for empty IBAN"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for iban in valid_ibans:
|
||||
bank_account.iban = iban
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
pass
|
||||
|
||||
@@ -155,8 +155,10 @@ def get_payment_entries_for_bank_clearance(
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
@@ -181,19 +183,20 @@ def get_payment_entries_for_bank_clearance(
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry` as pe
|
||||
join `tabCompany` c on c.name = pe.company
|
||||
where
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
||||
{condition}
|
||||
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||
{pe_condition}
|
||||
order by
|
||||
posting_date ASC, name DESC
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN",
|
||||
"options": "IBAN",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -214,9 +215,10 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:37.731207",
|
||||
"modified": "2025-08-29 11:52:33.550847",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
@@ -250,9 +252,10 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "customer",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "customer"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,18 @@ class BankStatementImport(DataImport):
|
||||
self.validate_google_sheets_url()
|
||||
|
||||
def start_import(self):
|
||||
"""
|
||||
Start a background import job for this Bank Statement Import.
|
||||
|
||||
Validates that the preview contains a "Bank Account" column and that the scheduler is active (unless running in test or developer mode). If validation passes and there is not already an enqueued job for this document, enqueue a background worker to perform the import.
|
||||
|
||||
Returns:
|
||||
str | None: The enqueued job_id when a new job was queued, otherwise None.
|
||||
|
||||
Raises:
|
||||
frappe.ValidationError: If the preview is missing a "Bank Account" column.
|
||||
frappe.ValidationError: If the scheduler is inactive and import is not allowed to run immediately.
|
||||
"""
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
@@ -111,20 +123,94 @@ class BankStatementImport(DataImport):
|
||||
return None
|
||||
|
||||
|
||||
def preprocess_mt940_content(content: str) -> str:
|
||||
"""
|
||||
Truncate overly long MT940 statement numbers found in `:28C:` tags to the last 5 digits.
|
||||
|
||||
This function fixes MT940 files where banks supply statement numbers longer than the MT940-expected maximum (5 digits),
|
||||
which can break parsers. It only processes lines that start with the `:28C:` tag and:
|
||||
- leaves content unchanged if no `:28C:` tag is present,
|
||||
- truncates numeric statement numbers longer than 5 digits to their last 5 digits,
|
||||
- preserves any `/sequence` suffix and trailing whitespace on the same line.
|
||||
|
||||
Parameters:
|
||||
content (str): Raw MT940 file content.
|
||||
|
||||
Returns:
|
||||
str: The processed content with corrected `:28C:` statement numbers.
|
||||
"""
|
||||
# Fast-path: bail if no :28C: tag exists
|
||||
if ":28C:" not in content:
|
||||
return content
|
||||
|
||||
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
|
||||
pattern = re.compile(r'(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$')
|
||||
|
||||
def replace_statement_number(match):
|
||||
"""
|
||||
Replace a matched MT940 :28C: statement number by truncating it to the last five digits if it is longer.
|
||||
|
||||
Parameters:
|
||||
match (re.Match): A regex match with groups:
|
||||
1: prefix (e.g., ':28C:')
|
||||
2: numeric statement number
|
||||
3: optional sequence part (e.g., '/1')
|
||||
4: optional trailing whitespace
|
||||
|
||||
Returns:
|
||||
str: Reconstructed replacement string preserving prefix, (possibly truncated) statement number, sequence part, and trailing whitespace.
|
||||
"""
|
||||
prefix = match.group(1) # ':28C:'
|
||||
statement_num = match.group(2) # The statement number
|
||||
sequence_part = match.group(3) or '' # The sequence part like '/1'
|
||||
trailing_space = match.group(4) or '' # Preserve trailing whitespace
|
||||
|
||||
# If statement number is longer than 5 digits, truncate to last 5 digits
|
||||
if len(statement_num) > 5:
|
||||
statement_num = statement_num[-5:]
|
||||
|
||||
return prefix + statement_num + sequence_part + trailing_space
|
||||
|
||||
# Apply the replacement
|
||||
processed_content = pattern.sub(replace_statement_number, content)
|
||||
return processed_content
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import, mt940_file_path):
|
||||
"""
|
||||
Convert an MT940 file to a CSV and save it to the Frappe File Manager, returning the saved file URL.
|
||||
|
||||
This function:
|
||||
- Loads the specified MT940 file, verifies it is MT940 format, preprocesses content to fix statement number formatting, and parses transactions.
|
||||
- Writes parsed transactions to an in-memory CSV with headers: Date, Deposit, Withdrawal, Description, Reference Number, Bank Account, Currency.
|
||||
- Saves the CSV as a private attachment on the Bank Statement Import document and returns the file URL.
|
||||
|
||||
Parameters:
|
||||
data_import (str): Name (docname) of the Bank Statement Import document to attach the converted CSV to.
|
||||
mt940_file_path (str): File path or file identifier pointing to the uploaded MT940 file to convert.
|
||||
|
||||
Returns:
|
||||
str: URL of the saved CSV file in the File Manager.
|
||||
|
||||
Raises:
|
||||
frappe.ValidationError: If the file is not MT940, MT940 import is not enabled on the document, parsing fails, or no transactions are found.
|
||||
"""
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
|
||||
file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
if not is_mt940_format(content):
|
||||
is_mt940 = is_mt940_format(content)
|
||||
if not is_mt940:
|
||||
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
|
||||
|
||||
if is_mt940_format(content) and not doc.import_mt940_fromat:
|
||||
if is_mt940 and not doc.import_mt940_fromat:
|
||||
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
|
||||
|
||||
try:
|
||||
transactions = mt940.parse(content)
|
||||
# Preprocess MT940 content to fix statement number format issues
|
||||
processed_content = preprocess_mt940_content(content)
|
||||
transactions = mt940.parse(processed_content)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
|
||||
|
||||
@@ -249,6 +335,20 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
|
||||
|
||||
def update_mapping_db(bank, template_options):
|
||||
"""
|
||||
Update a Bank document's transaction field mappings to match the provided template options.
|
||||
|
||||
This replaces all existing entries in the Bank.bank_transaction_mapping child table with mappings from
|
||||
the JSON-encoded template_options. The expected template_options JSON contains a "column_to_field_map"
|
||||
object mapping file column names (keys) to bank transaction field names (values).
|
||||
|
||||
Parameters:
|
||||
bank (str | frappe.model.document.Document): Bank name/docname or a Bank document.
|
||||
template_options (str): JSON string containing a "column_to_field_map" mapping of file column -> bank field.
|
||||
|
||||
Side effects:
|
||||
Overwrites the Bank.bank_transaction_mapping entries and saves the Bank document.
|
||||
"""
|
||||
bank = frappe.get_doc("Bank", bank)
|
||||
for d in bank.bank_transaction_mapping:
|
||||
d.delete()
|
||||
@@ -260,6 +360,17 @@ def update_mapping_db(bank, template_options):
|
||||
|
||||
|
||||
def add_bank_account(data, bank_account):
|
||||
"""
|
||||
Ensure every data row contains the given bank account value.
|
||||
|
||||
Assumes `data` is a list of rows where data[0] is the header row. If the header row does not contain "Bank Account",
|
||||
this function appends that header and appends the `bank_account` value to each subsequent row. If the header exists,
|
||||
it sets the `bank_account` value into the existing "Bank Account" column for every data row. Mutates `data` in place.
|
||||
|
||||
Parameters:
|
||||
data (list[list]): Table-like data with the first row as headers.
|
||||
bank_account (str): Bank account value to set for each data row.
|
||||
"""
|
||||
bank_account_loc = None
|
||||
if "Bank Account" not in data[0]:
|
||||
data[0].append("Bank Account")
|
||||
@@ -276,6 +387,21 @@ def add_bank_account(data, bank_account):
|
||||
|
||||
|
||||
def write_files(import_file, data):
|
||||
"""
|
||||
Write processed tabular data back to the original import file path (CSV or Excel).
|
||||
|
||||
This function overwrites the file referenced by import_file.file_doc.get_full_path().
|
||||
- If the file extension is "csv", writes rows using the csv writer (expects `data` as an iterable of row iterables).
|
||||
- If the extension is "xlsx" or "xls", writes to an Excel workbook using write_xlsx with sheet name "trans".
|
||||
|
||||
Parameters:
|
||||
import_file: object
|
||||
File wrapper whose `.file_doc.get_full_path()` and `.file_doc.get_extension()` are used to determine the target path and extension.
|
||||
data: Iterable[Iterable]
|
||||
Sequence of rows (each row is an iterable of cell values) to be written.
|
||||
|
||||
No return value.
|
||||
"""
|
||||
full_file_path = import_file.file_doc.get_full_path()
|
||||
parts = import_file.file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
@@ -285,11 +411,26 @@ def write_files(import_file, data):
|
||||
with open(full_file_path, "w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(data)
|
||||
elif extension == "xlsx" or "xls":
|
||||
elif extension in ("xlsx", "xls"):
|
||||
write_xlsx(data, "trans", file_path=full_file_path)
|
||||
|
||||
|
||||
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
"""
|
||||
Write rows of data to an Excel worksheet and save the workbook.
|
||||
|
||||
Creates a sheet named `sheet_name` in the provided openpyxl workbook (or a new write-only workbook if `wb` is None), applies optional column widths, converts HTML in string cells (except for sheets named "Data Import Template" or "Data Export"), strips characters illegal in Excel, and saves the workbook to `file_path`.
|
||||
|
||||
Parameters:
|
||||
data (Iterable[Sequence]): Iterable of rows, where each row is a sequence of cell values.
|
||||
sheet_name (str): Name of the worksheet to create.
|
||||
wb (openpyxl.Workbook, optional): Workbook to append the sheet to. If not provided, a new write-only Workbook is created.
|
||||
column_widths (Sequence[Number], optional): Sequence of column widths; indexes correspond to columns starting at 1.
|
||||
file_path (str): File path where the workbook will be saved.
|
||||
|
||||
Returns:
|
||||
bool: True on successful save.
|
||||
"""
|
||||
# from xlsx utils with changes
|
||||
column_widths = column_widths or []
|
||||
if wb is None:
|
||||
|
||||
@@ -1,10 +1,220 @@
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
# import frappe
|
||||
|
||||
import unittest
|
||||
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
|
||||
preprocess_mt940_content,
|
||||
is_mt940_format,
|
||||
)
|
||||
|
||||
|
||||
class TestBankStatementImport(IntegrationTestCase):
|
||||
pass
|
||||
class TestBankStatementImport(unittest.TestCase):
|
||||
"""Unit tests for Bank Statement Import functions"""
|
||||
|
||||
def test_preprocess_mt940_content_with_long_statement_number(self):
|
||||
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
|
||||
# Test case with 6-digit statement number (167619 -> 67619)
|
||||
mt940_content = ":28C:167619/1"
|
||||
expected_content = ":28C:67619/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_with_normal_statement_number(self):
|
||||
"""Test that statement numbers with 5 or fewer digits are unchanged"""
|
||||
# Test case with 5-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
# Test case with 4-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:1234/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_without_sequence_number(self):
|
||||
"""Test statement number truncation without sequence number"""
|
||||
# Test case with long statement number but no sequence (no /1)
|
||||
mt940_content = ":28C:987654321"
|
||||
expected_content = ":28C:54321"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_multiple_occurrences(self):
|
||||
"""Test multiple statement numbers in the same content"""
|
||||
mt940_content = """:28C:167619/1
|
||||
:28C:987654/2"""
|
||||
expected_content = """:28C:67619/1
|
||||
:28C:87654/2"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_edge_cases(self):
|
||||
"""Test edge cases like empty content and content without :28C: tags"""
|
||||
# Test empty content
|
||||
self.assertEqual(preprocess_mt940_content(""), "")
|
||||
|
||||
# Test content without :28C: tags
|
||||
content_without_28c = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
result = preprocess_mt940_content(content_without_28c)
|
||||
self.assertEqual(result, content_without_28c) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_with_full_mt940_document(self):
|
||||
"""Test preprocessing with complete MT940 document"""
|
||||
mt940_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
expected_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:67619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_is_mt940_format_detection(self):
|
||||
"""Test MT940 format detection function"""
|
||||
# Valid MT940 content with all required tags
|
||||
valid_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
|
||||
self.assertTrue(is_mt940_format(valid_mt940))
|
||||
|
||||
# Invalid MT940 content (CSV format)
|
||||
invalid_mt940 = """Date,Description,Amount
|
||||
2023-01-01,Test Transaction,100.00
|
||||
2023-01-02,Another Transaction,-50.00"""
|
||||
self.assertFalse(is_mt940_format(invalid_mt940))
|
||||
|
||||
# Partially valid MT940 (missing some required tags)
|
||||
partial_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
self.assertFalse(is_mt940_format(partial_mt940))
|
||||
|
||||
# Empty content
|
||||
self.assertFalse(is_mt940_format(""))
|
||||
|
||||
def test_preprocess_mt940_content_boundary_conditions(self):
|
||||
"""
|
||||
Verify preprocessing handles statement-number length boundaries in `:28C:` tags.
|
||||
|
||||
Checks that:
|
||||
- A 6-digit statement number is truncated to its last 5 digits.
|
||||
- A 5-digit statement number remains unchanged.
|
||||
- A very long statement number is reduced to its last 5 digits.
|
||||
"""
|
||||
# Test exactly 6 digits (should be truncated)
|
||||
mt940_content = ":28C:123456/1"
|
||||
expected_content = ":28C:23456/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test exactly 5 digits (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content)
|
||||
|
||||
# Test very long statement number
|
||||
mt940_content = ":28C:123456789012345/1"
|
||||
expected_content = ":28C:12345/1" # Last 5 digits
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_real_world_case(self):
|
||||
"""
|
||||
Verify preprocessing of a real-world MT940 document: truncate 6-digit `:28C:` statement numbers to their last 5 digits and preserve all other content.
|
||||
|
||||
Uses a sanitized, production-failing MT940 sample where `:28C:167619/1` must become `:28C:67619/1`. Asserts the entire document matches the expected transformed output, that the truncated tag is present and the original is absent, and that unrelated fields (e.g., `:20:` reference and UPI details) remain unchanged.
|
||||
"""
|
||||
# This is based on actual MT940 content that was causing parsing errors (sanitized)
|
||||
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:167619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
# Expected result with statement number 167619 truncated to 67619
|
||||
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:67619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Verify that the problematic statement number was actually changed
|
||||
self.assertIn(":28C:67619/1", result)
|
||||
self.assertNotIn(":28C:167619/1", result)
|
||||
|
||||
# Verify that other content remains unchanged
|
||||
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
|
||||
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
|
||||
|
||||
def test_preprocess_mt940_content_whitespace_variants(self):
|
||||
"""Test handling of whitespace and different line endings"""
|
||||
# Test with trailing spaces
|
||||
mt940_content = ":28C:167619/1 \n"
|
||||
expected_content = ":28C:67619/1 \n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with Windows line endings (CRLF)
|
||||
mt940_content = ":28C:167619/1\r\n"
|
||||
expected_content = ":28C:67619/1\r\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with leading spaces (should not match as it's not line start)
|
||||
mt940_content = " :28C:167619/1\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should remain unchanged
|
||||
|
||||
@@ -223,7 +223,8 @@
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
"label": "Party IBAN (Bank Statement)",
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
@@ -238,7 +239,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-18 17:24:57.044666",
|
||||
"modified": "2025-08-29 11:53:45.908169",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -7,6 +7,9 @@ from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
|
||||
IBAN_1 = "DE02000000003716541159"
|
||||
IBAN_2 = "DE02500105170137075030"
|
||||
|
||||
|
||||
class TestAutoMatchParty(IntegrationTestCase):
|
||||
@classmethod
|
||||
@@ -22,24 +25,24 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
||||
|
||||
def test_match_by_account_number(self):
|
||||
create_supplier_for_match(account_no="000000003716541159")
|
||||
create_supplier_for_match(account_no=IBAN_1[11:])
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_iban(self):
|
||||
create_supplier_for_match(iban="DE02000000003716541159")
|
||||
create_supplier_for_match(iban=IBAN_1)
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="c5455a224602afaa51592a9d9250600d",
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
@@ -51,7 +54,7 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
withdrawal=1200,
|
||||
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
||||
party_name="Ella Jackson",
|
||||
iban="DE04000000003716545346",
|
||||
iban=IBAN_2,
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Jackson Ella W.")
|
||||
|
||||
@@ -145,8 +145,10 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
if not frappe.db.count("Budget", cache=True):
|
||||
return
|
||||
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
if not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
|
||||
if args.get("company"):
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
@@ -134,7 +134,8 @@ class ExchangeRateRevaluation(Document):
|
||||
accounts = self.get_accounts_data()
|
||||
if accounts:
|
||||
for acc in accounts:
|
||||
self.append("accounts", acc)
|
||||
if acc.get("gain_loss"):
|
||||
self.append("accounts", acc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_accounts_data(self):
|
||||
|
||||
@@ -29,14 +29,17 @@
|
||||
"against_voucher",
|
||||
"voucher_detail_no",
|
||||
"transaction_exchange_rate",
|
||||
"reporting_currency_exchange_rate",
|
||||
"amounts_section",
|
||||
"debit_in_account_currency",
|
||||
"debit",
|
||||
"debit_in_transaction_currency",
|
||||
"debit_in_reporting_currency",
|
||||
"column_break_bm1w",
|
||||
"credit_in_account_currency",
|
||||
"credit",
|
||||
"credit_in_transaction_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_lmnm",
|
||||
@@ -353,13 +356,31 @@
|
||||
{
|
||||
"fieldname": "column_break_8abq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-21 15:29:11.221890",
|
||||
"modified": "2025-08-22 12:57:17.750252",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
@@ -390,8 +411,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ from erpnext.accounts.party import (
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.exceptions import InvalidAccountCurrency, ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
@@ -42,9 +43,11 @@ class GLEntry(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
credit_in_transaction_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
debit_in_transaction_currency: DF.Currency
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
@@ -57,6 +60,7 @@ class GLEntry(Document):
|
||||
posting_date: DF.Date | None
|
||||
project: DF.Link | None
|
||||
remarks: DF.Text | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
to_rename: DF.Check
|
||||
transaction_currency: DF.Link | None
|
||||
transaction_date: DF.Date | None
|
||||
@@ -88,6 +92,8 @@ class GLEntry(Document):
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
|
||||
self.set_amount_in_reporting_currency()
|
||||
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
@@ -131,18 +137,20 @@ class GLEntry(Document):
|
||||
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
# skipping validation for payroll entry creation in case party is not required
|
||||
if not frappe.flags.party_not_required_for_receivable_payable:
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Zero value transaction is not allowed
|
||||
if not (
|
||||
@@ -292,6 +300,25 @@ class GLEntry(Document):
|
||||
if self.party_type and self.party:
|
||||
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
|
||||
|
||||
def set_amount_in_reporting_currency(self):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", self.company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
transaction_date = self.transaction_date or self.posting_date
|
||||
self.reporting_currency_exchange_rate = get_exchange_rate(
|
||||
default_currency, reporting_currency, transaction_date
|
||||
)
|
||||
if not self.reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, transaction_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
self.debit_in_reporting_currency = flt(self.debit * self.reporting_currency_exchange_rate)
|
||||
self.credit_in_reporting_currency = flt(self.credit * self.reporting_currency_exchange_rate)
|
||||
|
||||
def validate_and_set_fiscal_year(self):
|
||||
if not self.fiscal_year:
|
||||
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
|
||||
@@ -308,7 +335,7 @@ def validate_balance_type(account, adv_adj=False):
|
||||
if balance_must_be:
|
||||
balance = frappe.db.sql(
|
||||
"""select sum(debit) - sum(credit)
|
||||
from `tabGL Entry` where account = %s""",
|
||||
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
|
||||
account,
|
||||
)[0][0]
|
||||
|
||||
|
||||
@@ -644,8 +644,11 @@ class JournalEntry(AccountsController):
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
|
||||
# skipping validation for payroll entry creation
|
||||
skip_validation = frappe.flags.party_not_required_for_receivable_payable
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (d.party_type and d.party):
|
||||
if not (d.party_type and d.party) and not skip_validation:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"help_section",
|
||||
"loyalty_program_help"
|
||||
],
|
||||
@@ -144,6 +145,12 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_4",
|
||||
"invoices"
|
||||
],
|
||||
@@ -63,6 +64,12 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
|
||||
@@ -74,6 +74,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
|
||||
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
|
||||
party_link.secondary_party = secondary_party
|
||||
|
||||
party_link.save(ignore_permissions=True)
|
||||
party_link.save()
|
||||
|
||||
return party_link
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
@@ -194,6 +195,12 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
@@ -392,6 +393,12 @@ class PaymentReconciliation(Document):
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||
allocated_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
|
||||
)
|
||||
difference_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
|
||||
)
|
||||
difference_amount = 0
|
||||
if frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
@@ -399,8 +406,14 @@ class PaymentReconciliation(Document):
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
||||
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
||||
allocated_amount_in_ref_rate = flt(
|
||||
payment_entry.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||
difference_amount_precision,
|
||||
)
|
||||
allocated_amount_in_inv_rate = flt(
|
||||
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||
difference_amount_precision,
|
||||
)
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
|
||||
return difference_amount
|
||||
|
||||
@@ -228,7 +228,8 @@
|
||||
"fetch_from": "bank_account.iban",
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "IBAN"
|
||||
"label": "IBAN",
|
||||
"options": "IBAN"
|
||||
},
|
||||
{
|
||||
"fetch_from": "bank_account.branch_code",
|
||||
@@ -458,11 +459,12 @@
|
||||
"label": "Phone Number"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-04 05:39:32.448857",
|
||||
"modified": "2025-08-29 11:52:48.555415",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -497,8 +499,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,17 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return
|
||||
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
previous_fiscal_year_closed = frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
)
|
||||
if previous_fiscal_year_closed:
|
||||
return
|
||||
|
||||
gle_exists_in_previous_year = frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
@@ -86,16 +97,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
if not gle_exists_in_previous_year:
|
||||
return
|
||||
|
||||
previous_fiscal_year_closed = frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
)
|
||||
if not previous_fiscal_year_closed:
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
def block_if_future_closing_voucher_exists(self):
|
||||
future_closing_voucher = self.get_future_closing_voucher()
|
||||
|
||||
@@ -83,6 +83,16 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("print_format", function () {
|
||||
return {
|
||||
filters: {
|
||||
print_format_for: "Report",
|
||||
report: frm.doc.report,
|
||||
disabled: 0,
|
||||
print_format_type: "Jinja",
|
||||
},
|
||||
};
|
||||
});
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
@@ -106,6 +116,16 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
frm.set_query("print_format", function () {
|
||||
return {
|
||||
filters: {
|
||||
print_format_for: "Report",
|
||||
report: frm.doc.report,
|
||||
disabled: 0,
|
||||
print_format_type: "Jinja",
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
customer_collection: function (frm) {
|
||||
frm.set_value("collection_name", "");
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"column_break_17",
|
||||
"customers",
|
||||
"preferences",
|
||||
"print_format",
|
||||
"orientation",
|
||||
"include_break",
|
||||
"include_ageing",
|
||||
@@ -406,10 +407,16 @@
|
||||
"fieldname": "show_future_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Future Payments"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Format",
|
||||
"options": "Print Format"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-08-29 00:20:08.088189",
|
||||
"modified": "2025-09-03 14:24:43.608565",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -67,6 +67,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
pdf_name: DF.Data | None
|
||||
posting_date: DF.Date | None
|
||||
primary_mandatory: DF.Check
|
||||
print_format: DF.Link | None
|
||||
project: DF.TableMultiSelect[PSOAProject]
|
||||
report: DF.Literal["General Ledger", "Accounts Receivable"]
|
||||
sales_partner: DF.Link | None
|
||||
@@ -109,6 +110,25 @@ class ProcessStatementOfAccounts(Document):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
if self.print_format:
|
||||
pf = frappe.db.get_value(
|
||||
"Print Format",
|
||||
self.print_format,
|
||||
["print_format_type", "print_format_for", "report", "disabled"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not pf:
|
||||
frappe.throw(title=_("Invalid Print Format"), msg=_("Selected Print Format does not exist."))
|
||||
if pf.print_format_type != "Jinja":
|
||||
frappe.throw(title=_("Invalid Print Format"), msg=_("Print Format Type should be Jinja."))
|
||||
if pf.print_format_for != "Report" or pf.report != self.report or pf.disabled:
|
||||
frappe.throw(
|
||||
title=_("Invalid Print Format"),
|
||||
msg=_(
|
||||
"Print Format must be an enabled Report Print Format matching the selected Report."
|
||||
),
|
||||
)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
return
|
||||
@@ -290,6 +310,10 @@ def get_html(doc, filters, entry, col, res, ageing):
|
||||
if process_soa_html and process_soa_html.get(doc.report):
|
||||
template_path = process_soa_html[doc.report][-1]
|
||||
|
||||
if doc.print_format:
|
||||
custom_html, custom_css = frappe.db.get_value("Print Format", doc.print_format, ["html", "css"])
|
||||
template_path = f"<style>{custom_css}</style> {custom_html}"
|
||||
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
|
||||
@@ -328,11 +328,20 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
{% else %}
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="future_amount"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="remaining_balance"), currency=data[0]["currency"]) }}</b></td>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_9",
|
||||
"account_currency",
|
||||
"net_amount",
|
||||
@@ -214,6 +215,13 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||
|
||||
@@ -275,6 +275,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.tax_id",
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"hide_days": 1,
|
||||
@@ -2241,7 +2242,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-08-04 19:20:28.732039",
|
||||
"modified": "2025-09-09 14:48:59.472826",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
@@ -191,6 +192,13 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"shipping_amount_section",
|
||||
"calculate_based_on",
|
||||
"column_break_8",
|
||||
@@ -138,6 +139,12 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
|
||||
@@ -976,6 +976,7 @@ class ReceivablePayableReport:
|
||||
|
||||
if self.account_type == "Receivable":
|
||||
self.add_customer_filters()
|
||||
self.exclude_employee_transaction()
|
||||
|
||||
elif self.account_type == "Payable":
|
||||
self.add_supplier_filters()
|
||||
@@ -1055,6 +1056,9 @@ class ReceivablePayableReport:
|
||||
)
|
||||
)
|
||||
|
||||
def exclude_employee_transaction(self):
|
||||
self.qb_selection_filter.append(self.ple.party_type != "Employee")
|
||||
|
||||
def add_supplier_filters(self):
|
||||
supplier = qb.DocType("Supplier")
|
||||
if self.filters.get("supplier_group"):
|
||||
|
||||
@@ -38,6 +38,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
|
||||
get_report_summary as get_pl_summary,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -564,7 +565,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
|
||||
|
||||
row[company] = flt(d.get(company, 0.0), 3)
|
||||
|
||||
if abs(row[company]) >= 0.005:
|
||||
if abs(row[company]) >= get_zero_cutoff(filters.presentation_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
total += flt(row[company])
|
||||
|
||||
@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
|
||||
filter_out_zero_value_rows,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -154,7 +155,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list):
|
||||
for dimension in dimension_list:
|
||||
row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
||||
|
||||
if abs(row[frappe.scrub(dimension)]) >= 0.005:
|
||||
if abs(row[frappe.scrub(dimension)]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
total += flt(d.get(frappe.scrub(dimension), 0.0), 3)
|
||||
|
||||
@@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.accounts.utils import get_fiscal_year, get_zero_cutoff
|
||||
|
||||
|
||||
def get_period_list(
|
||||
@@ -306,7 +306,7 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
|
||||
|
||||
row[period.key] = flt(d.get(period.key, 0.0), 3)
|
||||
|
||||
if abs(row[period.key]) >= 0.005:
|
||||
if abs(row[period.key]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
total += flt(row[period.key])
|
||||
@@ -530,6 +530,7 @@ def get_accounting_entries(
|
||||
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
|
||||
query = query.where(gl_entry.is_cancelled == 0)
|
||||
query = query.where(gl_entry.posting_date <= to_date)
|
||||
query = query.force_index("posting_date_company_index")
|
||||
|
||||
if ignore_opening_entries and not ignore_is_opening:
|
||||
query = query.where(gl_entry.is_opening == "No")
|
||||
|
||||
@@ -667,7 +667,7 @@ def get_columns(filters):
|
||||
"options": "GL Entry",
|
||||
"hidden": 1,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
|
||||
{
|
||||
"label": _("Account"),
|
||||
"fieldname": "account",
|
||||
|
||||
@@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import (
|
||||
filter_out_zero_value_rows,
|
||||
)
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
|
||||
value_fields = ("income", "expense", "gross_profit_loss")
|
||||
|
||||
@@ -149,7 +150,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
|
||||
for key in value_fields:
|
||||
row[key] = flt(d.get(key, 0.0), 3)
|
||||
|
||||
if abs(row[key]) >= 0.005:
|
||||
if abs(row[key]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from erpnext.accounts.report.financial_statements import (
|
||||
set_gl_entries_by_account,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
|
||||
value_fields = (
|
||||
"opening_debit",
|
||||
@@ -412,7 +413,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
for key in value_fields:
|
||||
row[key] = flt(d.get(key, 0.0), 3)
|
||||
|
||||
if abs(row[key]) >= 0.005:
|
||||
if abs(row[key]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
|
||||
|
||||
@@ -4,15 +4,25 @@
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"mandatory": 1,
|
||||
"options": "Company",
|
||||
"wildcard_filter": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2019-01-17 17:20:42.374958",
|
||||
"modified": "2025-08-28 19:06:54.273322",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Trial Balance (Simple)",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
|
||||
"query": "select fiscal_year as \"Fiscal Year:Data:80\",\n\tcompany as \"Company:Data:220\",\n\tposting_date as \"Posting Date:Date:100\",\n\taccount as \"Account:Data:380\",\n\tsum(debit) as \"Debit:Currency:140\",\n\tsum(credit) as \"Credit:Currency:140\",\n\tfinance_book as \"Finance Book:Link/Finance Book:140\"\nfrom `tabGL Entry`\nwhere is_cancelled = 0 and company = %(company)s\ngroup by fiscal_year, company, posting_date, account\norder by fiscal_year, company, posting_date, account",
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Trial Balance (Simple)",
|
||||
"report_type": "Query Report",
|
||||
|
||||
@@ -10,6 +10,7 @@ from erpnext.accounts.party import get_party_shipping_address
|
||||
from erpnext.accounts.utils import (
|
||||
get_future_stock_vouchers,
|
||||
get_voucherwise_gl_entries,
|
||||
get_zero_cutoff,
|
||||
sort_stock_vouchers_by_posting_date,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -157,6 +158,11 @@ class TestUtils(IntegrationTestCase):
|
||||
self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year))
|
||||
frappe.db.set_default("supp_master_name", "Supplier Name")
|
||||
|
||||
def test_get_zero_cutoff(self):
|
||||
self.assertEqual(get_zero_cutoff(None), 0.005)
|
||||
self.assertEqual(get_zero_cutoff("EUR"), 0.005)
|
||||
self.assertEqual(get_zero_cutoff("BHD"), 0.0005)
|
||||
|
||||
|
||||
ADDRESS_RECORDS = [
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ from frappe.utils import (
|
||||
now,
|
||||
nowdate,
|
||||
)
|
||||
from frappe.utils.caching import site_cache
|
||||
from pypika import Order
|
||||
from pypika.functions import Coalesce
|
||||
from pypika.terms import ExistsCriterion
|
||||
@@ -1147,6 +1148,29 @@ def get_currency_precision():
|
||||
return precision
|
||||
|
||||
|
||||
def get_fraction_units(currency: str) -> int:
|
||||
"""Returns the number of fraction units for a currency."""
|
||||
fraction_units = frappe.db.get_value("Currency", currency, "fraction_units")
|
||||
|
||||
if fraction_units is None:
|
||||
fraction_units = 100
|
||||
|
||||
return fraction_units
|
||||
|
||||
|
||||
@site_cache()
|
||||
def get_zero_cutoff(currency: str) -> float:
|
||||
"""Returns the zero cutoff for a currency.
|
||||
|
||||
For example, if the Fraction Units for a currency are set to 100, then the zero cutoff is 0.005.
|
||||
We don't want to display values less than the zero cutoff.
|
||||
This value was chosen for compatibility with the previous hard-coded value of 0.005.
|
||||
"""
|
||||
fraction_units = get_fraction_units(currency)
|
||||
|
||||
return 0.5 / (fraction_units or 1)
|
||||
|
||||
|
||||
def get_held_invoices(party_type, party):
|
||||
"""
|
||||
Returns a list of names Purchase Invoices for the given party that are on hold
|
||||
@@ -2482,6 +2506,10 @@ def build_qb_match_conditions(doctype, user=None) -> list:
|
||||
for filter in match_filters:
|
||||
for link_option, allowed_values in filter.items():
|
||||
fieldnames = link_fields_map.get(link_option, [])
|
||||
cond = None
|
||||
|
||||
if link_option == doctype:
|
||||
cond = _dt["name"].isin(allowed_values)
|
||||
|
||||
for fieldname in fieldnames:
|
||||
field = _dt[fieldname]
|
||||
@@ -2490,6 +2518,7 @@ def build_qb_match_conditions(doctype, user=None) -> list:
|
||||
if not apply_strict_user_permissions:
|
||||
cond = (Coalesce(field, "") == "") | cond
|
||||
|
||||
if cond:
|
||||
criterion.append(cond)
|
||||
|
||||
return criterion
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"target_fixed_asset_account"
|
||||
],
|
||||
"fields": [
|
||||
@@ -275,6 +276,12 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"fixed_asset_account"
|
||||
],
|
||||
"fields": [
|
||||
@@ -98,6 +99,13 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
|
||||
@@ -61,7 +61,7 @@ class AssetMovement(Document):
|
||||
if d.source_location:
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
|
||||
_("Asset {0} does not belong to the location {1}").format(d.asset, d.source_location)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
@@ -76,11 +76,11 @@ class AssetMovement(Document):
|
||||
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
|
||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||
frappe.throw(
|
||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
||||
_("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
|
||||
)
|
||||
|
||||
def validate_employee(self, d):
|
||||
if self.purpose == "Tranfer and Issue":
|
||||
if self.purpose == "Transfer and Issue":
|
||||
if not d.from_employee:
|
||||
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
@@ -89,7 +89,7 @@ class AssetMovement(Document):
|
||||
|
||||
if current_custodian != d.from_employee:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
||||
_("Asset {0} does not belong to the custodian {1}").format(d.asset, d.from_employee)
|
||||
)
|
||||
|
||||
if not d.to_employee:
|
||||
@@ -97,7 +97,7 @@ class AssetMovement(Document):
|
||||
|
||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||
frappe.throw(
|
||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
||||
_("Employee {0} does not belong to the company {1}").format(d.to_employee, self.company)
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
|
||||
@@ -503,6 +503,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
|
||||
onload() {
|
||||
super.onload();
|
||||
this.frm.set_query("supplier", function () {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -133,7 +133,10 @@ frappe.ui.form.on("Supplier", {
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
|
||||
if (
|
||||
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
|
||||
frappe.model.can_create("Party Link")
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Link with Customer"),
|
||||
function () {
|
||||
|
||||
@@ -1070,7 +1070,9 @@ class StockController(AccountsController):
|
||||
from erpnext.stock.stock_ledger import make_sl_entries
|
||||
|
||||
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
|
||||
update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher)
|
||||
update_batch_qty(
|
||||
self.doctype, self.name, self.docstatus, via_landed_cost_voucher=via_landed_cost_voucher
|
||||
)
|
||||
|
||||
def make_gl_entries_on_cancel(self, from_repost=False):
|
||||
if not from_repost:
|
||||
|
||||
@@ -24,3 +24,7 @@ class InvalidAccountDimensionError(frappe.ValidationError):
|
||||
|
||||
class MandatoryAccountDimensionError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ReportingCurrencyExchangeNotFoundError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
@@ -51,7 +51,7 @@ doctype_list_js = {
|
||||
],
|
||||
}
|
||||
|
||||
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||
extend_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||
|
||||
override_whitelisted_methods = {"frappe.www.contact.send_message": "erpnext.templates.utils.send_message"}
|
||||
|
||||
|
||||
1752
erpnext/locale/ar.po
1752
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
1760
erpnext/locale/bs.po
1760
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
1746
erpnext/locale/cs.po
1746
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
2424
erpnext/locale/da.po
2424
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
1806
erpnext/locale/de.po
1806
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
1754
erpnext/locale/eo.po
1754
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
1752
erpnext/locale/es.po
1752
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
1758
erpnext/locale/fa.po
1758
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
1945
erpnext/locale/fr.po
1945
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
1760
erpnext/locale/hr.po
1760
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
1800
erpnext/locale/hu.po
1800
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
1752
erpnext/locale/id.po
1752
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
1852
erpnext/locale/it.po
1852
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2991
erpnext/locale/nb.po
2991
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
1746
erpnext/locale/nl.po
1746
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
1746
erpnext/locale/pl.po
1746
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
1760
erpnext/locale/pt.po
1760
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1746
erpnext/locale/ru.po
1746
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
2034
erpnext/locale/sr.po
2034
erpnext/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1850
erpnext/locale/sv.po
1850
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
1752
erpnext/locale/th.po
1752
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
1754
erpnext/locale/tr.po
1754
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
1746
erpnext/locale/vi.po
1746
erpnext/locale/vi.po
File diff suppressed because it is too large
Load Diff
1754
erpnext/locale/zh.po
1754
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -1414,7 +1414,7 @@ def get_children(parent=None, is_root=False, **filters):
|
||||
return bom_items
|
||||
|
||||
|
||||
def add_additional_cost(stock_entry, work_order):
|
||||
def add_additional_cost(stock_entry, work_order, job_card=None):
|
||||
# Add non stock items cost in the additional cost
|
||||
stock_entry.additional_costs = []
|
||||
company_account = frappe.db.get_value(
|
||||
@@ -1427,13 +1427,16 @@ def add_additional_cost(stock_entry, work_order):
|
||||
expense_account = (
|
||||
company_account.default_operating_cost_account or company_account.default_expense_account
|
||||
)
|
||||
add_non_stock_items_cost(stock_entry, work_order, expense_account)
|
||||
add_operations_cost(stock_entry, work_order, expense_account)
|
||||
add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=job_card)
|
||||
add_operations_cost(stock_entry, work_order, expense_account, job_card=job_card)
|
||||
|
||||
|
||||
def add_non_stock_items_cost(stock_entry, work_order, expense_account):
|
||||
def add_non_stock_items_cost(stock_entry, work_order, expense_account, job_card=None):
|
||||
bom = frappe.get_doc("BOM", work_order.bom_no)
|
||||
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
|
||||
|
||||
table = "items"
|
||||
if work_order and not job_card:
|
||||
table = "exploded_items" if work_order.get("use_multi_level_bom") else "items"
|
||||
|
||||
items = {}
|
||||
for d in bom.get(table):
|
||||
@@ -1464,13 +1467,16 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account):
|
||||
|
||||
|
||||
def add_operating_cost_component_wise(
|
||||
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None
|
||||
stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None, job_card=None
|
||||
):
|
||||
if not work_order:
|
||||
return False
|
||||
|
||||
cost_added = False
|
||||
for row in work_order.operations:
|
||||
if job_card and job_card.operation_id != row.name:
|
||||
continue
|
||||
|
||||
workstation_cost = frappe.get_all(
|
||||
"Workstation Cost",
|
||||
fields=["operating_component", "operating_cost"],
|
||||
@@ -1511,14 +1517,14 @@ def get_component_account(parent):
|
||||
return frappe.db.get_value("Workstation Operating Component Account", parent, "expense_account")
|
||||
|
||||
|
||||
def add_operations_cost(stock_entry, work_order=None, expense_account=None):
|
||||
def add_operations_cost(stock_entry, work_order=None, expense_account=None, job_card=None):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
|
||||
|
||||
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
||||
|
||||
if operating_cost_per_unit:
|
||||
cost_added = add_operating_cost_component_wise(
|
||||
stock_entry, work_order, operating_cost_per_unit, expense_account
|
||||
stock_entry, work_order, operating_cost_per_unit, expense_account, job_card=job_card
|
||||
)
|
||||
|
||||
if not cost_added:
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate"
|
||||
"label": "Rate",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
@@ -158,7 +159,8 @@
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yuca",
|
||||
@@ -180,13 +182,15 @@
|
||||
"fieldname": "base_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Base Amount"
|
||||
"label": "Base Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Base Rate"
|
||||
"label": "Base Rate",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
||||
@@ -23,6 +23,7 @@ from frappe.utils import (
|
||||
time_diff_in_hours,
|
||||
)
|
||||
|
||||
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost
|
||||
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import (
|
||||
get_mins_between_operations,
|
||||
)
|
||||
@@ -817,9 +818,6 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
def update_work_order(self):
|
||||
if self.track_semi_finished_goods:
|
||||
return
|
||||
|
||||
if not self.work_order:
|
||||
return
|
||||
|
||||
@@ -849,9 +847,9 @@ class JobCard(Document):
|
||||
|
||||
def update_semi_finished_good_details(self):
|
||||
if self.operation_id:
|
||||
frappe.db.set_value(
|
||||
"Work Order Operation", self.operation_id, "completed_qty", self.manufactured_qty
|
||||
)
|
||||
qty = max(flt(self.manufactured_qty), flt(self.total_completed_qty))
|
||||
|
||||
frappe.db.set_value("Work Order Operation", self.operation_id, "completed_qty", qty)
|
||||
if (
|
||||
self.finished_good
|
||||
and frappe.get_cached_value("Work Order", self.work_order, "production_item")
|
||||
@@ -1322,6 +1320,9 @@ class JobCard(Document):
|
||||
|
||||
ste.make_stock_entry()
|
||||
ste.stock_entry.flags.ignore_mandatory = True
|
||||
wo_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
add_additional_cost(ste.stock_entry, wo_doc, self)
|
||||
|
||||
ste.stock_entry.save()
|
||||
|
||||
if auto_submit:
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
"overproduction_percentage_for_sales_order",
|
||||
"column_break_16",
|
||||
"overproduction_percentage_for_work_order",
|
||||
"section_break_xhtl",
|
||||
"transfer_extra_materials_percentage",
|
||||
"column_break_kemp",
|
||||
"job_card_section",
|
||||
"add_corrective_operation_cost_in_finished_good_valuation",
|
||||
"enforce_time_logs",
|
||||
@@ -243,13 +246,28 @@
|
||||
"fieldname": "enforce_time_logs",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enforce Time Logs"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xhtl",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Extra Material Transfer"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_kemp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "The user will be able to transfer additional materials from the store to the Work in Progress (WIP) warehouse.",
|
||||
"fieldname": "transfer_extra_materials_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Transfer Extra Raw Materials to WIP (%)"
|
||||
}
|
||||
],
|
||||
"icon": "icon-wrench",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-16 11:23:16.916512",
|
||||
"modified": "2025-09-08 19:48:31.726126",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing Settings",
|
||||
|
||||
@@ -35,6 +35,7 @@ class ManufacturingSettings(Document):
|
||||
overproduction_percentage_for_sales_order: DF.Percent
|
||||
overproduction_percentage_for_work_order: DF.Percent
|
||||
set_op_cost_and_scrap_from_sub_assemblies: DF.Check
|
||||
transfer_extra_materials_percentage: DF.Percent
|
||||
update_bom_costs_automatically: DF.Check
|
||||
validate_components_quantities_per_bom: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -3005,6 +3005,36 @@ class TestWorkOrder(IntegrationTestCase):
|
||||
wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10)
|
||||
)
|
||||
|
||||
def test_allow_additional_material_transfer(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 50)
|
||||
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2)
|
||||
for row in wo_order.required_items:
|
||||
make_stock_entry_test_record(
|
||||
item_code=row.item_code,
|
||||
target=row.source_warehouse,
|
||||
qty=row.required_qty * 2,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2))
|
||||
stock_entry.insert()
|
||||
stock_entry.submit()
|
||||
|
||||
wo_order.reload()
|
||||
self.assertEqual(wo_order.material_transferred_for_manufacturing, 2)
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1))
|
||||
stock_entry.insert()
|
||||
stock_entry.submit()
|
||||
|
||||
wo_order.reload()
|
||||
self.assertEqual(wo_order.material_transferred_for_manufacturing, 3)
|
||||
frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0)
|
||||
|
||||
|
||||
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
|
||||
@@ -116,20 +116,6 @@ frappe.ui.form.on("Work Order", {
|
||||
frm.set_indicator_formatter("operation", function (doc) {
|
||||
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
|
||||
});
|
||||
|
||||
if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
|
||||
args: {
|
||||
bom_no: frm.doc.bom_no,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message["set_scrap_wh_mandatory"]) {
|
||||
frm.toggle_reqd("scrap_warehouse", true);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
@@ -144,6 +130,20 @@ frappe.ui.form.on("Work Order", {
|
||||
});
|
||||
erpnext.work_order.set_default_warehouse(frm);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 0 && frm.doc.bom_no) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.work_order.work_order.check_if_scrap_warehouse_mandatory",
|
||||
args: {
|
||||
bom_no: frm.doc.bom_no,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message["set_scrap_wh_mandatory"]) {
|
||||
frm.toggle_reqd("scrap_warehouse", true);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
source_warehouse: function (frm) {
|
||||
@@ -732,6 +732,19 @@ erpnext.work_order = {
|
||||
let pending_to_transfer = frm.doc.required_items.some(
|
||||
(item) => flt(item.transferred_qty) < flt(item.required_qty)
|
||||
);
|
||||
|
||||
let transfer_extra_materials_percentage =
|
||||
frm.doc.__onload?.transfer_extra_materials_percentage;
|
||||
let allowed_qty = 0;
|
||||
let transfer_extra_materials = false;
|
||||
if (!pending_to_transfer && transfer_extra_materials_percentage) {
|
||||
allowed_qty = frm.doc.qty + (transfer_extra_materials_percentage / 100) * frm.doc.qty;
|
||||
|
||||
if (allowed_qty > frm.doc.material_transferred_for_manufacturing) {
|
||||
transfer_extra_materials = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_to_transfer && frm.doc.status != "Stopped") {
|
||||
frm.has_start_btn = true;
|
||||
frm.add_custom_button(__("Create Pick List"), function () {
|
||||
@@ -742,6 +755,14 @@ erpnext.work_order = {
|
||||
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture");
|
||||
});
|
||||
start_btn.addClass("btn-primary");
|
||||
} else if (transfer_extra_materials && allowed_qty) {
|
||||
let qty = allowed_qty - flt(frm.doc.material_transferred_for_manufacturing);
|
||||
|
||||
if (qty > 0) {
|
||||
frm.add_custom_button(__("Transfer Extra Material"), function () {
|
||||
erpnext.work_order.make_se(frm, "Material Transfer for Manufacture", qty);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -967,19 +988,35 @@ erpnext.work_order = {
|
||||
});
|
||||
},
|
||||
|
||||
make_se: function (frm, purpose) {
|
||||
this.show_prompt_for_qty_input(frm, purpose)
|
||||
.then((data) => {
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
||||
make_se: function (frm, purpose, qty) {
|
||||
if (qty) {
|
||||
frappe
|
||||
.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: purpose,
|
||||
qty: data.qty,
|
||||
qty: qty,
|
||||
})
|
||||
.then((stock_entry) => {
|
||||
frappe.model.sync(stock_entry);
|
||||
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
|
||||
});
|
||||
})
|
||||
.then((stock_entry) => {
|
||||
frappe.model.sync(stock_entry);
|
||||
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
|
||||
});
|
||||
} else {
|
||||
this.show_prompt_for_qty_input(frm, purpose)
|
||||
.then((data) => {
|
||||
return frappe.xcall(
|
||||
"erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry",
|
||||
{
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: purpose,
|
||||
qty: data.qty,
|
||||
}
|
||||
);
|
||||
})
|
||||
.then((stock_entry) => {
|
||||
frappe.model.sync(stock_entry);
|
||||
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
create_pick_list: function (frm, purpose = "Material Transfer for Manufacture") {
|
||||
|
||||
@@ -149,6 +149,7 @@ class WorkOrder(Document):
|
||||
self.set_onload("material_consumption", ms.material_consumption)
|
||||
self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on)
|
||||
self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order)
|
||||
self.set_onload("transfer_extra_materials_percentage", ms.transfer_extra_materials_percentage)
|
||||
self.set_onload("show_create_job_card_button", self.show_create_job_card_button())
|
||||
self.set_onload(
|
||||
"enable_stock_reservation",
|
||||
@@ -354,6 +355,10 @@ class WorkOrder(Document):
|
||||
def calculate_operating_cost(self):
|
||||
self.planned_operating_cost, self.actual_operating_cost = 0.0, 0.0
|
||||
for d in self.get("operations"):
|
||||
if not d.hour_rate:
|
||||
if d.workstation:
|
||||
d.hour_rate = get_hour_rate(d.workstation)
|
||||
|
||||
d.planned_operating_cost = flt(
|
||||
flt(d.hour_rate) * (flt(d.time_in_mins) / 60.0), d.precision("planned_operating_cost")
|
||||
)
|
||||
@@ -485,6 +490,13 @@ class WorkOrder(Document):
|
||||
|
||||
qty = self.get_transferred_or_manufactured_qty(purpose)
|
||||
|
||||
if not allowance_percentage and purpose == "Material Transfer for Manufacture":
|
||||
allowance_percentage = flt(
|
||||
frappe.db.get_single_value(
|
||||
"Manufacturing Settings", "transfer_extra_materials_percentage"
|
||||
)
|
||||
)
|
||||
|
||||
completed_qty = self.qty + (allowance_percentage / 100 * self.qty)
|
||||
if qty > completed_qty:
|
||||
frappe.throw(
|
||||
@@ -1408,8 +1420,8 @@ class WorkOrder(Document):
|
||||
if not self.reserve_stock:
|
||||
continue
|
||||
|
||||
wip_warehouse = wip_warehouse or item.source_warehouse
|
||||
self.update_consumed_qty_in_stock_reservation(item, consumed_qty, wip_warehouse)
|
||||
warehouse = wip_warehouse or item.source_warehouse
|
||||
self.update_consumed_qty_in_stock_reservation(item, consumed_qty, warehouse)
|
||||
|
||||
def update_consumed_qty_in_stock_reservation(self, item, consumed_qty, wip_warehouse):
|
||||
filters = {
|
||||
@@ -2424,3 +2436,8 @@ def get_row_wise_serial_batch(work_order, purpose=None):
|
||||
details.batch_nos[entry.batch_no] += abs(entry.qty)
|
||||
|
||||
return row_wise_serial_batch
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_hour_rate(workstation):
|
||||
return frappe.get_cached_value("Workstation", workstation, "hour_rate") or 0.0
|
||||
|
||||
@@ -397,7 +397,7 @@ erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts
|
||||
erpnext.patches.v15_0.refactor_closing_stock_balance #5
|
||||
erpnext.patches.v15_0.update_asset_status_to_work_in_progress
|
||||
erpnext.patches.v15_0.rename_manufacturing_settings_field
|
||||
erpnext.patches.v15_0.sync_auto_reconcile_config
|
||||
erpnext.patches.v15_0.sync_auto_reconcile_config #2025-08-26
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment")
|
||||
erpnext.patches.v14_0.disable_add_row_in_gross_profit
|
||||
erpnext.patches.v14_0.update_posting_datetime
|
||||
@@ -418,7 +418,7 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
|
||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||
erpnext.patches.v15_0.remove_agriculture_roles
|
||||
erpnext.patches.v14_0.update_full_name_in_contract
|
||||
erpnext.patches.v15_0.drop_sle_indexes
|
||||
erpnext.patches.v15_0.drop_sle_indexes #2025-09-18
|
||||
erpnext.patches.v15_0.update_pick_list_fields
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "confirm_before_resetting_posting_date", 1)
|
||||
erpnext.patches.v15_0.rename_pos_closing_entry_fields #2025-06-13
|
||||
@@ -436,3 +436,4 @@ erpnext.patches.v16_0.update_serial_no_reference_name
|
||||
erpnext.patches.v16_0.set_invoice_type_in_pos_settings
|
||||
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
|
||||
erpnext.patches.v16_0.make_workstation_operating_components #1
|
||||
erpnext.patches.v16_0.set_reporting_currency
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
def execute():
|
||||
table = "tabStock Ledger Entry"
|
||||
index_list = ["posting_datetime_creation_index", "item_warehouse"]
|
||||
index_list = ["posting_datetime_creation_index", "item_warehouse", "batch_no_item_code_warehouse_index"]
|
||||
|
||||
for index in index_list:
|
||||
if not frappe.db.has_index(table, index):
|
||||
|
||||
@@ -11,7 +11,7 @@ def execute():
|
||||
frappe.db.set_single_value("Accounts Settings", "reconciliation_queue_size", 5)
|
||||
|
||||
# Create Scheduler Event record if it doesn't exist
|
||||
if frappe.reload_doc("core", "doctype", "scheduler_event"):
|
||||
if frappe.reload_doc("core", "doctype", "scheduler_event", force=True):
|
||||
method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs"
|
||||
if not frappe.db.get_all(
|
||||
"Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method}
|
||||
|
||||
159
erpnext/patches/v16_0/set_reporting_currency.py
Normal file
159
erpnext/patches/v16_0/set_reporting_currency.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
def execute():
|
||||
set_company_reporting_currency()
|
||||
set_amounts_in_reporting_currency_on_gle_and_acb()
|
||||
|
||||
|
||||
def set_company_reporting_currency():
|
||||
root_companies = frappe.db.get_all(
|
||||
"Company", fields=["name", "default_currency"], filters={"parent_company": ""}, order_by="lft"
|
||||
)
|
||||
|
||||
for d in root_companies:
|
||||
company_subtree = get_descendants_of("Company", d.name)
|
||||
company_subtree.append(d.name)
|
||||
|
||||
update_company_subtree_reporting_currency(company_subtree, d.default_currency)
|
||||
|
||||
|
||||
def update_company_subtree_reporting_currency(companies, currency):
|
||||
Company = frappe.qb.DocType("Company")
|
||||
|
||||
frappe.qb.update(Company).set(Company.reporting_currency, currency).where(
|
||||
Company.name.isin(companies)
|
||||
).run()
|
||||
|
||||
|
||||
def set_amounts_in_reporting_currency_on_gle_and_acb():
|
||||
# get all the companies
|
||||
companies = frappe.db.get_all(
|
||||
"Company", fields=["name", "default_currency", "reporting_currency"], order_by="lft"
|
||||
)
|
||||
|
||||
# get current fiscal year
|
||||
current_fiscal_year = get_fiscal_year(getdate(), as_dict=1, raise_on_missing=False)
|
||||
|
||||
if not current_fiscal_year:
|
||||
return
|
||||
|
||||
previous_fiscal_year = frappe.db.get_value(
|
||||
"Fiscal Year",
|
||||
filters={"year_end_date": ("<", current_fiscal_year.year_start_date)},
|
||||
fieldname=["name", "year_start_date", "year_end_date"],
|
||||
order_by="year_end_date desc",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for d in companies:
|
||||
posting_dates = get_posting_closing_date(d, current_fiscal_year, previous_fiscal_year)
|
||||
exchange_rate_available = check_exchange_rate_availability(d, posting_dates)
|
||||
if not exchange_rate_available:
|
||||
continue
|
||||
set_reporting_currency_by_doctype("GL Entry", d, posting_dates.get("GL Entry"))
|
||||
|
||||
set_reporting_currency_by_doctype(
|
||||
"Account Closing Balance", d, posting_dates.get("Account Closing Balance")
|
||||
)
|
||||
|
||||
|
||||
def get_posting_closing_date(company_details, current_fiscal_year, previous_fiscal_year=None):
|
||||
posting_dates = {}
|
||||
posting_dates["GL Entry"] = get_closing_posting_dates(
|
||||
"GL Entry", company_details.get("name"), current_fiscal_year
|
||||
)
|
||||
|
||||
posting_dates["Account Closing Balance"] = get_closing_posting_dates(
|
||||
"Account Closing Balance", company_details.get("name"), current_fiscal_year
|
||||
)
|
||||
|
||||
if previous_fiscal_year:
|
||||
prev_fy_last_pcv_closing_date = frappe.db.get_value(
|
||||
"Period Closing Voucher",
|
||||
filters={"fiscal_year": previous_fiscal_year.name, "company": company_details.get("name")},
|
||||
fieldname=["transaction_date"],
|
||||
order_by="period_start_date desc",
|
||||
)
|
||||
|
||||
if prev_fy_last_pcv_closing_date:
|
||||
prev_fy_acb_closing_dates = get_closing_posting_dates(
|
||||
"Account Closing Balance",
|
||||
company_details.get("name"),
|
||||
closing_date=prev_fy_last_pcv_closing_date,
|
||||
)
|
||||
posting_dates.setdefault("Account Closing Balance", [])
|
||||
posting_dates["Account Closing Balance"].extend(prev_fy_acb_closing_dates)
|
||||
|
||||
return posting_dates
|
||||
|
||||
|
||||
def check_exchange_rate_availability(company_details, posting_dates):
|
||||
exchange_rate_available = True
|
||||
for doctype, values in posting_dates.items():
|
||||
if not exchange_rate_available:
|
||||
return False
|
||||
date_column = "posting_date" if doctype == "GL Entry" else "closing_date"
|
||||
for d in values:
|
||||
exchange_rate = get_exchange_rate(
|
||||
company_details.get("default_currency"),
|
||||
company_details.get("reporting_currency"),
|
||||
d[date_column],
|
||||
)
|
||||
|
||||
if not exchange_rate:
|
||||
exchange_rate_available = False
|
||||
break
|
||||
|
||||
return exchange_rate_available
|
||||
|
||||
|
||||
def set_reporting_currency_by_doctype(doctype, company_details, posting_closing_dates):
|
||||
date_column = "posting_date" if doctype == "GL Entry" else "closing_date"
|
||||
for d in posting_closing_dates:
|
||||
exchange_rate = get_exchange_rate(
|
||||
company_details.get("default_currency"),
|
||||
company_details.get("reporting_currency"),
|
||||
d[date_column],
|
||||
)
|
||||
|
||||
set_reporting_currency_on_individual_documents(
|
||||
doctype, company_details.get("name"), d[date_column], exchange_rate
|
||||
)
|
||||
|
||||
|
||||
def get_closing_posting_dates(doctype, company, fiscal_year=None, closing_date=None):
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
|
||||
date_column = "posting_date" if doctype == "GL Entry" else "closing_date"
|
||||
query = frappe.qb.from_(dt).select(dt[date_column]).where(dt.company == company).groupby(dt[date_column])
|
||||
|
||||
if doctype == "GL Entry" and fiscal_year:
|
||||
query = query.where(dt.fiscal_year == fiscal_year.name)
|
||||
|
||||
if doctype == "Account Closing Balance":
|
||||
if fiscal_year:
|
||||
query = query.where(dt.closing_date[fiscal_year.year_start_date : fiscal_year.year_end_date])
|
||||
if closing_date:
|
||||
query = query.where(dt.closing_date == closing_date)
|
||||
|
||||
posting_closing_dates = query.run(as_dict=1)
|
||||
|
||||
return posting_closing_dates
|
||||
|
||||
|
||||
def set_reporting_currency_on_individual_documents(doctype, company, posting_closing_date, exchange_rate):
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
|
||||
date_column = "posting_date" if doctype == "GL Entry" else "closing_date"
|
||||
|
||||
frappe.qb.update(dt).set(dt.reporting_currency_exchange_rate, exchange_rate).set(
|
||||
dt.debit_in_reporting_currency, exchange_rate * dt.debit
|
||||
).set(dt.credit_in_reporting_currency, exchange_rate * dt.credit).where(
|
||||
(dt.company == company) & (dt[date_column] == posting_closing_date)
|
||||
).run()
|
||||
@@ -1069,7 +1069,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
if (me.frm.doc.doctype == "Quotation" && me.frm.doc.quotation_to == "Customer") {
|
||||
(party_type = "Customer"), (party_name = me.frm.doc.party_name);
|
||||
} else {
|
||||
party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
|
||||
party_type = frappe.meta.has_field(me.frm.doc.doctype, "supplier") ? "Supplier" : "Customer";
|
||||
party_name = me.frm.doc[party_type.toLowerCase()];
|
||||
}
|
||||
if (party_name) {
|
||||
|
||||
@@ -264,6 +264,5 @@ erpnext.setup.fiscal_years = {
|
||||
Pakistan: ["07-01", "06-30"],
|
||||
Singapore: ["04-01", "03-31"],
|
||||
"South Africa": ["03-01", "02-28"],
|
||||
Thailand: ["10-01", "09-30"],
|
||||
"United Kingdom": ["04-01", "03-31"],
|
||||
};
|
||||
|
||||
@@ -190,7 +190,10 @@ frappe.ui.form.on("Customer", {
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
if (cint(frappe.defaults.get_default("enable_common_party_accounting"))) {
|
||||
if (
|
||||
cint(frappe.defaults.get_default("enable_common_party_accounting")) &&
|
||||
frappe.model.can_create("Party Link")
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Link with Supplier"),
|
||||
function () {
|
||||
|
||||
@@ -365,6 +365,7 @@
|
||||
"fieldname": "base_net_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Rate (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -698,7 +699,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-12 17:31:47.775890",
|
||||
"modified": "2025-08-26 20:31:47.775890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Item",
|
||||
|
||||
@@ -75,7 +75,7 @@ def create_demo_company():
|
||||
frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name)
|
||||
frappe.db.set_default("company", new_company.name)
|
||||
|
||||
bank_account = create_bank_account({"company_name": new_company.name})
|
||||
bank_account = create_bank_account({"company_name": new_company.name}, demo=True)
|
||||
frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name)
|
||||
|
||||
return new_company.name
|
||||
|
||||
@@ -17,6 +17,9 @@ frappe.ui.form.on("Company", {
|
||||
frm.toggle_enable("default_currency", !r.message);
|
||||
});
|
||||
}
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value("reporting_currency", "");
|
||||
}
|
||||
},
|
||||
setup: function (frm) {
|
||||
frm.__rename_queue = "long";
|
||||
@@ -156,6 +159,10 @@ frappe.ui.form.on("Company", {
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value("reporting_currency", "");
|
||||
}
|
||||
|
||||
erpnext.company.set_chart_of_accounts_options(frm.doc);
|
||||
},
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"domain",
|
||||
"date_of_establishment",
|
||||
"parent_company",
|
||||
"reporting_currency",
|
||||
"company_info",
|
||||
"company_logo",
|
||||
"date_of_incorporation",
|
||||
@@ -750,6 +751,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Upon enabling this, the JV will be submitted for a different exchange rate.",
|
||||
"fieldname": "submit_err_jv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit ERR Journals?"
|
||||
@@ -834,6 +836,14 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reconciliation Takes Effect On",
|
||||
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reporting Currency",
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-building",
|
||||
@@ -841,7 +851,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-09 20:12:25.471544",
|
||||
"modified": "2025-08-25 18:34:03.602046",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
@@ -903,9 +913,10 @@
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ class Company(NestedSet):
|
||||
"Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date"
|
||||
]
|
||||
registration_details: DF.Code | None
|
||||
reporting_currency: DF.Link | None
|
||||
rgt: DF.Int
|
||||
round_off_account: DF.Link | None
|
||||
round_off_cost_center: DF.Link | None
|
||||
@@ -153,6 +154,7 @@ class Company(NestedSet):
|
||||
self.check_parent_changed()
|
||||
self.set_chart_of_accounts()
|
||||
self.validate_parent_company()
|
||||
self.set_reporting_currency()
|
||||
|
||||
def validate_abbr(self):
|
||||
if not self.abbr:
|
||||
@@ -490,6 +492,14 @@ class Company(NestedSet):
|
||||
if not is_group:
|
||||
frappe.throw(_("Parent Company must be a group company"))
|
||||
|
||||
def set_reporting_currency(self):
|
||||
self.reporting_currency = self.default_currency
|
||||
if self.parent_company:
|
||||
parent_reporting_currency = frappe.db.get_value(
|
||||
"Company", self.parent_company, ["reporting_currency"]
|
||||
)
|
||||
self.reporting_currency = parent_reporting_currency
|
||||
|
||||
def set_default_accounts(self):
|
||||
default_accounts = {
|
||||
"default_cash_account": "Cash",
|
||||
@@ -681,7 +691,7 @@ class Company(NestedSet):
|
||||
frappe.db.sql("delete from tabBOM where company=%s", self.name)
|
||||
for dt in ("BOM Operation", "BOM Item", "BOM Scrap Item", "BOM Explosion Item"):
|
||||
frappe.db.sql(
|
||||
"delete from `tab{}` where parent in ({})" "".format(dt, ", ".join(["%s"] * len(boms))),
|
||||
"delete from `tab{}` where parent in ({})".format(dt, ", ".join(["%s"] * len(boms))),
|
||||
tuple(boms),
|
||||
)
|
||||
|
||||
|
||||
@@ -814,7 +814,8 @@
|
||||
"depends_on": "eval:doc.salary_mode == 'Bank'",
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN"
|
||||
"label": "IBAN",
|
||||
"options": "IBAN"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
@@ -822,7 +823,7 @@
|
||||
"image_field": "image",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-04 08:29:34.347269",
|
||||
"modified": "2025-08-29 11:52:12.819878",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Employee",
|
||||
@@ -864,6 +865,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "employee_name",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
@@ -871,4 +873,4 @@
|
||||
"states": [],
|
||||
"title_field": "employee_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import unittest
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import build_qb_match_conditions
|
||||
from erpnext.setup.doctype.employee.employee import InactiveEmployeeStatusError
|
||||
|
||||
|
||||
@@ -32,6 +34,32 @@ class TestEmployee(IntegrationTestCase):
|
||||
employee_doc.save()
|
||||
self.assertTrue("Employee" not in frappe.get_roles(user))
|
||||
|
||||
def test_employee_user_permission(self):
|
||||
employee1 = make_employee("employee_1_test@company.com", create_user_permission=1)
|
||||
employee2 = make_employee("employee_2_test@company.com", create_user_permission=1)
|
||||
make_employee("employee_3_test@company.com", create_user_permission=1)
|
||||
|
||||
employee1_doc = frappe.get_doc("Employee", employee1)
|
||||
employee2_doc = frappe.get_doc("Employee", employee2)
|
||||
|
||||
employee2_doc.reload()
|
||||
employee2_doc.reports_to = employee1_doc.name
|
||||
employee2_doc.save()
|
||||
|
||||
frappe.set_user(employee1_doc.user_id)
|
||||
|
||||
Employee = frappe.qb.DocType("Employee")
|
||||
qb_employee_list = (
|
||||
frappe.qb.from_(Employee)
|
||||
.select(Employee.name)
|
||||
.where(Criterion.all(build_qb_match_conditions("Employee")))
|
||||
.orderby(Employee.Name)
|
||||
).run(pluck=Employee.name)
|
||||
employee_list = frappe.db.get_list("Employee", pluck="name", order_by="name")
|
||||
|
||||
self.assertEqual(qb_employee_list, employee_list)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user