mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-17 20:19:20 +00:00
Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b57767d36 | ||
|
|
dc74912f5d | ||
|
|
d860a5b35d | ||
|
|
b00cc83ae6 | ||
|
|
2a2d8da628 | ||
|
|
27b63beb18 | ||
|
|
3c50cfef4e | ||
|
|
1d158d58f6 | ||
|
|
cd57f5cd0a | ||
|
|
3e2bc139ab | ||
|
|
16d0d42afe | ||
|
|
63de576be6 | ||
|
|
d5a544ca69 | ||
|
|
4a713f6b5e | ||
|
|
f165e1732b | ||
|
|
ea57f2b292 | ||
|
|
c880476cbe | ||
|
|
0a9c92fce9 | ||
|
|
aa090beae0 | ||
|
|
ebdacc094c | ||
|
|
0c28726ce2 | ||
|
|
be0604f7cf | ||
|
|
435280d626 | ||
|
|
558d49b3d3 | ||
|
|
05795af471 | ||
|
|
38aa3769bb | ||
|
|
5c6d9c9812 | ||
|
|
85fda71835 | ||
|
|
7150aff520 | ||
|
|
c157978912 | ||
|
|
ae93f7f967 | ||
|
|
f5ddc9a543 | ||
|
|
4c5570ae7d | ||
|
|
d0356f81ba | ||
|
|
c8e2c9aa25 | ||
|
|
fdda86455a | ||
|
|
eb4a485df6 | ||
|
|
579d8e293e | ||
|
|
a807776f83 | ||
|
|
48059a7c74 | ||
|
|
0199bc127a | ||
|
|
17c2734042 | ||
|
|
a1643ad292 | ||
|
|
1f97979059 | ||
|
|
c81b5e3d9c | ||
|
|
0d41c23383 | ||
|
|
0e39aa349e | ||
|
|
e607795bae | ||
|
|
821f39203c | ||
|
|
35e365c263 | ||
|
|
1c50111371 | ||
|
|
5999a8e24f | ||
|
|
4b046160f8 | ||
|
|
9c4b5814a6 | ||
|
|
0d67c62f43 | ||
|
|
5f785ede16 | ||
|
|
5b2fce2df7 | ||
|
|
ae81bb3c1b | ||
|
|
48d6fcaab8 | ||
|
|
633be8d06b | ||
|
|
651d7e4cfc | ||
|
|
0084f45629 | ||
|
|
8ab9fc7f55 | ||
|
|
11c54d27f2 | ||
|
|
173d60fb7d | ||
|
|
5bbef90f08 | ||
|
|
c3bc724523 | ||
|
|
0fbc60a20e | ||
|
|
70b5b08d58 | ||
|
|
294ded2030 | ||
|
|
9f3ae08e3b | ||
|
|
1f683afa43 | ||
|
|
0ab0b4f716 | ||
|
|
4be6a78691 | ||
|
|
d0d97c26a0 | ||
|
|
81b9832917 | ||
|
|
8d8027d423 | ||
|
|
9a374ddbd4 | ||
|
|
3f577779be | ||
|
|
fd1cbf4b6f | ||
|
|
5e7cfeb514 | ||
|
|
223d30ecd8 | ||
|
|
0e1f5ff391 | ||
|
|
a2612d5f36 | ||
|
|
632412bd72 | ||
|
|
09c28760e4 | ||
|
|
7c85e4056f | ||
|
|
d073b005a8 | ||
|
|
45e41827c7 | ||
|
|
d6ef43858c | ||
|
|
66af7f4a14 | ||
|
|
f4518cac9a | ||
|
|
ca56709295 | ||
|
|
860350a5b3 | ||
|
|
6a34abefba | ||
|
|
a523c14fd5 | ||
|
|
6f798ab288 | ||
|
|
ef882de509 | ||
|
|
67809c781a | ||
|
|
c20def5d59 | ||
|
|
e0cb5f9ba8 | ||
|
|
7e61aca512 | ||
|
|
89bd4eba46 | ||
|
|
ea0f24aa57 | ||
|
|
069c763010 | ||
|
|
bc93de682b | ||
|
|
8aa4779191 | ||
|
|
cde19066fe | ||
|
|
465a26f714 | ||
|
|
ca8e7e9891 | ||
|
|
7423d7d337 | ||
|
|
db21def58b | ||
|
|
6a4058052b | ||
|
|
b65e16a91b | ||
|
|
49dad1a456 | ||
|
|
0d3802873b | ||
|
|
20d0e95d7c | ||
|
|
d77a880a62 | ||
|
|
7ccb2ead09 | ||
|
|
8cd455b050 | ||
|
|
072c5b7753 | ||
|
|
0469b0d1ec | ||
|
|
74af7d01a2 | ||
|
|
c545de7bc6 | ||
|
|
8928e062b1 | ||
|
|
97f2341b98 | ||
|
|
aa2ae5e408 | ||
|
|
6d10ccfc15 | ||
|
|
dc99e74ae3 | ||
|
|
fd9aa288c4 | ||
|
|
af74a3c32f | ||
|
|
db1bc8a3db | ||
|
|
4856a9633e | ||
|
|
a4398626f6 | ||
|
|
17e00b397f | ||
|
|
1261513ab2 | ||
|
|
5f752e29f9 | ||
|
|
c0e6f3f4df | ||
|
|
11deff98d9 | ||
|
|
7f8334f29a | ||
|
|
8b02402f62 | ||
|
|
81f1f1f1bb | ||
|
|
16ffee9992 | ||
|
|
723d10241b | ||
|
|
2d284de426 | ||
|
|
52e1551c23 | ||
|
|
08b896fc2c | ||
|
|
0ffeb9f6ad | ||
|
|
e0060f8ffe | ||
|
|
3518991ff0 | ||
|
|
b047425a6f | ||
|
|
d4f0512a10 | ||
|
|
8b15a965dd | ||
|
|
ccf99cf985 | ||
|
|
9b690e9ae6 | ||
|
|
d0e2b7c341 | ||
|
|
4afd4b4044 | ||
|
|
80f0d5b5ec | ||
|
|
234741f35f | ||
|
|
7483839418 | ||
|
|
9e7e6041ed | ||
|
|
9ce1c25c04 | ||
|
|
4335659905 | ||
|
|
514fe69b65 | ||
|
|
a0ea68499b | ||
|
|
76d6dd346c | ||
|
|
83b9680318 | ||
|
|
f3c3f170a7 | ||
|
|
bc03b68b13 | ||
|
|
ad0c65500a | ||
|
|
6bff9d39e3 | ||
|
|
704452a9fa | ||
|
|
7abcfca1cb | ||
|
|
610c483d83 | ||
|
|
608966158a | ||
|
|
efdbe93cf0 | ||
|
|
c2748e923e | ||
|
|
a17e1f6b6d | ||
|
|
0ea6691189 | ||
|
|
eca43916f0 | ||
|
|
8cc59e3be7 | ||
|
|
b130e2065b | ||
|
|
d7deed6c45 | ||
|
|
7cc31df587 | ||
|
|
c30a17cd7a | ||
|
|
c0d3f8cbbe | ||
|
|
b6524946bc | ||
|
|
c98a0ccd1d | ||
|
|
73a31cb395 | ||
|
|
08f6ceeb50 | ||
|
|
f04a934ed1 | ||
|
|
c6bfaa41be | ||
|
|
5848de76ea | ||
|
|
2a54cd5004 | ||
|
|
d61f696f85 | ||
|
|
725d107288 | ||
|
|
b6fe1f5842 | ||
|
|
5b3f0825af | ||
|
|
381101f552 | ||
|
|
1fe534290d | ||
|
|
5004b8fbc9 | ||
|
|
f4603910e4 | ||
|
|
50d15249fc | ||
|
|
5bd633b40f | ||
|
|
41c8cfac73 | ||
|
|
68f3dd848a | ||
|
|
c59a778503 | ||
|
|
8ee7e7d828 | ||
|
|
5d6451fca7 | ||
|
|
90b7ce2dd6 | ||
|
|
7b77128aab | ||
|
|
9598b1fc0f | ||
|
|
ba79560c0c | ||
|
|
9bfd5cdb2b | ||
|
|
8bec67cbcf | ||
|
|
7eb4b42280 | ||
|
|
da2f6a045a | ||
|
|
820692f246 | ||
|
|
186b646dee | ||
|
|
b28ff25180 | ||
|
|
9a3e9c4c9a | ||
|
|
8e6249d361 | ||
|
|
cc07402b5e | ||
|
|
7c78e0025d | ||
|
|
d8d8330123 | ||
|
|
825571ac98 | ||
|
|
836a05d07f | ||
|
|
b9ec43c354 | ||
|
|
c1983a4846 | ||
|
|
b65b57c054 | ||
|
|
8af005cef0 | ||
|
|
bce7acf9cc | ||
|
|
a833dd67f3 | ||
|
|
6e83fec5ca | ||
|
|
290bddea77 | ||
|
|
5fea0b5525 | ||
|
|
c2f7615eeb | ||
|
|
4ba07a40eb | ||
|
|
e3d74684d5 | ||
|
|
73661ac633 | ||
|
|
55f7f63e6e | ||
|
|
aca1577040 | ||
|
|
2754793ff9 | ||
|
|
5e196b9f8b | ||
|
|
61559be8a4 | ||
|
|
07dcf3fac2 | ||
|
|
471781a47e | ||
|
|
0afe893a83 | ||
|
|
92551751bf | ||
|
|
c8682d33d0 | ||
|
|
3423d3c13d | ||
|
|
f3ee439b33 | ||
|
|
ba6e068abc | ||
|
|
34f64f02bf | ||
|
|
762f3bac65 | ||
|
|
b314f3839b | ||
|
|
f387a8fceb | ||
|
|
9ac54f694c | ||
|
|
2bce735300 | ||
|
|
2de9292ac0 | ||
|
|
d0e5568010 | ||
|
|
9724cefce8 | ||
|
|
2affa60ea9 | ||
|
|
2183b99330 | ||
|
|
be07421ab7 | ||
|
|
363f15124e | ||
|
|
34b5639d1c | ||
|
|
4cfeb79355 | ||
|
|
65ec7c5604 | ||
|
|
593428b16d | ||
|
|
3c0623f593 | ||
|
|
7d098328d0 | ||
|
|
3e29ae8534 | ||
|
|
d648875681 | ||
|
|
865786e0b6 | ||
|
|
e6894b949c | ||
|
|
11745add18 | ||
|
|
705a26a2fa | ||
|
|
50fa77276e | ||
|
|
4cde77d8d8 | ||
|
|
f5610e29be | ||
|
|
56f25ae065 | ||
|
|
5958d0c257 | ||
|
|
6dcd015a39 | ||
|
|
489fde8220 | ||
|
|
487b5776e6 | ||
|
|
f397361ba7 | ||
|
|
23e9a4607e | ||
|
|
978a0078d8 | ||
|
|
6649d17b06 | ||
|
|
d7f91824c0 | ||
|
|
107d53b358 | ||
|
|
58ca4a2b99 | ||
|
|
4819535a52 | ||
|
|
7d8d9cfdfe | ||
|
|
ff4751c9e8 | ||
|
|
eeff0a1252 | ||
|
|
47a8fc28df | ||
|
|
5dca98a1cf | ||
|
|
318830c57d | ||
|
|
f4d2ba5bbd | ||
|
|
633997b1b0 | ||
|
|
36b22e290a | ||
|
|
c3e61aebd2 | ||
|
|
de0c6f2ca9 | ||
|
|
20033eef9b |
16
CODEOWNERS
16
CODEOWNERS
@@ -4,21 +4,21 @@
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
||||
erpnext/assets/ @khushi8112 @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/subcontracting @rohitwaghchaure @s-aga-r
|
||||
erpnext/buying/ @rohitwaghchaure
|
||||
erpnext/maintenance/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure
|
||||
erpnext/quality_management/ @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure
|
||||
erpnext/subcontracting @rohitwaghchaure
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007
|
||||
|
||||
.github/ @deepeshgarg007
|
||||
pyproject.toml @phot0n
|
||||
pyproject.toml @akhilnarang
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.41.0"
|
||||
__version__ = "15.45.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -58,7 +58,7 @@ def build_conditions(process_type, account, company):
|
||||
)
|
||||
|
||||
if account:
|
||||
conditions += f"AND {deferred_account}='{frappe.db.escape(account)}'"
|
||||
conditions += f"AND {deferred_account}={frappe.db.escape(account)}"
|
||||
elif company:
|
||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -191,7 +191,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-27 16:23:04.444354",
|
||||
"modified": "2024-08-19 15:19:11.095045",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
|
||||
@@ -60,6 +60,7 @@ class Account(NestedSet):
|
||||
"Payable",
|
||||
"Receivable",
|
||||
"Round Off",
|
||||
"Round Off for Opening",
|
||||
"Stock",
|
||||
"Stock Adjustment",
|
||||
"Stock Received But Not Billed",
|
||||
|
||||
@@ -74,12 +74,12 @@ def get_dimension_filter_map():
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
|
||||
WHERE
|
||||
p.name = a.parent
|
||||
AND p.disabled = 0
|
||||
AND p.name = d.parent
|
||||
""",
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -97,7 +97,6 @@ def get_dimension_filter_map():
|
||||
f.allow_or_restrict,
|
||||
f.is_mandatory,
|
||||
)
|
||||
|
||||
frappe.flags.dimension_filter_map = dimension_filter_map
|
||||
|
||||
return frappe.flags.dimension_filter_map
|
||||
|
||||
@@ -19,16 +19,6 @@
|
||||
"currency",
|
||||
"column_break_11",
|
||||
"conversion_rate",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"section_break_6",
|
||||
"dunning_type",
|
||||
"column_break_8",
|
||||
@@ -56,7 +46,21 @@
|
||||
"income_account",
|
||||
"column_break_48",
|
||||
"cost_center",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"address_and_contact_tab",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"column_break_vodj",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"section_break_xban",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"column_break_lqmf"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -178,10 +182,8 @@
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_display",
|
||||
@@ -377,11 +379,28 @@
|
||||
{
|
||||
"fieldname": "column_break_48",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_and_contact_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vodj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xban",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lqmf",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-15 15:46:53.865712",
|
||||
"modified": "2024-11-26 13:46:07.760867",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
@@ -435,4 +454,4 @@
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,21 @@ class ExchangeRateRevaluation(Document):
|
||||
if not (self.company and self.posting_date):
|
||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||
|
||||
def before_submit(self):
|
||||
self.remove_accounts_without_gain_loss()
|
||||
|
||||
def remove_accounts_without_gain_loss(self):
|
||||
self.accounts = [account for account in self.accounts if account.gain_loss]
|
||||
|
||||
if not self.accounts:
|
||||
frappe.throw(_("At least one account with exchange gain or loss is required"))
|
||||
|
||||
frappe.msgprint(
|
||||
_("Removing rows without exchange gain or loss"),
|
||||
alert=True,
|
||||
indicator="yellow",
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = "GL Entry"
|
||||
|
||||
@@ -248,23 +263,23 @@ class ExchangeRateRevaluation(Document):
|
||||
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
|
||||
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
|
||||
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": d.balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
for d in [x for x in account_details if x.zero_balance]:
|
||||
@@ -288,23 +303,22 @@ class ExchangeRateRevaluation(Document):
|
||||
current_exchange_rate * d.balance_in_account_currency
|
||||
)
|
||||
|
||||
if gain_loss:
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
accounts.append(
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"party": d.party,
|
||||
"account_currency": d.account_currency,
|
||||
"balance_in_base_currency": d.balance,
|
||||
"balance_in_account_currency": d.balance_in_account_currency,
|
||||
"zero_balance": d.zero_balance,
|
||||
"current_exchange_rate": current_exchange_rate,
|
||||
"new_exchange_rate": new_exchange_rate,
|
||||
"new_balance_in_base_currency": new_balance_in_base_currency,
|
||||
"new_balance_in_account_currency": new_balance_in_account_currency,
|
||||
"gain_loss": gain_loss,
|
||||
}
|
||||
)
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 95
|
||||
pe.source_exchange_rate = 84.211
|
||||
pe.source_exchange_rate = 84.2105
|
||||
pe.received_amount = 8000
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
@@ -229,7 +229,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
row = next(x for x in je.accounts if x.account == self.debtors_usd)
|
||||
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||
row = next(x for x in je.accounts if x.account != self.debtors_usd)
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.05) # in INR
|
||||
|
||||
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||
|
||||
@@ -72,10 +72,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Less than 12 months.",
|
||||
"description": "More/Less than 12 months.",
|
||||
"fieldname": "is_short_year",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Short Year",
|
||||
"label": "Is Short/Long Year",
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -127,9 +127,6 @@ class JournalEntry(AccountsController):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.validate_debit_credit_amount()
|
||||
self.set_total_debit_credit()
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
if not frappe.flags.is_reverse_depr_entry:
|
||||
self.validate_against_jv()
|
||||
@@ -184,6 +181,11 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
def before_submit(self):
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
|
||||
@@ -515,6 +515,55 @@ class TestJournalEntry(unittest.TestCase):
|
||||
self.assertEqual(row.debit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
|
||||
def test_toggle_debit_credit_if_negative(self):
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
|
||||
# Create JV with defaut cost center - _Test Cost Center
|
||||
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = nowdate()
|
||||
jv.company = "_Test Company"
|
||||
jv.user_remark = "test"
|
||||
jv.extend(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": "_Test Cash - _TC",
|
||||
"debit": 100 * -1,
|
||||
"debit_in_account_currency": 100 * -1,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"credit": 100 * -1,
|
||||
"credit_in_account_currency": 100 * -1,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
jv.flags.ignore_validate = True
|
||||
jv.save()
|
||||
|
||||
self.assertEqual(len(jv.accounts), 2)
|
||||
|
||||
gl_map = jv.build_gl_map()
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.debit, 100 * -1)
|
||||
self.assertEqual(row.debit_in_account_currency, 100 * -1)
|
||||
self.assertEqual(row.debit_in_transaction_currency, 100 * -1)
|
||||
|
||||
gl_map = process_gl_map(gl_map, False)
|
||||
|
||||
for row in gl_map:
|
||||
if row.account == "_Test Cash - _TC":
|
||||
self.assertEqual(row.credit, 100)
|
||||
self.assertEqual(row.credit_in_account_currency, 100)
|
||||
self.assertEqual(row.credit_in_transaction_currency, 100)
|
||||
|
||||
def test_transaction_exchange_rate_on_journals(self):
|
||||
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
|
||||
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})
|
||||
|
||||
@@ -26,6 +26,10 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
|
||||
if (frm.is_new()) {
|
||||
set_default_party_type(frm);
|
||||
}
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
@@ -181,6 +185,10 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
company: doc.company,
|
||||
status: ["!=", "Paid"],
|
||||
outstanding_amount: [">", 0], // for compatibility with old data
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -316,11 +324,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"write_off_difference_amount",
|
||||
frm.doc.difference_amount && frm.doc.party && frm.doc.total_allocated_amount > party_amount
|
||||
);
|
||||
|
||||
frm.toggle_display(
|
||||
"set_exchange_gain_loss",
|
||||
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount
|
||||
);
|
||||
},
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
@@ -403,6 +406,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
payment_type: function (frm) {
|
||||
set_default_party_type(frm);
|
||||
|
||||
if (frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(
|
||||
[
|
||||
@@ -1109,36 +1114,34 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
set_unallocated_amount: function (frm) {
|
||||
var unallocated_amount = 0;
|
||||
var total_deductions = frappe.utils.sum(
|
||||
$.map(frm.doc.deductions || [], function (d) {
|
||||
return flt(d.amount);
|
||||
})
|
||||
);
|
||||
let unallocated_amount = 0;
|
||||
let deductions_to_consider = 0;
|
||||
|
||||
for (const row of frm.doc.deductions || []) {
|
||||
if (!row.is_exchange_gain_loss) deductions_to_consider += flt(row.amount);
|
||||
}
|
||||
const included_taxes = get_included_taxes(frm);
|
||||
|
||||
if (frm.doc.party) {
|
||||
if (
|
||||
frm.doc.payment_type == "Receive" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions &&
|
||||
frm.doc.total_allocated_amount <
|
||||
frm.doc.paid_amount + total_deductions / frm.doc.source_exchange_rate
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_received_amount +
|
||||
total_deductions -
|
||||
flt(frm.doc.base_total_taxes_and_charges) -
|
||||
frm.doc.base_total_allocated_amount) /
|
||||
frm.doc.source_exchange_rate;
|
||||
} else if (
|
||||
frm.doc.payment_type == "Pay" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions &&
|
||||
frm.doc.total_allocated_amount <
|
||||
frm.doc.received_amount + total_deductions / frm.doc.target_exchange_rate
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount + deductions_to_consider
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_paid_amount +
|
||||
flt(frm.doc.base_total_taxes_and_charges) -
|
||||
(total_deductions + frm.doc.base_total_allocated_amount)) /
|
||||
deductions_to_consider -
|
||||
frm.doc.base_total_allocated_amount -
|
||||
included_taxes) /
|
||||
frm.doc.source_exchange_rate;
|
||||
} else if (
|
||||
frm.doc.payment_type == "Pay" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount - deductions_to_consider
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_received_amount -
|
||||
deductions_to_consider -
|
||||
frm.doc.base_total_allocated_amount -
|
||||
included_taxes) /
|
||||
frm.doc.target_exchange_rate;
|
||||
}
|
||||
}
|
||||
@@ -1232,77 +1235,85 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
write_off_difference_amount: function (frm) {
|
||||
frm.events.set_deductions_entry(frm, "write_off_account");
|
||||
frm.events.set_write_off_deduction(frm);
|
||||
},
|
||||
|
||||
set_exchange_gain_loss: function (frm) {
|
||||
frm.events.set_deductions_entry(frm, "exchange_gain_loss_account");
|
||||
base_paid_amount: function (frm) {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
|
||||
set_deductions_entry: function (frm, account) {
|
||||
if (frm.doc.difference_amount) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
const write_off_row = $.map(frm.doc["deductions"] || [], function (t) {
|
||||
return t.account == r.message[account] ? t : null;
|
||||
});
|
||||
base_received_amount: function (frm) {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
|
||||
const difference_amount = flt(
|
||||
frm.doc.difference_amount,
|
||||
precision("difference_amount")
|
||||
);
|
||||
set_exchange_gain_loss_deduction: async function (frm) {
|
||||
// wait for allocate_party_amount_against_ref_docs to finish
|
||||
await frappe.after_ajax();
|
||||
const base_paid_amount = frm.doc.base_paid_amount || 0;
|
||||
const base_received_amount = frm.doc.base_received_amount || 0;
|
||||
const exchange_gain_loss = flt(
|
||||
base_paid_amount - base_received_amount,
|
||||
get_deduction_amount_precision()
|
||||
);
|
||||
|
||||
const add_deductions = (details) => {
|
||||
let row = null;
|
||||
if (!write_off_row.length && difference_amount) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = details[account];
|
||||
row.cost_center = details["cost_center"];
|
||||
} else {
|
||||
row = write_off_row[0];
|
||||
}
|
||||
|
||||
if (row) {
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
} else {
|
||||
frappe.msgprint(__("No gain or loss in the exchange rate"));
|
||||
}
|
||||
refresh_field("deductions");
|
||||
};
|
||||
|
||||
if (!r.message[account]) {
|
||||
frappe.prompt(
|
||||
{
|
||||
label: __("Please Specify Account"),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
(values) => {
|
||||
const details = Object.assign({}, r.message, values);
|
||||
add_deductions(details);
|
||||
},
|
||||
__(frappe.unscrub(account))
|
||||
);
|
||||
} else {
|
||||
add_deductions(r.message);
|
||||
}
|
||||
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (!exchange_gain_loss) {
|
||||
frm.events.delete_exchange_gain_loss(frm);
|
||||
return;
|
||||
}
|
||||
|
||||
const account_fieldname = "exchange_gain_loss_account";
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const account =
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
row.amount = exchange_gain_loss;
|
||||
frm.refresh_field("deductions");
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
delete_exchange_gain_loss: function (frm) {
|
||||
const exchange_gain_loss_row = (frm.doc.deductions || []).find((row) => row.is_exchange_gain_loss);
|
||||
|
||||
if (!exchange_gain_loss_row) return;
|
||||
|
||||
exchange_gain_loss_row.amount = 0;
|
||||
frm.get_field("deductions").grid.grid_rows[exchange_gain_loss_row.idx - 1].remove();
|
||||
frm.refresh_field("deductions");
|
||||
},
|
||||
|
||||
set_write_off_deduction: async function (frm) {
|
||||
const difference_amount = flt(frm.doc.difference_amount, get_deduction_amount_precision());
|
||||
if (!difference_amount) return;
|
||||
|
||||
const account_fieldname = "write_off_account";
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
const write_off_account =
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
if (!write_off_account) return;
|
||||
|
||||
let row = (frm.doc["deductions"] || []).find((t) => t.account == write_off_account);
|
||||
if (!row) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = write_off_account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
}
|
||||
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
frm.refresh_field("deductions");
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
@@ -1768,6 +1779,13 @@ frappe.ui.form.on("Advance Taxes and Charges", {
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Deduction", {
|
||||
before_deductions_remove: function (doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
if (row.is_exchange_gain_loss && row.amount) {
|
||||
frappe.throw(__("Cannot delete Exchange Gain/Loss row"));
|
||||
}
|
||||
},
|
||||
|
||||
amount: function (frm) {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
@@ -1776,3 +1794,66 @@ frappe.ui.form.on("Payment Entry Deduction", {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
});
|
||||
|
||||
function set_default_party_type(frm) {
|
||||
if (frm.doc.party) return;
|
||||
|
||||
let party_type;
|
||||
if (frm.doc.payment_type == "Receive") {
|
||||
party_type = "Customer";
|
||||
} else if (frm.doc.payment_type == "Pay") {
|
||||
party_type = "Supplier";
|
||||
}
|
||||
|
||||
if (party_type) frm.set_value("party_type", party_type);
|
||||
}
|
||||
|
||||
function get_included_taxes(frm) {
|
||||
let included_taxes = 0;
|
||||
for (const tax of frm.doc.taxes) {
|
||||
if (!tax.included_in_paid_amount) continue;
|
||||
|
||||
if (tax.add_deduct_tax == "Add") {
|
||||
included_taxes += tax.base_tax_amount;
|
||||
} else {
|
||||
included_taxes -= tax.base_tax_amount;
|
||||
}
|
||||
}
|
||||
|
||||
return included_taxes;
|
||||
}
|
||||
|
||||
function get_company_defaults(company) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||
args: {
|
||||
company: company,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function prompt_for_missing_account(frm, account) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = frappe.prompt(
|
||||
{
|
||||
label: __(frappe.unscrub(account)),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
(values) => resolve(values?.[account]),
|
||||
__("Please Specify Account")
|
||||
);
|
||||
|
||||
dialog.on_hide = () => resolve("");
|
||||
});
|
||||
}
|
||||
|
||||
function get_deduction_amount_precision() {
|
||||
return frappe.meta.get_field_precision(frappe.meta.get_field("Payment Entry Deduction", "amount"));
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"section_break_34",
|
||||
"total_allocated_amount",
|
||||
"base_total_allocated_amount",
|
||||
"set_exchange_gain_loss",
|
||||
"column_break_36",
|
||||
"unallocated_amount",
|
||||
"difference_amount",
|
||||
@@ -390,11 +389,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "set_exchange_gain_loss",
|
||||
"fieldtype": "Button",
|
||||
"label": "Set Exchange Gain / Loss"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -801,7 +795,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-31 17:07:06.197249",
|
||||
"modified": "2024-11-07 11:19:19.320883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -893,6 +893,7 @@ class PaymentEntry(AccountsController):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.set_total_allocated_amount()
|
||||
self.set_unallocated_amount()
|
||||
self.set_exchange_gain_loss()
|
||||
self.set_difference_amount()
|
||||
|
||||
def validate_amounts(self):
|
||||
@@ -988,10 +989,10 @@ class PaymentEntry(AccountsController):
|
||||
if d.exchange_rate is None:
|
||||
d.exchange_rate = 1
|
||||
|
||||
allocated_amount_in_pe_exchange_rate = flt(
|
||||
allocated_amount_in_ref_exchange_rate = flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_ref_exchange_rate
|
||||
return base_allocated_amount
|
||||
|
||||
def set_total_allocated_amount(self):
|
||||
@@ -1009,29 +1010,80 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def set_unallocated_amount(self):
|
||||
self.unallocated_amount = 0
|
||||
if self.party:
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
if (
|
||||
self.payment_type == "Receive"
|
||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
|
||||
and self.total_allocated_amount
|
||||
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount + total_deductions - self.base_total_allocated_amount
|
||||
) / self.source_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
elif (
|
||||
self.payment_type == "Pay"
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
|
||||
and self.total_allocated_amount
|
||||
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
|
||||
) / self.target_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
if not self.party:
|
||||
return
|
||||
|
||||
deductions_to_consider = sum(
|
||||
flt(d.amount) for d in self.get("deductions") if not d.is_exchange_gain_loss
|
||||
)
|
||||
included_taxes = self.get_included_taxes()
|
||||
|
||||
if self.payment_type == "Receive" and self.base_total_allocated_amount < (
|
||||
self.base_paid_amount + deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.source_exchange_rate
|
||||
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
|
||||
self.base_received_amount - deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.target_exchange_rate
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
exchange_gain_loss = flt(
|
||||
self.base_paid_amount - self.base_received_amount,
|
||||
self.precision("amount", "deductions"),
|
||||
)
|
||||
|
||||
exchange_gain_loss_rows = [row for row in self.get("deductions") if row.is_exchange_gain_loss]
|
||||
exchange_gain_loss_row = exchange_gain_loss_rows.pop(0) if exchange_gain_loss_rows else None
|
||||
|
||||
for row in exchange_gain_loss_rows:
|
||||
self.remove(row)
|
||||
|
||||
if not exchange_gain_loss:
|
||||
if exchange_gain_loss_row:
|
||||
self.remove(exchange_gain_loss_row)
|
||||
|
||||
return
|
||||
|
||||
if not exchange_gain_loss_row:
|
||||
values = frappe.get_cached_value(
|
||||
"Company", self.company, ("exchange_gain_loss_account", "cost_center"), as_dict=True
|
||||
)
|
||||
|
||||
for fieldname, value in values.items():
|
||||
if value:
|
||||
continue
|
||||
|
||||
label = _(frappe.get_meta("Company").get_label(fieldname))
|
||||
return frappe.msgprint(
|
||||
_("Please set {0} in Company {1} to account for Exchange Gain / Loss").format(
|
||||
label, get_link_to_form("Company", self.company)
|
||||
),
|
||||
title=_("Missing Default in Company"),
|
||||
indicator="red" if self.docstatus.is_submitted() else "yellow",
|
||||
raise_exception=self.docstatus.is_submitted(),
|
||||
)
|
||||
|
||||
exchange_gain_loss_row = self.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": values.exchange_gain_loss_account,
|
||||
"cost_center": values.cost_center,
|
||||
"is_exchange_gain_loss": 1,
|
||||
},
|
||||
)
|
||||
|
||||
exchange_gain_loss_row.amount = exchange_gain_loss
|
||||
|
||||
def set_difference_amount(self):
|
||||
base_unallocated_amount = flt(self.unallocated_amount) * (
|
||||
@@ -1059,11 +1111,13 @@ class PaymentEntry(AccountsController):
|
||||
def get_included_taxes(self):
|
||||
included_taxes = 0
|
||||
for tax in self.get("taxes"):
|
||||
if tax.included_in_paid_amount:
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
if not tax.included_in_paid_amount:
|
||||
continue
|
||||
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
|
||||
return included_taxes
|
||||
|
||||
@@ -1146,6 +1200,12 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
||||
self.setup_party_account_field()
|
||||
|
||||
company_currency = erpnext.get_company_currency(self.company)
|
||||
if self.paid_from_account_currency != company_currency:
|
||||
self.currency = self.paid_from_account_currency
|
||||
elif self.paid_to_account_currency != company_currency:
|
||||
self.currency = self.paid_to_account_currency
|
||||
|
||||
gl_entries = []
|
||||
self.add_party_gl_entries(gl_entries)
|
||||
self.add_bank_gl_entries(gl_entries)
|
||||
@@ -1213,11 +1273,19 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": cost_center,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
@@ -1248,13 +1316,22 @@ class PaymentEntry(AccountsController):
|
||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
}
|
||||
)
|
||||
|
||||
gle.update(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": self.cost_center,
|
||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||
dr_or_cr: base_unallocated_amount,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
gle.update(
|
||||
{
|
||||
@@ -1731,7 +1808,7 @@ class PaymentEntry(AccountsController):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
_("Cannot {0} from {2} without any negative outstanding invoice").format(
|
||||
_("Cannot {0} from {1} without any negative outstanding invoice").format(
|
||||
self.payment_type,
|
||||
self.party_type,
|
||||
)
|
||||
@@ -1889,8 +1966,8 @@ class PaymentEntry(AccountsController):
|
||||
def get_matched_payment_request_of_references(references=None):
|
||||
"""
|
||||
Get those `Payment Requests` which are matched with `References`.\n
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
@@ -1992,7 +2069,7 @@ def get_outstanding_of_references_with_payment_term(references=None):
|
||||
def get_outstanding_of_references_with_no_payment_term(references):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
|
||||
Note: `None` is used for allocation of `Payment Request`
|
||||
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||
@@ -2806,9 +2883,6 @@ def get_payment_entry(
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
base_total_discount_loss = 0
|
||||
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||
@@ -2818,7 +2892,8 @@ def get_payment_entry(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
|
||||
pe.set_difference_amount()
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
if not created_from_payment_request:
|
||||
@@ -2830,7 +2905,7 @@ def get_payment_entry(
|
||||
def get_open_payment_requests_for_references(references=None):
|
||||
"""
|
||||
Fetch all unpaid Payment Requests for the references. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
|
||||
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||
"""
|
||||
@@ -2854,6 +2929,7 @@ def get_open_payment_requests_for_references(references=None):
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
|
||||
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||
).run(as_dict=True)
|
||||
|
||||
@@ -3164,13 +3240,14 @@ def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss
|
||||
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * positive_negative,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -479,16 +479,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||
|
||||
# Exchange loss
|
||||
self.assertEqual(pe.difference_amount, 300.0)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 300.0,
|
||||
},
|
||||
)
|
||||
self.assertEqual(pe.deductions[-1].amount, 300.0)
|
||||
pe.deductions[-1].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[-1].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
@@ -552,16 +545,10 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
self.assertEqual(pe.difference_amount, 100)
|
||||
self.assertEqual(pe.deductions[0].amount, 100)
|
||||
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 100,
|
||||
},
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@@ -654,16 +641,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
self.assertEqual(pe.difference_amount, 500)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 500,
|
||||
},
|
||||
)
|
||||
self.assertEqual(pe.deductions[0].amount, 500)
|
||||
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
@@ -956,6 +936,53 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||
self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2))
|
||||
|
||||
def test_gl_of_multi_currency_payment_transaction(self):
|
||||
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||
save_new_records,
|
||||
test_records,
|
||||
)
|
||||
|
||||
save_new_records(test_records)
|
||||
paid_from = create_account(
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="_Test Cash USD",
|
||||
company="_Test Company",
|
||||
account_type="Cash",
|
||||
account_currency="USD",
|
||||
)
|
||||
payment_entry = create_payment_entry(
|
||||
party="_Test Supplier USD",
|
||||
paid_from=paid_from,
|
||||
paid_to="_Test Payable USD - _TC",
|
||||
paid_amount=100,
|
||||
save=True,
|
||||
)
|
||||
payment_entry.source_exchange_rate = 84.4
|
||||
payment_entry.target_exchange_rate = 84.4
|
||||
payment_entry.save()
|
||||
payment_entry = payment_entry.submit()
|
||||
gle = qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.account,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.debit_in_account_currency,
|
||||
gle.credit_in_account_currency,
|
||||
gle.debit_in_transaction_currency,
|
||||
gle.credit_in_transaction_currency,
|
||||
)
|
||||
.orderby(gle.account)
|
||||
.where(gle.voucher_no == payment_entry.name)
|
||||
.run()
|
||||
)
|
||||
expected_gl_entries = (
|
||||
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0),
|
||||
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0),
|
||||
)
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
def test_multi_currency_payment_entry_with_taxes(self):
|
||||
payment_entry = create_payment_entry(
|
||||
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"cost_center",
|
||||
"amount",
|
||||
"column_break_2",
|
||||
"is_exchange_gain_loss",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
@@ -45,12 +46,20 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_exchange_gain_loss",
|
||||
"fieldname": "is_exchange_gain_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Exchange Gain / Loss?",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-06 07:11:57.739619",
|
||||
"modified": "2024-11-05 16:07:47.307971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
|
||||
@@ -18,6 +18,7 @@ class PaymentEntryDeduction(Document):
|
||||
amount: DF.Currency
|
||||
cost_center: DF.Link
|
||||
description: DF.SmallText | None
|
||||
is_exchange_gain_loss: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -211,12 +211,14 @@ class PaymentReconciliation(Document):
|
||||
if self.get("cost_center"):
|
||||
conditions.append(jea.cost_center == self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
conditions.append(jea[dr_or_cr].gt(0))
|
||||
account_type = erpnext.get_party_account_type(self.party_type)
|
||||
|
||||
if account_type == "Receivable":
|
||||
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
|
||||
elif account_type == "Payable":
|
||||
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
|
||||
|
||||
conditions.append(dr_or_cr.gt(0))
|
||||
|
||||
if self.bank_cash_account:
|
||||
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||
@@ -231,7 +233,7 @@ class PaymentReconciliation(Document):
|
||||
je.posting_date,
|
||||
je.remark.as_("remarks"),
|
||||
jea.name.as_("reference_row"),
|
||||
jea[dr_or_cr].as_("amount"),
|
||||
dr_or_cr.as_("amount"),
|
||||
jea.is_advance,
|
||||
jea.exchange_rate,
|
||||
jea.account_currency.as_("currency"),
|
||||
@@ -371,6 +373,10 @@ class PaymentReconciliation(Document):
|
||||
if self.invoice_limit:
|
||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||
|
||||
non_reconciled_invoices = sorted(
|
||||
non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate())
|
||||
)
|
||||
|
||||
self.add_invoice_entries(non_reconciled_invoices)
|
||||
|
||||
def add_invoice_entries(self, non_reconciled_invoices):
|
||||
|
||||
@@ -632,6 +632,42 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_negative_debit_or_credit_journal_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
# credit debtors account to record a payment
|
||||
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = self.customer
|
||||
je.accounts[1].credit_in_account_currency = 0
|
||||
je.accounts[1].debit_in_account_currency = -1 * amount
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
sales = "Sales - _PR"
|
||||
@@ -954,6 +990,100 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer4
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debtors_eur
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Journal Entry
|
||||
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
|
||||
je1.multi_currency = 1
|
||||
je1.accounts[0].exchange_rate = 1
|
||||
je1.accounts[0].credit_in_account_currency = -8000
|
||||
je1.accounts[0].credit = -8000
|
||||
je1.accounts[0].debit_in_account_currency = 0
|
||||
je1.accounts[0].debit = 0
|
||||
je1.accounts[1].party_type = "Customer"
|
||||
je1.accounts[1].party = self.customer4
|
||||
je1.accounts[1].exchange_rate = 80
|
||||
je1.accounts[1].credit_in_account_currency = 100
|
||||
je1.accounts[1].credit = 8000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
|
||||
je2.multi_currency = 1
|
||||
je2.accounts[0].exchange_rate = 1
|
||||
je2.accounts[0].credit_in_account_currency = -16000
|
||||
je2.accounts[0].credit = -16000
|
||||
je2.accounts[0].debit_in_account_currency = 0
|
||||
je2.accounts[0].debit = 0
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer4
|
||||
je2.accounts[1].exchange_rate = 80
|
||||
je2.accounts[1].credit_in_account_currency = 200
|
||||
je1.accounts[1].credit = 16000
|
||||
je1.accounts[1].debit_in_account_currency = 0
|
||||
je1.accounts[1].debit = 0
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer4
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 2)
|
||||
|
||||
# Test exact payment allocation
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Test partial payment allocation (with excess payment entry)
|
||||
pr.set("allocation", [])
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[1].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||
|
||||
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||
pr.reconcile()
|
||||
total_credit_amount = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||
"sum(credit) as amount",
|
||||
group_by="reference_name",
|
||||
)[0].amount
|
||||
|
||||
# total credit includes the exchange gain/loss amount
|
||||
self.assertEqual(flt(total_credit_amount, 2), 8500)
|
||||
|
||||
jea_parent = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
|
||||
fields=["parent"],
|
||||
)[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||
)
|
||||
|
||||
def test_difference_amount_via_payment_entry(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
@@ -564,11 +564,35 @@ def make_payment_request(**args):
|
||||
# fetches existing payment request `grand_total` amount
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
if existing_payment_request_amount:
|
||||
grand_total -= existing_payment_request_amount
|
||||
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
|
||||
grand_total -= existing_payment_request_amount
|
||||
if not grand_total:
|
||||
frappe.throw(_("Payment Request is already created"))
|
||||
return grand_total
|
||||
|
||||
if existing_payment_request_amount:
|
||||
if args.order_type == "Shopping Cart":
|
||||
# If Payment Request is in an advanced stage, then create for remaining amount.
|
||||
if get_existing_payment_request_amount(
|
||||
ref_doc.doctype, ref_doc.name, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
|
||||
):
|
||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||
else:
|
||||
# If PR's are processed, cancel all of them.
|
||||
cancel_old_payment_requests(ref_doc.doctype, ref_doc.name)
|
||||
else:
|
||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||
|
||||
if existing_paid_amount:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
if ref_doc.conversion_rate:
|
||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||
else:
|
||||
grand_total -= flt(existing_paid_amount)
|
||||
else:
|
||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
@@ -678,21 +702,88 @@ def get_amount(ref_doc, payment_account=None):
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
def get_irequest_status(payment_requests: None | list = None) -> list:
|
||||
IR = frappe.qb.DocType("Integration Request")
|
||||
res = []
|
||||
if payment_requests:
|
||||
res = (
|
||||
frappe.qb.from_(IR)
|
||||
.select(IR.name)
|
||||
.where(IR.reference_doctype.eq("Payment Request"))
|
||||
.where(IR.reference_docname.isin(payment_requests))
|
||||
.where(IR.status.isin(["Authorized", "Completed"]))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return res
|
||||
|
||||
|
||||
def cancel_old_payment_requests(ref_dt, ref_dn):
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
if res := (
|
||||
frappe.qb.from_(PR)
|
||||
.select(PR.name)
|
||||
.where(PR.reference_doctype == ref_dt)
|
||||
.where(PR.reference_name == ref_dn)
|
||||
.where(PR.docstatus == 1)
|
||||
.where(PR.status.isin(["Draft", "Requested"]))
|
||||
.run(as_dict=True)
|
||||
):
|
||||
if get_irequest_status([x.name for x in res]):
|
||||
frappe.throw(_("Another Payment Request is already processed"))
|
||||
else:
|
||||
for x in res:
|
||||
doc = frappe.get_doc("Payment Request", x.name)
|
||||
doc.flags.ignore_permissions = True
|
||||
doc.cancel()
|
||||
|
||||
if ireqs := get_irequests_of_payment_request(doc.name):
|
||||
for ireq in ireqs:
|
||||
frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled")
|
||||
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list:
|
||||
"""
|
||||
Return the total amount of Payment Requests against a reference document.
|
||||
"""
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
query = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(Sum(PR.grand_total))
|
||||
.where(PR.reference_doctype == ref_dt)
|
||||
.where(PR.reference_name == ref_dn)
|
||||
.where(PR.docstatus == 1)
|
||||
.run()
|
||||
)
|
||||
|
||||
if statuses:
|
||||
query = query.where(PR.status.isin(statuses))
|
||||
|
||||
response = query.run()
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_existing_paid_amount(doctype, name):
|
||||
PL = frappe.qb.DocType("Payment Ledger Entry")
|
||||
PER = frappe.qb.DocType("Payment Entry Reference")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PL)
|
||||
.left_join(PER)
|
||||
.on(
|
||||
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
|
||||
)
|
||||
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
|
||||
.where(PL.against_voucher_type.eq(doctype))
|
||||
.where(PL.against_voucher_no.eq(name))
|
||||
.where(PL.amount < 0)
|
||||
.where(PL.delinked == 0)
|
||||
.where(PER.docstatus == 1)
|
||||
.where(PER.payment_request.isnull())
|
||||
)
|
||||
response = query.run()
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
@@ -888,21 +979,17 @@ def validate_payment(doc, method=None):
|
||||
@frappe.whitelist()
|
||||
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# permission checks in `get_list()`
|
||||
reference_doctype = filters.get("reference_doctype")
|
||||
reference_name = filters.get("reference_doctype")
|
||||
filters = frappe._dict(filters)
|
||||
|
||||
if not reference_doctype or not reference_name:
|
||||
if not filters.reference_doctype or not filters.reference_name:
|
||||
return []
|
||||
|
||||
if txt:
|
||||
filters.name = ["like", f"%{txt}%"]
|
||||
|
||||
open_payment_requests = frappe.get_list(
|
||||
"Payment Request",
|
||||
filters={
|
||||
"reference_doctype": filters["reference_doctype"],
|
||||
"reference_name": filters["reference_name"],
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
},
|
||||
filters=filters,
|
||||
fields=["name", "grand_total", "outstanding_amount"],
|
||||
order_by="transaction_date ASC,creation ASC",
|
||||
)
|
||||
@@ -915,3 +1002,17 @@ def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len,
|
||||
)
|
||||
for pr in open_payment_requests
|
||||
]
|
||||
|
||||
|
||||
def get_irequests_of_payment_request(doc: str | None = None) -> list:
|
||||
res = []
|
||||
if doc:
|
||||
res = frappe.db.get_all(
|
||||
"Integration Request",
|
||||
{
|
||||
"reference_doctype": "Payment Request",
|
||||
"reference_docname": doc,
|
||||
"status": "Queued",
|
||||
},
|
||||
)
|
||||
return res
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "payment_request",
|
||||
"internal_links": {
|
||||
"Payment Entry": ["references", "payment_request"],
|
||||
"Payment Order": ["references", "payment_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]},
|
||||
],
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
const INDICATORS = {
|
||||
"Partially Paid": "orange",
|
||||
Cancelled: "red",
|
||||
Draft: "gray",
|
||||
Failed: "red",
|
||||
Initiated: "green",
|
||||
Paid: "blue",
|
||||
Requested: "green",
|
||||
};
|
||||
|
||||
frappe.listview_settings["Payment Request"] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Draft") {
|
||||
return [__("Draft"), "gray", "status,=,Draft"];
|
||||
}
|
||||
if (doc.status == "Requested") {
|
||||
return [__("Requested"), "green", "status,=,Requested"];
|
||||
} else if (doc.status == "Initiated") {
|
||||
return [__("Initiated"), "green", "status,=,Initiated"];
|
||||
} else if (doc.status == "Partially Paid") {
|
||||
return [__("Partially Paid"), "orange", "status,=,Partially Paid"];
|
||||
} else if (doc.status == "Paid") {
|
||||
return [__("Paid"), "blue", "status,=,Paid"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
}
|
||||
if (!doc.status || !INDICATORS[doc.status]) return;
|
||||
|
||||
return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -524,3 +525,48 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
def test_partial_paid_invoice_with_payment_request(self):
|
||||
si = create_sales_invoice(currency="INR", qty=1, rate=5000)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PAYEE0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
si.load_from_db()
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -171,9 +171,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
|
||||
for dimensions, account_balances in pl_account_balances.items():
|
||||
for acc, balances in account_balances.items():
|
||||
balance_in_company_currency = flt(balances.debit_in_account_currency) - flt(
|
||||
balances.credit_in_account_currency
|
||||
)
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
if balance_in_company_currency and acc != "balances":
|
||||
self.pl_accounts_reverse_gle.append(
|
||||
self.get_gle_for_pl_account(acc, balances, dimensions)
|
||||
@@ -417,7 +415,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"Period Closing Voucher",
|
||||
{"company": self.company, "docstatus": 1},
|
||||
"name",
|
||||
order_by="period_end_date",
|
||||
order_by="period_end_date asc",
|
||||
)
|
||||
|
||||
if not first_pcv or first_pcv == self.name:
|
||||
|
||||
@@ -147,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_payments(doc, frm, false);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
@@ -172,7 +172,7 @@ function set_form_data(data, frm) {
|
||||
frm.doc.grand_total += flt(d.grand_total);
|
||||
frm.doc.net_total += flt(d.net_total);
|
||||
frm.doc.total_quantity += flt(d.total_qty);
|
||||
refresh_payments(d, frm);
|
||||
refresh_payments(d, frm, true);
|
||||
refresh_taxes(d, frm);
|
||||
});
|
||||
}
|
||||
@@ -186,7 +186,7 @@ function add_to_pos_transaction(d, frm) {
|
||||
});
|
||||
}
|
||||
|
||||
function refresh_payments(d, frm) {
|
||||
function refresh_payments(d, frm, is_new) {
|
||||
d.payments.forEach((p) => {
|
||||
const payment = frm.doc.payment_reconciliation.find(
|
||||
(pay) => pay.mode_of_payment === p.mode_of_payment
|
||||
@@ -196,9 +196,7 @@ function refresh_payments(d, frm) {
|
||||
}
|
||||
if (payment) {
|
||||
payment.expected_amount += flt(p.amount);
|
||||
if (payment.closing_amount === 0) {
|
||||
payment.closing_amount = payment.expected_amount;
|
||||
}
|
||||
if (is_new) payment.closing_amount = payment.expected_amount;
|
||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||
} else {
|
||||
frm.add_child("payment_reconciliation", {
|
||||
|
||||
@@ -65,7 +65,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
super.refresh();
|
||||
|
||||
if (doc.docstatus == 1 && !doc.is_return) {
|
||||
this.frm.add_custom_button(__("Return"), this.make_sales_return, __("Create"));
|
||||
this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create"));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -1558,12 +1559,19 @@
|
||||
"fieldname": "update_billed_amount_in_delivery_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 16:00:34.268756",
|
||||
"modified": "2024-11-26 13:10:50.309570",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -32,12 +32,8 @@ class POSInvoice(SalesInvoice):
|
||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||
from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem
|
||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
|
||||
SalesInvoiceAdvance,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
|
||||
SalesInvoicePayment,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
|
||||
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
||||
SalesInvoiceTimesheet,
|
||||
)
|
||||
@@ -75,6 +71,7 @@ class POSInvoice(SalesInvoice):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
|
||||
@@ -93,7 +93,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
inv.save()
|
||||
|
||||
self.assertEqual(inv.net_total, 4298.25)
|
||||
self.assertEqual(inv.net_total, 4298.24)
|
||||
self.assertEqual(inv.grand_total, 4900.00)
|
||||
|
||||
def test_tax_calculation_with_multiple_items(self):
|
||||
|
||||
@@ -438,7 +438,9 @@ def split_invoices(invoices):
|
||||
if not item.serial_no and not item.serial_and_batch_bundle:
|
||||
continue
|
||||
|
||||
return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
|
||||
return_against_is_added = any(
|
||||
d for d in _invoices if d and d[0].pos_invoice == pos_invoice.return_against
|
||||
)
|
||||
if return_against_is_added:
|
||||
break
|
||||
|
||||
|
||||
@@ -343,7 +343,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.status, "Return")
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -446,7 +446,20 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
if isinstance(pricing_rule, str):
|
||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
|
||||
update_pricing_rule_uom(pricing_rule, args)
|
||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
|
||||
fetch_other_item = True if pricing_rule.apply_rule_on_other else False
|
||||
pricing_rule.apply_rule_on_other_items = (
|
||||
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1:
|
||||
if not args.coupon_code:
|
||||
return item_details
|
||||
|
||||
coupon_code = frappe.db.get_value(
|
||||
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
|
||||
)
|
||||
if args.coupon_code != coupon_code:
|
||||
continue
|
||||
|
||||
if pricing_rule.get("suggestion"):
|
||||
continue
|
||||
@@ -473,9 +486,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
||||
pricing_rule.apply_rule_on_other_items
|
||||
)
|
||||
|
||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
|
||||
return item_details
|
||||
|
||||
if not pricing_rule.validate_applied_rule:
|
||||
if pricing_rule.price_or_product_discount == "Price":
|
||||
apply_price_discount_rule(pricing_rule, item_details, args)
|
||||
|
||||
@@ -1137,6 +1137,45 @@ class TestPricingRule(FrappeTestCase):
|
||||
so.save()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
def test_pricing_rule_for_product_free_item_round_free_qty(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"currency": "USD",
|
||||
"items": [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
}
|
||||
],
|
||||
"selling": 1,
|
||||
"rate": 0,
|
||||
"min_qty": 100,
|
||||
"max_qty": 0,
|
||||
"price_or_product_discount": "Product",
|
||||
"same_item": 1,
|
||||
"free_qty": 10,
|
||||
"round_free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 100,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
|
||||
# With pricing rule
|
||||
so = make_sales_order(item_code="_Test Item", qty=100)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 10)
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=150)
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 10)
|
||||
|
||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
@@ -655,7 +655,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if transaction_qty:
|
||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = math.floor(qty)
|
||||
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
|
||||
|
||||
if not qty:
|
||||
return
|
||||
|
||||
@@ -474,6 +474,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
reference_doctype="Process Statement Of Accounts",
|
||||
reference_name=document_name,
|
||||
attachments=attachments,
|
||||
expose_recipients="header",
|
||||
)
|
||||
|
||||
if doc.enable_auto_email and from_scheduler:
|
||||
|
||||
@@ -863,6 +863,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
self.make_gl_entries_for_tax_withholding(gl_entries)
|
||||
|
||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||
|
||||
@@ -896,32 +897,37 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
|
||||
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"due_date": self.due_date,
|
||||
"against": self.against_expense_account,
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
self.party_account_currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
def add_supplier_gl_entry(
|
||||
self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False
|
||||
):
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl = {
|
||||
"account": self.credit_to,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"due_date": self.due_date,
|
||||
"against": against_account or self.against_expense_account,
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
"cost_center": self.cost_center,
|
||||
"_skip_merge": skip_merge,
|
||||
}
|
||||
|
||||
if remarks:
|
||||
gl["remarks"] = remarks
|
||||
|
||||
gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self))
|
||||
|
||||
def make_item_gl_entries(self, gl_entries):
|
||||
# item gl entries
|
||||
@@ -1413,6 +1419,31 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
)
|
||||
|
||||
def make_gl_entries_for_tax_withholding(self, gl_entries):
|
||||
"""
|
||||
Tax withholding amount is not part of supplier invoice.
|
||||
Separate supplier GL Entry for correct reporting.
|
||||
"""
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
for row in self.get("taxes"):
|
||||
if not row.is_tax_withholding_account or not row.tax_amount:
|
||||
continue
|
||||
|
||||
base_tds_amount = row.base_tax_amount_after_discount_amount
|
||||
tds_amount = row.tax_amount_after_discount_amount
|
||||
|
||||
self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount)
|
||||
self.add_supplier_gl_entry(
|
||||
gl_entries,
|
||||
-base_tds_amount,
|
||||
-tds_amount,
|
||||
against_account=row.account_head,
|
||||
remarks=_("TDS Deducted"),
|
||||
skip_merge=True,
|
||||
)
|
||||
|
||||
def make_payment_gl_entries(self, gl_entries):
|
||||
# Make Cash GL Entries
|
||||
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
|
||||
@@ -1506,10 +1537,29 @@ class PurchaseInvoice(BuyingController):
|
||||
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
|
||||
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry
|
||||
if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
if self.is_opening == "Yes" and self.rounding_adjustment:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||
).format(
|
||||
frappe.bold(self.rounding_adjustment),
|
||||
frappe.bold("Round Off for Opening"),
|
||||
get_link_to_form("Company", self.company),
|
||||
frappe.bold("Disable Rounded Total"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
round_off_account = round_off_for_opening
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
|
||||
@@ -1544,6 +1544,61 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
payment_entry.load_from_db()
|
||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||
|
||||
def test_purchase_gl_with_tax_withholding_tax(self):
|
||||
company = "_Test Company"
|
||||
|
||||
tds_account_args = {
|
||||
"doctype": "Account",
|
||||
"account_name": "TDS Payable",
|
||||
"account_type": "Tax",
|
||||
"parent_account": frappe.db.get_value(
|
||||
"Account", {"account_name": "Duties and Taxes", "company": company}
|
||||
),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
tds_account = create_account(**tds_account_args)
|
||||
tax_withholding_category = "Test TDS - 194 - Dividends - Individual"
|
||||
|
||||
# Update tax withholding category with current fiscal year and rate details
|
||||
create_tax_witholding_category(tax_withholding_category, company, tds_account)
|
||||
|
||||
# create a new supplier to test
|
||||
supplier = create_supplier(
|
||||
supplier_name="_Test TDS Advance Supplier",
|
||||
tax_withholding_category=tax_withholding_category,
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
supplier=supplier.name,
|
||||
rate=3000,
|
||||
qty=1,
|
||||
item="_Test Non Stock Item",
|
||||
do_not_submit=1,
|
||||
)
|
||||
pi.apply_tds = 1
|
||||
pi.tax_withholding_category = tax_withholding_category
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
self.assertEqual(pi.taxes[0].tax_amount, 300)
|
||||
self.assertEqual(pi.taxes[0].account_head, tds_account)
|
||||
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pi.name, "voucher_type": "Purchase Invoice", "account": "Creditors - _TC"},
|
||||
fields=["account", "against", "debit", "credit"],
|
||||
)
|
||||
|
||||
for gle in gl_entries:
|
||||
if gle.debit:
|
||||
# GL Entry with TDS Amount
|
||||
self.assertEqual(gle.against, tds_account)
|
||||
self.assertEqual(gle.debit, 300)
|
||||
else:
|
||||
# GL Entry with Purchase Invoice Amount
|
||||
self.assertEqual(gle.credit, 3000)
|
||||
|
||||
def test_provisional_accounting_entry(self):
|
||||
setup_provisional_accounting()
|
||||
|
||||
@@ -1680,6 +1735,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
@@ -2310,6 +2389,65 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
item.reload()
|
||||
self.assertEqual(item.last_purchase_rate, 0)
|
||||
|
||||
def test_opening_invoice_rounding_adjustment_validation(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
pi.items[0].rate = 99.98
|
||||
pi.items[0].qty = 1
|
||||
pi.items[0].expense_account = "Temporary Opening - _TC"
|
||||
pi.is_opening = "Yes"
|
||||
pi.save()
|
||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||
|
||||
def _create_opening_roundoff_account(self, company_name):
|
||||
liability_root = frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": company_name, "root_type": "Liability", "disabled": 0},
|
||||
order_by="lft",
|
||||
limit=1,
|
||||
)[0]
|
||||
|
||||
# setup round off account
|
||||
if acc := frappe.db.exists(
|
||||
"Account",
|
||||
{
|
||||
"account_name": "Round Off for Opening",
|
||||
"account_type": "Round Off for Opening",
|
||||
"company": company_name,
|
||||
},
|
||||
):
|
||||
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
|
||||
else:
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.company = company_name
|
||||
acc.parent_account = liability_root.name
|
||||
acc.account_name = "Round Off for Opening"
|
||||
acc.account_type = "Round Off for Opening"
|
||||
acc.save()
|
||||
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
|
||||
|
||||
def test_ledger_entries_of_opening_invoice_with_rounding_adjustment(self):
|
||||
pi = make_purchase_invoice(do_not_save=1)
|
||||
pi.items[0].rate = 99.98
|
||||
pi.items[0].qty = 1
|
||||
pi.items[0].expense_account = "Temporary Opening - _TC"
|
||||
pi.is_opening = "Yes"
|
||||
pi.save()
|
||||
self._create_opening_roundoff_account(pi.company)
|
||||
pi.submit()
|
||||
actual = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pi.name, "is_opening": "Yes", "is_cancelled": False},
|
||||
fields=["account", "debit", "credit", "is_opening"],
|
||||
order_by="account,debit",
|
||||
)
|
||||
expected = [
|
||||
{"account": "Creditors - _TC", "debit": 0.0, "credit": 100.0, "is_opening": "Yes"},
|
||||
{"account": "Round Off for Opening - _TC", "debit": 0.02, "credit": 0.0, "is_opening": "Yes"},
|
||||
{"account": "Temporary Opening - _TC", "debit": 99.98, "credit": 0.0, "is_opening": "Yes"},
|
||||
]
|
||||
self.assertEqual(len(actual), 3)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -45,7 +45,7 @@ class RepostAccountingLedger(Document):
|
||||
latest_pcv = (
|
||||
frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"company": self.company},
|
||||
filters={"company": self.company, "docstatus": 1},
|
||||
order_by="period_end_date desc",
|
||||
pluck="period_end_date",
|
||||
limit=1,
|
||||
|
||||
@@ -741,20 +741,6 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
};
|
||||
};
|
||||
|
||||
frm.set_query("company_address", function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
|
||||
return {
|
||||
query: "frappe.contacts.doctype.address.address.address_query",
|
||||
filters: {
|
||||
link_doctype: "Company",
|
||||
link_name: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("pos_profile", function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
|
||||
@@ -159,8 +159,9 @@
|
||||
"dispatch_address",
|
||||
"company_address_section",
|
||||
"company_address",
|
||||
"company_addr_col_break",
|
||||
"company_address_display",
|
||||
"company_addr_col_break",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"ignore_default_payment_terms_template",
|
||||
@@ -2166,6 +2167,13 @@
|
||||
"label": "Update Outstanding for Self",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2178,7 +2186,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified": "2024-11-26 12:34:09.110690",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2233,4 +2241,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ class SalesInvoice(SellingController):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
company_tax_id: DF.Data | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
@@ -1633,10 +1634,29 @@ class SalesInvoice(SellingController):
|
||||
and self.base_rounding_adjustment
|
||||
and not self.is_internal_transfer()
|
||||
):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
if self.is_opening == "Yes" and self.rounding_adjustment:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||
).format(
|
||||
frappe.bold(self.rounding_adjustment),
|
||||
frappe.bold("Round Off for Opening"),
|
||||
get_link_to_form("Company", self.company),
|
||||
frappe.bold("Disable Rounded Total"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
round_off_account = round_off_for_opening
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1735,6 +1755,9 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def update_project(self):
|
||||
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
|
||||
if self.project and self.project not in unique_projects:
|
||||
unique_projects.append(self.project)
|
||||
|
||||
for p in unique_projects:
|
||||
project = frappe.get_doc("Project", p)
|
||||
project.update_billed_amount()
|
||||
|
||||
@@ -314,7 +314,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.insert()
|
||||
|
||||
# with inclusive tax
|
||||
self.assertEqual(si.items[0].net_amount, 3947.368421052631)
|
||||
self.assertEqual(si.items[0].net_amount, 3947.37)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 3947.37)
|
||||
self.assertEqual(si.grand_total, 5000)
|
||||
|
||||
@@ -658,7 +659,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
62.5,
|
||||
625.0,
|
||||
50,
|
||||
499.97600115194473,
|
||||
499.98,
|
||||
],
|
||||
"_Test Item Home Desktop 200": [
|
||||
190.66,
|
||||
@@ -669,7 +670,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
190.66,
|
||||
953.3,
|
||||
150,
|
||||
749.9968530500239,
|
||||
750,
|
||||
],
|
||||
}
|
||||
|
||||
@@ -682,20 +683,21 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
||||
|
||||
# check net total
|
||||
self.assertEqual(si.net_total, 1249.97)
|
||||
self.assertEqual(si.base_net_total, si.net_total)
|
||||
self.assertEqual(si.net_total, 1249.98)
|
||||
self.assertEqual(si.total, 1578.3)
|
||||
|
||||
# check tax calculation
|
||||
expected_values = {
|
||||
"keys": ["tax_amount", "total"],
|
||||
"_Test Account Excise Duty - _TC": [140, 1389.97],
|
||||
"_Test Account Education Cess - _TC": [2.8, 1392.77],
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.17],
|
||||
"_Test Account CST - _TC": [27.88, 1422.05],
|
||||
"_Test Account VAT - _TC": [156.25, 1578.30],
|
||||
"_Test Account Customs Duty - _TC": [125, 1703.30],
|
||||
"_Test Account Shipping Charges - _TC": [100, 1803.30],
|
||||
"_Test Account Discount - _TC": [-180.33, 1622.97],
|
||||
"_Test Account Excise Duty - _TC": [140, 1389.98],
|
||||
"_Test Account Education Cess - _TC": [2.8, 1392.78],
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.18],
|
||||
"_Test Account CST - _TC": [27.88, 1422.06],
|
||||
"_Test Account VAT - _TC": [156.25, 1578.31],
|
||||
"_Test Account Customs Duty - _TC": [125, 1703.31],
|
||||
"_Test Account Shipping Charges - _TC": [100, 1803.31],
|
||||
"_Test Account Discount - _TC": [-180.33, 1622.98],
|
||||
}
|
||||
|
||||
for d in si.get("taxes"):
|
||||
@@ -731,7 +733,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"base_rate": 2500,
|
||||
"base_amount": 25000,
|
||||
"net_rate": 40,
|
||||
"net_amount": 399.9808009215558,
|
||||
"net_amount": 399.98,
|
||||
"base_net_rate": 2000,
|
||||
"base_net_amount": 19999,
|
||||
},
|
||||
@@ -745,7 +747,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"base_rate": 7500,
|
||||
"base_amount": 37500,
|
||||
"net_rate": 118.01,
|
||||
"net_amount": 590.0531205155963,
|
||||
"net_amount": 590.05,
|
||||
"base_net_rate": 5900.5,
|
||||
"base_net_amount": 29502.5,
|
||||
},
|
||||
@@ -783,8 +785,13 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertEqual(si.base_grand_total, 60795)
|
||||
self.assertEqual(si.grand_total, 1215.90)
|
||||
self.assertEqual(si.rounding_adjustment, 0.01)
|
||||
self.assertEqual(si.base_rounding_adjustment, 0.50)
|
||||
# no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01
|
||||
if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01:
|
||||
self.assertEqual(si.rounding_adjustment, 0.10)
|
||||
self.assertEqual(si.base_rounding_adjustment, 5.0)
|
||||
else:
|
||||
self.assertEqual(si.rounding_adjustment, 0.0)
|
||||
self.assertEqual(si.base_rounding_adjustment, 0.0)
|
||||
|
||||
def test_outstanding(self):
|
||||
w = self.make()
|
||||
@@ -2172,7 +2179,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
def test_rounding_adjustment_2(self):
|
||||
si = create_sales_invoice(rate=400, do_not_save=True)
|
||||
for rate in [400, 600, 100]:
|
||||
for rate in [400.25, 600.30, 100.65]:
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
@@ -2198,18 +2205,19 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 1271.19)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 1272.20)
|
||||
self.assertEqual(si.grand_total, 1501.20)
|
||||
self.assertEqual(si.total_taxes_and_charges, 229)
|
||||
self.assertEqual(si.rounding_adjustment, -0.20)
|
||||
|
||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||
expected_values = {
|
||||
"_Test Account Service Tax - _TC": [0.0, 114.41],
|
||||
"_Test Account VAT - _TC": [0.0, 114.41],
|
||||
si.debit_to: [1500, 0.0],
|
||||
round_off_account: [0.01, 0.01],
|
||||
"Sales - _TC": [0.0, 1271.18],
|
||||
"_Test Account Service Tax - _TC": [0.0, 114.50],
|
||||
"_Test Account VAT - _TC": [0.0, 114.50],
|
||||
si.debit_to: [1501, 0.0],
|
||||
round_off_account: [0.20, 0.0],
|
||||
"Sales - _TC": [0.0, 1272.20],
|
||||
}
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@@ -2267,7 +2275,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 4007.16)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 4007.15)
|
||||
self.assertEqual(si.grand_total, 4488.02)
|
||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||
@@ -2280,7 +2289,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
[round_off_account, 0.02, 0.01],
|
||||
[round_off_account, 0.01, 0.0],
|
||||
]
|
||||
)
|
||||
|
||||
@@ -4005,6 +4014,223 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.submit()
|
||||
self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}")
|
||||
|
||||
def test_gl_voucher_subtype(self):
|
||||
si = create_sales_invoice()
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_type": "Sales Invoice", "voucher_no": si.name},
|
||||
pluck="voucher_subtype",
|
||||
)
|
||||
|
||||
self.assertTrue(all([x == "Sales Invoice" for x in gl_entries]))
|
||||
|
||||
si = create_sales_invoice(is_return=1, qty=-1)
|
||||
gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_type": "Sales Invoice", "voucher_no": si.name},
|
||||
pluck="voucher_subtype",
|
||||
)
|
||||
|
||||
self.assertTrue(all([x == "Credit Note" for x in gl_entries]))
|
||||
|
||||
def test_validation_on_opening_invoice_with_rounding(self):
|
||||
si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
|
||||
si.is_opening = "Yes"
|
||||
si.items[0].income_account = "Temporary Opening - _TC"
|
||||
si.save()
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
def _create_opening_roundoff_account(self, company_name):
|
||||
liability_root = frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": company_name, "root_type": "Liability", "disabled": 0},
|
||||
order_by="lft",
|
||||
limit=1,
|
||||
)[0]
|
||||
|
||||
# setup round off account
|
||||
if acc := frappe.db.exists(
|
||||
"Account",
|
||||
{
|
||||
"account_name": "Round Off for Opening",
|
||||
"account_type": "Round Off for Opening",
|
||||
"company": company_name,
|
||||
},
|
||||
):
|
||||
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
|
||||
else:
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.company = company_name
|
||||
acc.parent_account = liability_root.name
|
||||
acc.account_name = "Round Off for Opening"
|
||||
acc.account_type = "Round Off for Opening"
|
||||
acc.save()
|
||||
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
|
||||
|
||||
def test_opening_invoice_with_rounding_adjustment(self):
|
||||
si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
|
||||
si.is_opening = "Yes"
|
||||
si.items[0].income_account = "Temporary Opening - _TC"
|
||||
si.save()
|
||||
|
||||
self._create_opening_roundoff_account(si.company)
|
||||
|
||||
si.reload()
|
||||
si.submit()
|
||||
res = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": si.name, "is_opening": "Yes"},
|
||||
fields=["account", "debit", "credit", "is_opening"],
|
||||
)
|
||||
self.assertEqual(len(res), 3)
|
||||
|
||||
def _create_opening_invoice_with_inclusive_tax(self):
|
||||
si = create_sales_invoice(qty=1, rate=90, do_not_submit=True)
|
||||
si.is_opening = "Yes"
|
||||
si.items[0].income_account = "Temporary Opening - _TC"
|
||||
item_template = si.items[0].as_dict()
|
||||
item_template.name = None
|
||||
item_template.rate = 55
|
||||
si.append("items", item_template)
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Testing...",
|
||||
"rate": 5,
|
||||
"included_in_print_rate": True,
|
||||
},
|
||||
)
|
||||
# there will be 0.01 precision loss between Dr and Cr
|
||||
# caused by 'included_in_print_tax' option
|
||||
si.save()
|
||||
return si
|
||||
|
||||
def test_rounding_validation_for_opening_with_inclusive_tax(self):
|
||||
si = self._create_opening_invoice_with_inclusive_tax()
|
||||
# 'Round Off for Opening' not set in Company master
|
||||
# Ledger level validation must be thrown
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self):
|
||||
si = self._create_opening_invoice_with_inclusive_tax()
|
||||
# 'Round Off for Opening' is set in Company master
|
||||
self._create_opening_roundoff_account(si.company)
|
||||
|
||||
si.submit()
|
||||
actual = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False},
|
||||
fields=["account", "debit", "credit", "is_opening"],
|
||||
order_by="account,debit",
|
||||
)
|
||||
expected = [
|
||||
{"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"},
|
||||
{"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"},
|
||||
{"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"},
|
||||
{"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"},
|
||||
]
|
||||
self.assertEqual(len(actual), 4)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_common_party_with_different_currency_in_debtor_and_creditor(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
creditors = create_account(
|
||||
account_name="Creditors INR",
|
||||
parent_account="Accounts Payable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="INR",
|
||||
account_type="Payable",
|
||||
)
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Party INR").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "INR"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": creditors,
|
||||
}
|
||||
supp_doc.append("accounts", test_account_details)
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
def test_total_billed_amount(self):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
si.project = project.name
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -89,11 +89,14 @@
|
||||
"incoming_rate",
|
||||
"item_tax_rate",
|
||||
"actual_batch_qty",
|
||||
"actual_qty",
|
||||
"section_break_eoec",
|
||||
"serial_no",
|
||||
"column_break_ytgd",
|
||||
"batch_no",
|
||||
"available_quantity_section",
|
||||
"actual_qty",
|
||||
"column_break_ogff",
|
||||
"company_total_stock",
|
||||
"edit_references",
|
||||
"sales_order",
|
||||
"so_detail",
|
||||
@@ -675,7 +678,8 @@
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Available Qty at Warehouse",
|
||||
"label": "Qty (Warehouse)",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "actual_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
@@ -923,12 +927,30 @@
|
||||
{
|
||||
"fieldname": "column_break_ytgd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "available_quantity_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Available Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ogff",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "company_total_stock",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty (Company)",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-10-28 15:06:40.980995",
|
||||
"modified": "2024-11-25 16:27:33.287341",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -28,6 +28,7 @@ class SalesInvoiceItem(Document):
|
||||
base_rate_with_margin: DF.Currency
|
||||
batch_no: DF.Link | None
|
||||
brand: DF.Data | None
|
||||
company_total_stock: DF.Float
|
||||
conversion_factor: DF.Float
|
||||
cost_center: DF.Link
|
||||
customer_item_code: DF.Data | None
|
||||
|
||||
@@ -568,7 +568,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
tax_details.tax_on_excess_amount
|
||||
):
|
||||
supp_credit_amt = net_total - cumulative_threshold
|
||||
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
|
||||
|
||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||
tds_amount = get_lower_deduction_amount(
|
||||
|
||||
@@ -74,11 +74,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
self.assertEqual(pi.grand_total, 18000)
|
||||
|
||||
# check gl entry for the purchase invoice
|
||||
gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"])
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pi.name},
|
||||
fields=["account", "sum(debit) as debit", "sum(credit) as credit"],
|
||||
group_by="account",
|
||||
)
|
||||
self.assertEqual(len(gl_entries), 3)
|
||||
for d in gl_entries:
|
||||
if d.account == pi.credit_to:
|
||||
self.assertEqual(d.credit, 18000)
|
||||
self.assertEqual(d.credit, 20000)
|
||||
self.assertEqual(d.debit, 2000)
|
||||
elif d.account == pi.items[0].get("expense_account"):
|
||||
self.assertEqual(d.debit, 20000)
|
||||
elif d.account == pi.taxes[0].get("account_head"):
|
||||
@@ -161,6 +167,45 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_with_tax_on_excess_amount(self):
|
||||
invoices = []
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
|
||||
|
||||
# Invoice with tax and without exceeding single and cumulative thresholds
|
||||
for _ in range(2):
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True)
|
||||
pi.apply_tds = 1
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 500,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
|
||||
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
|
||||
pi1.apply_tds = 1
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 10,000
|
||||
# Threshold calculation should be only on the third invoice
|
||||
self.assertTrue(len(pi1.taxes) > 0)
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
|
||||
@@ -262,6 +262,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
pe1.paid_from = self.debtors_usd
|
||||
pe1.paid_from_account_currency = "USD"
|
||||
pe1.source_exchange_rate = 75
|
||||
pe1.paid_amount = 100
|
||||
pe1.received_amount = 75 * 100
|
||||
pe1.save()
|
||||
# Allocate payment against both invoices
|
||||
@@ -279,6 +280,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
pe2.paid_from = self.debtors_usd
|
||||
pe2.paid_from_account_currency = "USD"
|
||||
pe2.source_exchange_rate = 75
|
||||
pe2.paid_amount = 100
|
||||
pe2.received_amount = 75 * 100
|
||||
pe2.save()
|
||||
# Allocate payment against both invoices
|
||||
|
||||
@@ -7,7 +7,7 @@ import copy
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, flt, formatdate, getdate, now
|
||||
from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -234,6 +234,10 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
merge_properties = get_merge_properties(accounting_dimensions)
|
||||
|
||||
for entry in gl_map:
|
||||
if entry._skip_merge:
|
||||
merged_gl_map.append(entry)
|
||||
continue
|
||||
|
||||
entry.merge_key = get_merge_key(entry, merge_properties)
|
||||
# if there is already an entry in this account then just add it
|
||||
# to that entry
|
||||
@@ -311,66 +315,48 @@ def check_if_in_list(gle, gl_map):
|
||||
|
||||
|
||||
def toggle_debit_credit_if_negative(gl_map):
|
||||
debit_credit_field_map = {
|
||||
"debit": "credit",
|
||||
"debit_in_account_currency": "credit_in_account_currency",
|
||||
"debit_in_transaction_currency": "credit_in_transaction_currency",
|
||||
}
|
||||
|
||||
for entry in gl_map:
|
||||
# toggle debit, credit if negative entry
|
||||
if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit):
|
||||
entry.credit *= -1
|
||||
entry.debit *= -1
|
||||
for debit_field, credit_field in debit_credit_field_map.items():
|
||||
debit = flt(entry.get(debit_field))
|
||||
credit = flt(entry.get(credit_field))
|
||||
|
||||
if (
|
||||
flt(entry.debit_in_account_currency) < 0
|
||||
and flt(entry.credit_in_account_currency) < 0
|
||||
and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency)
|
||||
):
|
||||
entry.credit_in_account_currency *= -1
|
||||
entry.debit_in_account_currency *= -1
|
||||
if debit < 0 and credit < 0 and debit == credit:
|
||||
debit *= -1
|
||||
credit *= -1
|
||||
|
||||
if flt(entry.debit) < 0:
|
||||
entry.credit = flt(entry.credit) - flt(entry.debit)
|
||||
entry.debit = 0.0
|
||||
if debit < 0:
|
||||
credit = credit - debit
|
||||
debit = 0.0
|
||||
|
||||
if flt(entry.debit_in_account_currency) < 0:
|
||||
entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt(
|
||||
entry.debit_in_account_currency
|
||||
)
|
||||
entry.debit_in_account_currency = 0.0
|
||||
if credit < 0:
|
||||
debit = debit - credit
|
||||
credit = 0.0
|
||||
|
||||
if flt(entry.credit) < 0:
|
||||
entry.debit = flt(entry.debit) - flt(entry.credit)
|
||||
entry.credit = 0.0
|
||||
# update net values
|
||||
# In some scenarios net value needs to be shown in the ledger
|
||||
# This method updates net values as debit or credit
|
||||
if entry.post_net_value and debit and credit:
|
||||
if debit > credit:
|
||||
debit = debit - credit
|
||||
credit = 0.0
|
||||
|
||||
if flt(entry.credit_in_account_currency) < 0:
|
||||
entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt(
|
||||
entry.credit_in_account_currency
|
||||
)
|
||||
entry.credit_in_account_currency = 0.0
|
||||
else:
|
||||
credit = credit - debit
|
||||
debit = 0.0
|
||||
|
||||
update_net_values(entry)
|
||||
entry[debit_field] = debit
|
||||
entry[credit_field] = credit
|
||||
|
||||
return gl_map
|
||||
|
||||
|
||||
def update_net_values(entry):
|
||||
# In some scenarios net value needs to be shown in the ledger
|
||||
# This method updates net values as debit or credit
|
||||
if entry.post_net_value and entry.debit and entry.credit:
|
||||
if entry.debit > entry.credit:
|
||||
entry.debit = entry.debit - entry.credit
|
||||
entry.debit_in_account_currency = (
|
||||
entry.debit_in_account_currency - entry.credit_in_account_currency
|
||||
)
|
||||
entry.credit = 0
|
||||
entry.credit_in_account_currency = 0
|
||||
else:
|
||||
entry.credit = entry.credit - entry.debit
|
||||
entry.credit_in_account_currency = (
|
||||
entry.credit_in_account_currency - entry.debit_in_account_currency
|
||||
)
|
||||
|
||||
entry.debit = 0
|
||||
entry.debit_in_account_currency = 0
|
||||
|
||||
|
||||
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
if not from_repost:
|
||||
validate_cwip_accounts(gl_map)
|
||||
@@ -492,16 +478,36 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_
|
||||
)
|
||||
|
||||
|
||||
def has_opening_entries(gl_map: list) -> bool:
|
||||
for x in gl_map:
|
||||
if x.is_opening == "Yes":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center(
|
||||
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
||||
)
|
||||
round_off_gle = frappe._dict()
|
||||
round_off_account_exists = False
|
||||
has_opening_entry = has_opening_entries(gl_map)
|
||||
|
||||
if has_opening_entry:
|
||||
if not round_off_for_opening:
|
||||
frappe.throw(
|
||||
_("Please set '{0}' in Company: {1}").format(
|
||||
frappe.bold("Round Off for Opening"), get_link_to_form("Company", gl_map[0].company)
|
||||
)
|
||||
)
|
||||
|
||||
account = round_off_for_opening
|
||||
else:
|
||||
account = round_off_account
|
||||
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
for d in gl_map:
|
||||
if d.account == round_off_account:
|
||||
if d.account == account:
|
||||
round_off_gle = d
|
||||
if d.debit:
|
||||
debit_credit_diff -= flt(d.debit) - flt(d.credit)
|
||||
@@ -519,7 +525,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
|
||||
round_off_gle.update(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"account": account,
|
||||
"debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
||||
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
|
||||
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
||||
@@ -533,6 +539,9 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
}
|
||||
)
|
||||
|
||||
if has_opening_entry:
|
||||
round_off_gle.update({"is_opening": "Yes"})
|
||||
|
||||
update_accounting_dimensions(round_off_gle)
|
||||
if not round_off_account_exists:
|
||||
gl_map.append(round_off_gle)
|
||||
@@ -557,9 +566,9 @@ def update_accounting_dimensions(round_off_gle):
|
||||
|
||||
|
||||
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):
|
||||
round_off_account, round_off_cost_center = frappe.get_cached_value(
|
||||
"Company", company, ["round_off_account", "round_off_cost_center"]
|
||||
) or [None, None]
|
||||
round_off_account, round_off_cost_center, round_off_for_opening = frappe.get_cached_value(
|
||||
"Company", company, ["round_off_account", "round_off_cost_center", "round_off_for_opening"]
|
||||
) or [None, None, None]
|
||||
|
||||
# Use expense account as fallback
|
||||
if not round_off_account:
|
||||
@@ -574,12 +583,20 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use
|
||||
round_off_cost_center = parent_cost_center
|
||||
|
||||
if not round_off_account:
|
||||
frappe.throw(_("Please mention Round Off Account in Company"))
|
||||
frappe.throw(
|
||||
_("Please mention '{0}' in Company: {1}").format(
|
||||
frappe.bold("Round Off Account"), get_link_to_form("Company", company)
|
||||
)
|
||||
)
|
||||
|
||||
if not round_off_cost_center:
|
||||
frappe.throw(_("Please mention Round Off Cost Center in Company"))
|
||||
frappe.throw(
|
||||
_("Please mention '{0}' in Company: {1}").format(
|
||||
frappe.bold("Round Off Cost Center"), get_link_to_form("Company", company)
|
||||
)
|
||||
)
|
||||
|
||||
return round_off_account, round_off_cost_center
|
||||
return round_off_account, round_off_cost_center, round_off_for_opening
|
||||
|
||||
|
||||
def make_reverse_gl_entries(
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Purchase Invoice",
|
||||
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"label": "Total Incoming Bills",
|
||||
"modified": "2020-07-22 13:06:46.045344",
|
||||
"modified": "2024-11-20 19:08:37.043777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Total Incoming Bills",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Sales Invoice",
|
||||
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Payment Entry",
|
||||
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
|
||||
@@ -29,6 +29,12 @@ from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render_address
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render_address
|
||||
|
||||
PURCHASE_TRANSACTION_TYPES = {
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
@@ -982,10 +988,4 @@ def add_party_account(party_type, party, company, account):
|
||||
|
||||
|
||||
def render_address(address, check_permissions=True):
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render
|
||||
|
||||
return frappe.call(_render, address, check_permissions=check_permissions)
|
||||
return frappe.call(_render_address, address, check_permissions=check_permissions)
|
||||
|
||||
@@ -1013,22 +1013,29 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
self.add_column("Posting Date", fieldtype="Date")
|
||||
self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date")
|
||||
self.add_column(
|
||||
label="Party Type",
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label="Party",
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
width=180,
|
||||
)
|
||||
if self.account_type == "Receivable":
|
||||
label = _("Receivable Account")
|
||||
elif self.account_type == "Payable":
|
||||
label = _("Payable Account")
|
||||
else:
|
||||
label = _("Party Account")
|
||||
|
||||
self.add_column(
|
||||
label=self.account_type + " Account",
|
||||
label=label,
|
||||
fieldname="party_account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
@@ -1037,10 +1044,10 @@ class ReceivablePayableReport:
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
if self.account_type == "Payable":
|
||||
label = "Supplier Name"
|
||||
label = _("Supplier Name")
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
label = "Customer Name"
|
||||
label = _("Customer Name")
|
||||
fieldname = "customer_name"
|
||||
self.add_column(
|
||||
label=label,
|
||||
@@ -1066,7 +1073,7 @@ class ReceivablePayableReport:
|
||||
width=180,
|
||||
)
|
||||
|
||||
self.add_column(label="Due Date", fieldtype="Date")
|
||||
self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date")
|
||||
|
||||
if self.account_type == "Payable":
|
||||
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
|
||||
|
||||
@@ -89,7 +89,9 @@ def get_data(filters):
|
||||
& (DepreciationSchedule.schedule_date == d.posting_date)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
|
||||
asset_data.accumulated_depreciation_amount = (
|
||||
query[0]["accumulated_depreciation_amount"] if query else 0
|
||||
)
|
||||
|
||||
else:
|
||||
asset_data.accumulated_depreciation_amount += d.debit
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
@@ -101,6 +102,9 @@ def execute(filters=None):
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
return columns, data, message, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Cash Flow"] = $.extend({}, erpnext.financial_statements);
|
||||
frappe.query_reports["Cash Flow"] = $.extend(erpnext.financial_statements, {
|
||||
name_field: "section",
|
||||
parent_field: "parent_section",
|
||||
});
|
||||
|
||||
erpnext.utils.add_dimensions("Cash Flow", 10);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def execute(filters=None):
|
||||
company=filters.company,
|
||||
)
|
||||
|
||||
cash_flow_accounts = get_cash_flow_accounts()
|
||||
cash_flow_sections = get_cash_flow_accounts()
|
||||
|
||||
# compute net profit / loss
|
||||
income = get_data(
|
||||
@@ -60,14 +60,14 @@ def execute(filters=None):
|
||||
summary_data = {}
|
||||
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
|
||||
|
||||
for cash_flow_account in cash_flow_accounts:
|
||||
for cash_flow_section in cash_flow_sections:
|
||||
section_data = []
|
||||
data.append(
|
||||
{
|
||||
"account_name": cash_flow_account["section_header"],
|
||||
"parent_account": None,
|
||||
"section_name": "'" + cash_flow_section["section_header"] + "'",
|
||||
"parent_section": None,
|
||||
"indent": 0.0,
|
||||
"account": cash_flow_account["section_header"],
|
||||
"section": cash_flow_section["section_header"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -75,31 +75,40 @@ def execute(filters=None):
|
||||
# add first net income in operations section
|
||||
if net_profit_loss:
|
||||
net_profit_loss.update(
|
||||
{"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]}
|
||||
{"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]}
|
||||
)
|
||||
data.append(net_profit_loss)
|
||||
section_data.append(net_profit_loss)
|
||||
|
||||
for account in cash_flow_account["account_types"]:
|
||||
account_data = get_account_type_based_data(
|
||||
filters.company, account["account_type"], period_list, filters.accumulated_values, filters
|
||||
for row in cash_flow_section["account_types"]:
|
||||
row_data = get_account_type_based_data(
|
||||
filters.company, row["account_type"], period_list, filters.accumulated_values, filters
|
||||
)
|
||||
account_data.update(
|
||||
accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={
|
||||
"account_type": row["account_type"],
|
||||
"is_group": 0,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
row_data.update(
|
||||
{
|
||||
"account_name": account["label"],
|
||||
"account": account["label"],
|
||||
"section_name": row["label"],
|
||||
"section": row["label"],
|
||||
"indent": 1,
|
||||
"parent_account": cash_flow_account["section_header"],
|
||||
"accounts": accounts,
|
||||
"parent_section": cash_flow_section["section_header"],
|
||||
"currency": company_currency,
|
||||
}
|
||||
)
|
||||
data.append(account_data)
|
||||
section_data.append(account_data)
|
||||
data.append(row_data)
|
||||
section_data.append(row_data)
|
||||
|
||||
add_total_row_account(
|
||||
data,
|
||||
section_data,
|
||||
cash_flow_account["section_footer"],
|
||||
cash_flow_section["section_footer"],
|
||||
period_list,
|
||||
company_currency,
|
||||
summary_data,
|
||||
@@ -109,7 +118,7 @@ def execute(filters=None):
|
||||
add_total_row_account(
|
||||
data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters
|
||||
)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True)
|
||||
|
||||
chart = get_chart_data(columns, data, company_currency)
|
||||
|
||||
@@ -217,8 +226,8 @@ def get_start_date(period, accumulated_values, company):
|
||||
|
||||
def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False):
|
||||
total_row = {
|
||||
"account_name": "'" + _("{0}").format(label) + "'",
|
||||
"account": "'" + _("{0}").format(label) + "'",
|
||||
"section_name": "'" + _("{0}").format(label) + "'",
|
||||
"section": "'" + _("{0}").format(label) + "'",
|
||||
"currency": currency,
|
||||
}
|
||||
|
||||
@@ -229,7 +238,7 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
|
||||
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
|
||||
|
||||
for row in data:
|
||||
if row.get("parent_account"):
|
||||
if row.get("parent_section"):
|
||||
for period in period_list:
|
||||
key = period if consolidated else period["key"]
|
||||
total_row.setdefault(key, 0.0)
|
||||
@@ -254,13 +263,14 @@ def get_report_summary(summary_data, currency):
|
||||
|
||||
def get_chart_data(columns, data, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
print(data)
|
||||
datasets = [
|
||||
{
|
||||
"name": account.get("account").replace("'", ""),
|
||||
"values": [account.get(d.get("fieldname")) for d in columns[2:]],
|
||||
"name": section.get("section").replace("'", ""),
|
||||
"values": [section.get(d.get("fieldname")) for d in columns[2:]],
|
||||
}
|
||||
for account in data
|
||||
if account.get("parent_account") is None and account.get("currency")
|
||||
for section in data
|
||||
if section.get("parent_section") is None and section.get("currency")
|
||||
]
|
||||
datasets = datasets[:-1]
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
@@ -334,8 +335,8 @@ def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False
|
||||
|
||||
def add_total_row(out, root_type, balance_must_be, period_list, company_currency):
|
||||
total_row = {
|
||||
"account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
|
||||
"account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
|
||||
"account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'",
|
||||
"account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'",
|
||||
"currency": company_currency,
|
||||
"opening_balance": 0.0,
|
||||
}
|
||||
@@ -616,11 +617,11 @@ def get_cost_centers_with_children(cost_centers):
|
||||
return list(set(all_cost_centers))
|
||||
|
||||
|
||||
def get_columns(periodicity, period_list, accumulated_values=1, company=None):
|
||||
def get_columns(periodicity, period_list, accumulated_values=1, company=None, cash_flow=False):
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "account",
|
||||
"label": _("Account"),
|
||||
"label": _("Account") if not cash_flow else _("Section"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
"width": 300,
|
||||
@@ -668,3 +669,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list):
|
||||
filtered_summary_list.append(period)
|
||||
|
||||
return filtered_summary_list
|
||||
|
||||
|
||||
def compute_growth_view_data(data, columns):
|
||||
data_copy = copy.deepcopy(data)
|
||||
|
||||
for row_idx in range(len(data_copy)):
|
||||
for column_idx in range(1, len(columns)):
|
||||
previous_period_key = columns[column_idx - 1].get("key")
|
||||
current_period_key = columns[column_idx].get("key")
|
||||
current_period_value = data_copy[row_idx].get(current_period_key)
|
||||
previous_period_value = data_copy[row_idx].get(previous_period_key)
|
||||
annual_growth = 0
|
||||
|
||||
if current_period_value is None:
|
||||
data[row_idx][current_period_key] = None
|
||||
continue
|
||||
|
||||
if previous_period_value == 0 and current_period_value > 0:
|
||||
annual_growth = 1
|
||||
|
||||
elif previous_period_value > 0:
|
||||
annual_growth = (current_period_value - previous_period_value) / previous_period_value
|
||||
|
||||
growth_percent = round(annual_growth * 100, 2)
|
||||
|
||||
data[row_idx][current_period_key] = growth_percent
|
||||
|
||||
|
||||
def compute_margin_view_data(data, columns, accumulated_values):
|
||||
if not columns:
|
||||
return
|
||||
|
||||
if not accumulated_values:
|
||||
columns.append({"key": "total"})
|
||||
|
||||
data_copy = copy.deepcopy(data)
|
||||
|
||||
base_row = None
|
||||
for row in data_copy:
|
||||
if row.get("account_name") == _("Income"):
|
||||
base_row = row
|
||||
break
|
||||
|
||||
if not base_row:
|
||||
return
|
||||
|
||||
for row_idx in range(len(data_copy)):
|
||||
# Taking the total income from each column (for all the financial years) as the base (100%)
|
||||
row = data_copy[row_idx]
|
||||
if not row:
|
||||
continue
|
||||
|
||||
for column in columns:
|
||||
curr_period = column.get("key")
|
||||
base_value = base_row[curr_period]
|
||||
curr_value = row[curr_period]
|
||||
|
||||
if curr_value is None or base_value <= 0:
|
||||
data[row_idx][curr_period] = None
|
||||
continue
|
||||
|
||||
margin_percent = round((curr_value / base_value) * 100, 2)
|
||||
|
||||
data[row_idx][curr_period] = margin_percent
|
||||
|
||||
@@ -421,10 +421,10 @@ class GrossProfitGenerator:
|
||||
self.load_invoice_items()
|
||||
self.get_delivery_notes()
|
||||
|
||||
self.load_product_bundle()
|
||||
if filters.group_by == "Invoice":
|
||||
self.group_items_by_invoice()
|
||||
|
||||
self.load_product_bundle()
|
||||
self.load_non_stock_items()
|
||||
self.get_returned_invoice_items()
|
||||
self.process()
|
||||
@@ -440,6 +440,7 @@ class GrossProfitGenerator:
|
||||
|
||||
if grouped_by_invoice:
|
||||
buying_amount = 0
|
||||
base_amount = 0
|
||||
|
||||
for row in reversed(self.si_list):
|
||||
if self.filters.get("group_by") == "Monthly":
|
||||
@@ -480,12 +481,11 @@ class GrossProfitGenerator:
|
||||
else:
|
||||
row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision)
|
||||
|
||||
if grouped_by_invoice:
|
||||
if row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
elif row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
buying_amount = 0
|
||||
if grouped_by_invoice and row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
row.base_amount = base_amount
|
||||
buying_amount = 0
|
||||
base_amount = 0
|
||||
|
||||
# get buying rate
|
||||
if flt(row.qty):
|
||||
@@ -495,11 +495,19 @@ class GrossProfitGenerator:
|
||||
if self.is_not_invoice_row(row):
|
||||
row.buying_rate, row.base_rate = 0.0, 0.0
|
||||
|
||||
if self.is_not_invoice_row(row):
|
||||
self.update_return_invoices(row)
|
||||
|
||||
if grouped_by_invoice and row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
base_amount += row.base_amount
|
||||
|
||||
# calculate gross profit
|
||||
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
|
||||
if row.base_amount:
|
||||
row.gross_profit_percent = flt(
|
||||
(row.gross_profit / row.base_amount) * 100.0, self.currency_precision
|
||||
(row.gross_profit / row.base_amount) * 100.0,
|
||||
self.currency_precision,
|
||||
)
|
||||
else:
|
||||
row.gross_profit_percent = 0.0
|
||||
@@ -510,33 +518,29 @@ class GrossProfitGenerator:
|
||||
if self.grouped:
|
||||
self.get_average_rate_based_on_group_by()
|
||||
|
||||
def update_return_invoices(self, row):
|
||||
if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]:
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
returned_item_row.qty = 0
|
||||
returned_item_row.base_amount = 0
|
||||
|
||||
else:
|
||||
row.qty = 0
|
||||
row.base_amount = 0
|
||||
returned_item_row.qty += row.qty
|
||||
returned_item_row.base_amount += row.base_amount
|
||||
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
|
||||
def get_average_rate_based_on_group_by(self):
|
||||
for key in list(self.grouped):
|
||||
if self.filters.get("group_by") == "Invoice":
|
||||
for row in self.grouped[key]:
|
||||
if row.indent == 1.0:
|
||||
if (
|
||||
row.parent in self.returned_invoices
|
||||
and row.item_code in self.returned_invoices[row.parent]
|
||||
):
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
returned_item_row.qty = 0
|
||||
else:
|
||||
row.qty = 0
|
||||
returned_item_row.qty += row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
row.buying_amount = flt(
|
||||
flt(row.qty) * flt(row.buying_rate), self.currency_precision
|
||||
)
|
||||
if flt(row.qty) or row.base_amount:
|
||||
row = self.set_average_rate(row)
|
||||
self.grouped_data.append(row)
|
||||
elif self.filters.get("group_by") == "Payment Term":
|
||||
if self.filters.get("group_by") == "Payment Term":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
invoice_portion = 0
|
||||
|
||||
@@ -556,7 +560,7 @@ class GrossProfitGenerator:
|
||||
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
else:
|
||||
elif self.filters.get("group_by") != "Invoice":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if i == 0:
|
||||
new_row = row
|
||||
@@ -632,6 +636,7 @@ class GrossProfitGenerator:
|
||||
if packed_item.get("parent_detail_docname") == row.item_row:
|
||||
packed_item_row = row.copy()
|
||||
packed_item_row.warehouse = packed_item.warehouse
|
||||
packed_item_row.qty = packed_item.total_qty * -1
|
||||
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
|
||||
|
||||
return flt(buying_amount, self.currency_precision)
|
||||
@@ -664,7 +669,9 @@ class GrossProfitGenerator:
|
||||
else:
|
||||
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
|
||||
if (row.update_stock or row.dn_detail) and my_sle:
|
||||
parenttype, parent = row.parenttype, row.parent
|
||||
parenttype = row.parenttype
|
||||
parent = row.invoice or row.parent
|
||||
|
||||
if row.dn_detail:
|
||||
parenttype, parent = "Delivery Note", row.delivery_note
|
||||
|
||||
@@ -847,6 +854,7 @@ class GrossProfitGenerator:
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
|
||||
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
|
||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
|
||||
@@ -907,6 +915,7 @@ class GrossProfitGenerator:
|
||||
"""
|
||||
|
||||
grouped = OrderedDict()
|
||||
product_bundles = self.product_bundles.get("Sales Invoice", {})
|
||||
|
||||
for row in self.si_list:
|
||||
# initialize list with a header row for each new parent
|
||||
@@ -917,8 +926,7 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
# if item is a bundle, add it's components as seperate rows
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
bundled_items = self.get_bundle_items(row)
|
||||
if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code):
|
||||
for x in bundled_items:
|
||||
bundle_item = self.get_bundle_item_row(row, x)
|
||||
grouped.get(row.parent).append(bundle_item)
|
||||
@@ -954,47 +962,40 @@ class GrossProfitGenerator:
|
||||
"item_row": None,
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"),
|
||||
"base_net_amount": row.invoice_base_net_total,
|
||||
}
|
||||
)
|
||||
|
||||
def get_bundle_items(self, product_bundle):
|
||||
return frappe.get_all(
|
||||
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]
|
||||
)
|
||||
|
||||
def get_bundle_item_row(self, product_bundle, item):
|
||||
item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code)
|
||||
|
||||
def get_bundle_item_row(self, row, item):
|
||||
return frappe._dict(
|
||||
{
|
||||
"parent_invoice": product_bundle.item_code,
|
||||
"indent": product_bundle.indent + 1,
|
||||
"parent_invoice": row.item_code,
|
||||
"parenttype": row.parenttype,
|
||||
"indent": row.indent + 1,
|
||||
"parent": None,
|
||||
"invoice_or_item": item.item_code,
|
||||
"posting_date": product_bundle.posting_date,
|
||||
"posting_time": product_bundle.posting_time,
|
||||
"project": product_bundle.project,
|
||||
"customer": product_bundle.customer,
|
||||
"customer_group": product_bundle.customer_group,
|
||||
"posting_date": row.posting_date,
|
||||
"posting_time": row.posting_time,
|
||||
"project": row.project,
|
||||
"customer": row.customer,
|
||||
"customer_group": row.customer_group,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item_name,
|
||||
"description": description,
|
||||
"warehouse": product_bundle.warehouse,
|
||||
"item_group": item_group,
|
||||
"brand": brand,
|
||||
"dn_detail": product_bundle.dn_detail,
|
||||
"delivery_note": product_bundle.delivery_note,
|
||||
"qty": (flt(product_bundle.qty) * flt(item.qty)),
|
||||
"item_row": None,
|
||||
"is_return": product_bundle.is_return,
|
||||
"cost_center": product_bundle.cost_center,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"warehouse": item.warehouse or row.warehouse,
|
||||
"update_stock": row.update_stock,
|
||||
"item_group": "",
|
||||
"brand": "",
|
||||
"dn_detail": row.dn_detail,
|
||||
"delivery_note": row.delivery_note,
|
||||
"qty": item.total_qty * -1,
|
||||
"item_row": row.item_row,
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"invoice": row.parent,
|
||||
}
|
||||
)
|
||||
|
||||
def get_bundle_item_details(self, item_code):
|
||||
return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"])
|
||||
|
||||
def get_stock_ledger_entries(self, item_code, warehouse):
|
||||
if item_code and warehouse:
|
||||
if (item_code, warehouse) not in self.sle:
|
||||
|
||||
@@ -418,12 +418,12 @@ class TestGrossProfit(FrappeTestCase):
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 0.0,
|
||||
"avg._selling_rate": 100,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"selling_amount": 0.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
"gross_profit": 0.0,
|
||||
"gross_profit_%": 0.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
# Both items of Invoice should have '0' qty
|
||||
|
||||
@@ -7,6 +7,8 @@ from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
compute_margin_view_data,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
@@ -68,6 +70,12 @@ def execute(filters=None):
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
if filters.get("selected_view") == "Margin":
|
||||
compute_margin_view_data(data, period_list, filters.accumulated_values)
|
||||
|
||||
return columns, data, None, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
|
||||
179
erpnext/accounts/report/sales_register/test_sales_register.py
Normal file
179
erpnext/accounts/report/sales_register/test_sales_register.py
Normal file
@@ -0,0 +1,179 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.sales_register.sales_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.create_child_cost_center()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_child_cost_center(self):
|
||||
cc_name = "South Wing"
|
||||
if frappe.db.exists("Cost Center", cc_name):
|
||||
cc = frappe.get_doc("Cost Center", cc_name)
|
||||
else:
|
||||
parent = frappe.db.get_value("Cost Center", self.cost_center, "parent_cost_center")
|
||||
cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"company": self.company,
|
||||
"is_group": False,
|
||||
"parent_cost_center": parent,
|
||||
"cost_center_name": cc_name,
|
||||
}
|
||||
)
|
||||
cc = cc.save()
|
||||
self.south_cc = cc.name
|
||||
|
||||
def create_sales_invoice(self, rate=100, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=rate,
|
||||
price_list_rate=rate,
|
||||
do_not_save=1,
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def test_basic_report_output(self):
|
||||
si = self.create_sales_invoice(rate=98)
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
res = [x for x in report[1] if x.get("voucher_no") == si.name]
|
||||
|
||||
expected_result = {
|
||||
"voucher_type": si.doctype,
|
||||
"voucher_no": si.name,
|
||||
"posting_date": getdate(),
|
||||
"customer": self.customer,
|
||||
"receivable_account": self.debit_to,
|
||||
"net_total": 98.0,
|
||||
"grand_total": 98.0,
|
||||
"debit": 98.0,
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in res[0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
|
||||
def test_journal_with_cost_center_filter(self):
|
||||
je1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"voucher_type": "Journal Entry",
|
||||
"company": self.company,
|
||||
"posting_date": getdate(),
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"credit_in_account_currency": 77,
|
||||
"credit": 77,
|
||||
"is_advance": "Yes",
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit_in_account_currency": 77,
|
||||
"debit": 77,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je1.submit()
|
||||
|
||||
je2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"voucher_type": "Journal Entry",
|
||||
"company": self.company,
|
||||
"posting_date": getdate(),
|
||||
"accounts": [
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"credit_in_account_currency": 98,
|
||||
"credit": 98,
|
||||
"is_advance": "Yes",
|
||||
"cost_center": self.south_cc,
|
||||
},
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit_in_account_currency": 98,
|
||||
"debit": 98,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
je2.submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"company": self.company,
|
||||
"include_payments": True,
|
||||
"customer": self.customer,
|
||||
"cost_center": self.cost_center,
|
||||
}
|
||||
)
|
||||
report_output = execute(filters)[1]
|
||||
filtered_output = [x for x in report_output if x.get("voucher_no") == je1.name]
|
||||
self.assertEqual(len(filtered_output), 1)
|
||||
expected_result = {
|
||||
"voucher_type": je1.doctype,
|
||||
"voucher_no": je1.name,
|
||||
"posting_date": je1.posting_date,
|
||||
"customer": self.customer,
|
||||
"receivable_account": self.debit_to,
|
||||
"net_total": 77.0,
|
||||
"credit": 77.0,
|
||||
}
|
||||
result_fields = {k: v for k, v in filtered_output[0].items() if k in expected_result}
|
||||
self.assertDictEqual(result_fields, expected_result)
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"company": self.company,
|
||||
"include_payments": True,
|
||||
"customer": self.customer,
|
||||
"cost_center": self.south_cc,
|
||||
}
|
||||
)
|
||||
report_output = execute(filters)[1]
|
||||
filtered_output = [x for x in report_output if x.get("voucher_no") == je2.name]
|
||||
self.assertEqual(len(filtered_output), 1)
|
||||
expected_result = {
|
||||
"voucher_type": je2.doctype,
|
||||
"voucher_no": je2.name,
|
||||
"posting_date": je2.posting_date,
|
||||
"customer": self.customer,
|
||||
"receivable_account": self.debit_to,
|
||||
"net_total": 98.0,
|
||||
"credit": 98.0,
|
||||
}
|
||||
result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result}
|
||||
self.assertDictEqual(result_output, expected_result)
|
||||
@@ -255,7 +255,9 @@ def get_journal_entries(filters, args):
|
||||
)
|
||||
.orderby(je.posting_date, je.name, order=Order.desc)
|
||||
)
|
||||
query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True)
|
||||
query = apply_common_conditions(
|
||||
filters, query, doctype="Journal Entry", child_doctype="Journal Entry Account", payments=True
|
||||
)
|
||||
|
||||
journal_entries = query.run(as_dict=True)
|
||||
return journal_entries
|
||||
@@ -306,7 +308,9 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
|
||||
query = query.where(parent_doc.posting_date <= filters.to_date)
|
||||
|
||||
if payments:
|
||||
if filters.get("cost_center"):
|
||||
if doctype == "Journal Entry" and filters.get("cost_center"):
|
||||
query = query.where(child_doc.cost_center == filters.cost_center)
|
||||
elif filters.get("cost_center"):
|
||||
query = query.where(parent_doc.cost_center == filters.cost_center)
|
||||
else:
|
||||
if filters.get("cost_center"):
|
||||
|
||||
@@ -92,14 +92,14 @@ class TestUtils(unittest.TestCase):
|
||||
payment_entry.deductions = []
|
||||
payment_entry.save()
|
||||
|
||||
# below is the difference between base_received_amount and base_paid_amount
|
||||
self.assertEqual(payment_entry.difference_amount, -4855.0)
|
||||
# below is the difference between base_paid_amount and base_received_amount (exchange gain)
|
||||
self.assertEqual(payment_entry.deductions[0].amount, -4855.0)
|
||||
|
||||
payment_entry.target_exchange_rate = 62.9
|
||||
payment_entry.save()
|
||||
|
||||
# below is due to change in exchange rate
|
||||
self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0)
|
||||
# after changing the exchange rate, there is no exchange gain / loss
|
||||
self.assertEqual(payment_entry.deductions, [])
|
||||
|
||||
payment_entry.references = []
|
||||
self.assertEqual(payment_entry.difference_amount, 0.0)
|
||||
|
||||
@@ -630,6 +630,16 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
|
||||
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
|
||||
|
||||
rev_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if d["dr_or_cr"] == "credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
if jv_detail.get(rev_dr_or_cr):
|
||||
d["dr_or_cr"] = rev_dr_or_cr
|
||||
d["allocated_amount"] = d["allocated_amount"] * -1
|
||||
d["unadjusted_amount"] = d["unadjusted_amount"] * -1
|
||||
|
||||
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
||||
# adjust the unreconciled balance
|
||||
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Asset",
|
||||
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[]",
|
||||
"function": "Sum",
|
||||
"idx": 0,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Asset",
|
||||
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\",false]]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Asset",
|
||||
"dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
|
||||
@@ -93,7 +93,7 @@ frappe.ui.form.on("Purchase Order", {
|
||||
get_materials_from_supplier: function (frm) {
|
||||
let po_details = [];
|
||||
|
||||
if (frm.doc.supplied_items && (flt(frm.doc.per_received, 2) == 100 || frm.doc.status === "Closed")) {
|
||||
if (frm.doc.supplied_items && (flt(frm.doc.per_received) == 100 || frm.doc.status === "Closed")) {
|
||||
frm.doc.supplied_items.forEach((d) => {
|
||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||
po_details.push(d.name);
|
||||
@@ -329,8 +329,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
if (!["Closed", "Delivered"].includes(doc.status)) {
|
||||
if (
|
||||
this.frm.doc.status !== "Closed" &&
|
||||
flt(this.frm.doc.per_received, 2) < 100 &&
|
||||
flt(this.frm.doc.per_billed, 2) < 100
|
||||
flt(this.frm.doc.per_received) < 100 &&
|
||||
flt(this.frm.doc.per_billed) < 100
|
||||
) {
|
||||
if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
|
||||
this.frm.add_custom_button(__("Update Items"), () => {
|
||||
@@ -344,7 +344,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
}
|
||||
if (this.frm.has_perm("submit")) {
|
||||
if (flt(doc.per_billed, 2) < 100 || flt(doc.per_received, 2) < 100) {
|
||||
if (flt(doc.per_billed) < 100 || flt(doc.per_received) < 100) {
|
||||
if (doc.status != "On Hold") {
|
||||
this.frm.add_custom_button(
|
||||
__("Hold"),
|
||||
@@ -383,7 +383,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
if (doc.status != "Closed") {
|
||||
if (doc.status != "On Hold") {
|
||||
if (flt(doc.per_received) < 100 && allow_receipt) {
|
||||
cur_frm.add_custom_button(
|
||||
this.frm.add_custom_button(
|
||||
__("Purchase Receipt"),
|
||||
this.make_purchase_receipt,
|
||||
__("Create")
|
||||
@@ -408,14 +408,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
}
|
||||
}
|
||||
// Please do not add precision in the below flt function
|
||||
if (flt(doc.per_billed) < 100)
|
||||
cur_frm.add_custom_button(
|
||||
this.frm.add_custom_button(
|
||||
__("Purchase Invoice"),
|
||||
this.make_purchase_invoice,
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (flt(doc.per_billed, 2) < 100 && doc.status != "Delivered") {
|
||||
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
() => this.make_payment_entry(),
|
||||
@@ -423,7 +424,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
|
||||
if (flt(doc.per_billed, 2) < 100) {
|
||||
if (flt(doc.per_billed) < 100) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
|
||||
@@ -581,7 +581,7 @@ class PurchaseOrder(BuyingController):
|
||||
def update_receiving_percentage(self):
|
||||
total_qty, received_qty = 0.0, 0.0
|
||||
for item in self.items:
|
||||
received_qty += item.received_qty
|
||||
received_qty += min(item.received_qty, item.qty)
|
||||
total_qty += item.qty
|
||||
if total_qty:
|
||||
self.db_set("per_received", flt(received_qty / total_qty) * 100, update_modified=False)
|
||||
@@ -625,9 +625,11 @@ class PurchaseOrder(BuyingController):
|
||||
if not self.is_against_so():
|
||||
return
|
||||
for item in removed_items:
|
||||
prev_ordered_qty = frappe.get_cached_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty"
|
||||
prev_ordered_qty = (
|
||||
frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty")
|
||||
or 0.0
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
update_received_amount(data)
|
||||
|
||||
if not data:
|
||||
return [], [], None, []
|
||||
@@ -40,7 +41,6 @@ def get_data(filters):
|
||||
po = frappe.qb.DocType("Purchase Order")
|
||||
po_item = frappe.qb.DocType("Purchase Order Item")
|
||||
pi_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
pr_item = frappe.qb.DocType("Purchase Receipt Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(po)
|
||||
@@ -48,8 +48,6 @@ def get_data(filters):
|
||||
.on(po_item.parent == po.name)
|
||||
.left_join(pi_item)
|
||||
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
|
||||
.left_join(pr_item)
|
||||
.on((pr_item.purchase_order_item == po_item.name) & (pr_item.docstatus == 1))
|
||||
.select(
|
||||
po.transaction_date.as_("date"),
|
||||
po_item.schedule_date.as_("required_date"),
|
||||
@@ -63,7 +61,6 @@ def get_data(filters):
|
||||
(po_item.qty - po_item.received_qty).as_("pending_qty"),
|
||||
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
|
||||
po_item.base_amount.as_("amount"),
|
||||
(pr_item.base_amount).as_("received_qty_amount"),
|
||||
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
|
||||
(po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_(
|
||||
"pending_amount"
|
||||
@@ -95,6 +92,39 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def update_received_amount(data):
|
||||
pr_data = get_received_amount_data(data)
|
||||
|
||||
for row in data:
|
||||
row.received_qty_amount = flt(pr_data.get(row.name))
|
||||
|
||||
|
||||
def get_received_amount_data(data):
|
||||
pr = frappe.qb.DocType("Purchase Receipt")
|
||||
pr_item = frappe.qb.DocType("Purchase Receipt Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pr)
|
||||
.inner_join(pr_item)
|
||||
.on(pr_item.parent == pr.name)
|
||||
.select(
|
||||
pr_item.purchase_order_item,
|
||||
Sum(pr_item.base_amount).as_("received_qty_amount"),
|
||||
)
|
||||
.where((pr_item.parent == pr.name) & (pr.docstatus == 1))
|
||||
.groupby(pr_item.purchase_order_item)
|
||||
)
|
||||
|
||||
query = query.where(pr_item.purchase_order_item.isin([row.name for row in data]))
|
||||
|
||||
data = query.run()
|
||||
|
||||
if not data:
|
||||
return frappe._dict()
|
||||
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
def prepare_data(data, filters):
|
||||
completed, pending = 0, 0
|
||||
pending_field = "pending_amount"
|
||||
|
||||
@@ -1096,9 +1096,11 @@ class AccountsController(TransactionBase):
|
||||
return "Purchase Return"
|
||||
elif self.doctype == "Delivery Note" and self.is_return:
|
||||
return "Sales Return"
|
||||
elif (self.doctype == "Sales Invoice" and self.is_return) or self.doctype == "Purchase Invoice":
|
||||
elif self.doctype == "Sales Invoice" and self.is_return:
|
||||
return "Credit Note"
|
||||
elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice":
|
||||
elif self.doctype == "Sales Invoice" and self.is_debit_note:
|
||||
return "Debit Note"
|
||||
elif self.doctype == "Purchase Invoice" and self.is_return:
|
||||
return "Debit Note"
|
||||
|
||||
return self.doctype
|
||||
@@ -1296,7 +1298,11 @@ class AccountsController(TransactionBase):
|
||||
d.exchange_gain_loss = difference
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
(
|
||||
round_off_account,
|
||||
round_off_cost_center,
|
||||
round_off_for_opening,
|
||||
) = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
@@ -2459,6 +2465,12 @@ class AccountsController(TransactionBase):
|
||||
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
|
||||
primary_account_currency = get_account_currency(primary_account)
|
||||
secondary_account_currency = get_account_currency(secondary_account)
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
|
||||
# Determine if multi-currency journal entry is needed
|
||||
multi_currency = (
|
||||
primary_account_currency != default_currency or secondary_account_currency != default_currency
|
||||
)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.voucher_type = "Journal Entry"
|
||||
@@ -2483,7 +2495,7 @@ class AccountsController(TransactionBase):
|
||||
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
advance_entry.is_advance = "Yes"
|
||||
|
||||
# update dimesions
|
||||
# Update dimensions
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
@@ -2492,17 +2504,58 @@ class AccountsController(TransactionBase):
|
||||
reconcilation_entry.update(dimensions_dict)
|
||||
advance_entry.update(dimensions_dict)
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
# Calculate exchange rates if necessary
|
||||
if multi_currency:
|
||||
# Exchange rates for primary and secondary accounts
|
||||
exc_rate_primary_to_default = (
|
||||
1
|
||||
if primary_account_currency == default_currency
|
||||
else get_exchange_rate(primary_account_currency, default_currency, self.posting_date)
|
||||
)
|
||||
exc_rate_secondary_to_default = (
|
||||
1
|
||||
if secondary_account_currency == default_currency
|
||||
else get_exchange_rate(secondary_account_currency, default_currency, self.posting_date)
|
||||
)
|
||||
exc_rate_secondary_to_primary = (
|
||||
1
|
||||
if secondary_account_currency == primary_account_currency
|
||||
else get_exchange_rate(
|
||||
secondary_account_currency, primary_account_currency, self.posting_date
|
||||
)
|
||||
)
|
||||
|
||||
# Convert outstanding amount from secondary to primary account currency, if needed
|
||||
|
||||
os_in_default_currency = self.outstanding_amount * exc_rate_secondary_to_default
|
||||
os_in_primary_currency = self.outstanding_amount * exc_rate_secondary_to_primary
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
# Calculate credit and debit values for reconciliation and advance entries
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.credit = os_in_default_currency
|
||||
|
||||
advance_entry.debit_in_account_currency = os_in_primary_currency
|
||||
advance_entry.debit = os_in_default_currency
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = os_in_primary_currency
|
||||
advance_entry.credit = os_in_default_currency
|
||||
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit = os_in_default_currency
|
||||
|
||||
# Set exchange rates for entries
|
||||
reconcilation_entry.exchange_rate = exc_rate_secondary_to_default
|
||||
advance_entry.exchange_rate = exc_rate_primary_to_default
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if primary_account_currency != default_currency or secondary_account_currency != default_currency:
|
||||
jv.multi_currency = 1
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
|
||||
jv.multi_currency = multi_currency
|
||||
jv.append("accounts", reconcilation_entry)
|
||||
jv.append("accounts", advance_entry)
|
||||
|
||||
|
||||
@@ -356,14 +356,14 @@ class BuyingController(SubcontractingController):
|
||||
if not self.is_internal_transfer():
|
||||
return
|
||||
|
||||
self.set_sales_incoming_rate_for_internal_transfer()
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
)
|
||||
if allow_at_arms_length_price:
|
||||
return
|
||||
|
||||
self.set_sales_incoming_rate_for_internal_transfer()
|
||||
|
||||
for d in self.get("items"):
|
||||
d.discount_percentage = 0.0
|
||||
d.discount_amount = 0.0
|
||||
|
||||
@@ -11,7 +11,13 @@ def set_print_templates_for_item_table(doc, settings):
|
||||
"items": {
|
||||
"qty": "templates/print_formats/includes/item_table_qty.html",
|
||||
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
|
||||
}
|
||||
},
|
||||
"packed_items": {
|
||||
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
|
||||
},
|
||||
"supplied_items": {
|
||||
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
|
||||
},
|
||||
}
|
||||
|
||||
doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"]
|
||||
|
||||
@@ -415,7 +415,6 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
||||
stock_ledger_entry.batch_no,
|
||||
Sum(stock_ledger_entry.actual_qty).as_("qty"),
|
||||
)
|
||||
.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
|
||||
.where(stock_ledger_entry.is_cancelled == 0)
|
||||
.where(
|
||||
(stock_ledger_entry.item_code == filters.get("item_code"))
|
||||
@@ -428,6 +427,9 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
if not filters.get("include_expired_batches"):
|
||||
query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
|
||||
|
||||
query = query.select(
|
||||
Concat("MFG-", batch_table.manufacturing_date).as_("manufacturing_date"),
|
||||
Concat("EXP-", batch_table.expiry_date).as_("expiry_date"),
|
||||
@@ -466,7 +468,6 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
||||
bundle.batch_no,
|
||||
Sum(bundle.qty).as_("qty"),
|
||||
)
|
||||
.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
|
||||
.where(stock_ledger_entry.is_cancelled == 0)
|
||||
.where(
|
||||
(stock_ledger_entry.item_code == filters.get("item_code"))
|
||||
@@ -479,6 +480,11 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
if not filters.get("include_expired_batches"):
|
||||
bundle_query = bundle_query.where(
|
||||
(batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())
|
||||
)
|
||||
|
||||
bundle_query = bundle_query.select(
|
||||
Concat("MFG-", batch_table.manufacturing_date),
|
||||
Concat("EXP-", batch_table.expiry_date),
|
||||
|
||||
@@ -1036,7 +1036,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
||||
available_serial_nos.append(serial_no)
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
@@ -1052,7 +1052,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
||||
if batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
batch_qty = get_available_batch_qty(
|
||||
parent_doc,
|
||||
batch_no,
|
||||
|
||||
@@ -21,9 +21,15 @@ class SellingController(StockController):
|
||||
|
||||
def onload(self):
|
||||
super().onload()
|
||||
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice", "Quotation"):
|
||||
for item in self.get("items") + (self.get("packed_items") or []):
|
||||
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
|
||||
company = self.company
|
||||
|
||||
item.update(
|
||||
get_bin_details(
|
||||
item.item_code, item.warehouse, company=company, include_child_warehouses=True
|
||||
)
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
@@ -68,19 +74,13 @@ class SellingController(StockController):
|
||||
if customer:
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
|
||||
fetch_payment_terms_template = False
|
||||
if self.get("__islocal") or self.company != frappe.db.get_value(
|
||||
self.doctype, self.name, "company"
|
||||
):
|
||||
fetch_payment_terms_template = True
|
||||
|
||||
party_details = _get_party_details(
|
||||
customer,
|
||||
ignore_permissions=self.flags.ignore_permissions,
|
||||
doctype=self.doctype,
|
||||
company=self.company,
|
||||
posting_date=self.get("posting_date"),
|
||||
fetch_payment_terms_template=fetch_payment_terms_template,
|
||||
fetch_payment_terms_template=self.has_value_changed("company"),
|
||||
party_address=self.customer_address,
|
||||
shipping_address=self.shipping_address_name,
|
||||
company_address=self.get("company_address"),
|
||||
@@ -167,6 +167,9 @@ class SellingController(StockController):
|
||||
|
||||
total = 0.0
|
||||
sales_team = self.get("sales_team")
|
||||
|
||||
self.validate_sales_team(sales_team)
|
||||
|
||||
for sales_person in sales_team:
|
||||
self.round_floats_in(sales_person)
|
||||
|
||||
@@ -186,6 +189,20 @@ class SellingController(StockController):
|
||||
if sales_team and total != 100.0:
|
||||
throw(_("Total allocated percentage for sales team should be 100"))
|
||||
|
||||
def validate_sales_team(self, sales_team):
|
||||
sales_persons = [d.sales_person for d in sales_team]
|
||||
|
||||
if not sales_persons:
|
||||
return
|
||||
|
||||
sales_person_status = frappe.db.get_all(
|
||||
"Sales Person", filters={"name": ["in", sales_persons]}, fields=["name", "enabled"]
|
||||
)
|
||||
|
||||
for row in sales_person_status:
|
||||
if not row.enabled:
|
||||
frappe.throw(_("Sales Person <b>{0}</b> is disabled.").format(row.name))
|
||||
|
||||
def validate_max_discount(self):
|
||||
for d in self.get("items"):
|
||||
if d.item_code:
|
||||
@@ -358,12 +375,32 @@ class SellingController(StockController):
|
||||
return il
|
||||
|
||||
def has_product_bundle(self, item_code):
|
||||
product_bundle = frappe.qb.DocType("Product Bundle")
|
||||
return (
|
||||
frappe.qb.from_(product_bundle)
|
||||
.select(product_bundle.name)
|
||||
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
|
||||
).run()
|
||||
product_bundle_items = getattr(self, "_product_bundle_items", None)
|
||||
if product_bundle_items is None:
|
||||
self._product_bundle_items = product_bundle_items = {}
|
||||
|
||||
if item_code not in product_bundle_items:
|
||||
self._fetch_product_bundle_items(item_code)
|
||||
|
||||
return product_bundle_items[item_code]
|
||||
|
||||
def _fetch_product_bundle_items(self, item_code):
|
||||
product_bundle_items = self._product_bundle_items
|
||||
items_to_fetch = {row.item_code for row in self.items if row.item_code not in product_bundle_items}
|
||||
# fetch for requisite item_code even if it is not in items
|
||||
items_to_fetch.add(item_code)
|
||||
|
||||
items_with_product_bundle = {
|
||||
row.new_item_code
|
||||
for row in frappe.get_all(
|
||||
"Product Bundle",
|
||||
filters={"new_item_code": ("in", items_to_fetch), "disabled": 0},
|
||||
fields="new_item_code",
|
||||
)
|
||||
}
|
||||
|
||||
for item_code in items_to_fetch:
|
||||
product_bundle_items[item_code] = item_code in items_with_product_bundle
|
||||
|
||||
def get_already_delivered_qty(self, current_docname, so, so_detail):
|
||||
delivered_via_dn = frappe.db.sql(
|
||||
|
||||
@@ -839,6 +839,15 @@ class StockController(AccountsController):
|
||||
if not dimension:
|
||||
continue
|
||||
|
||||
if (
|
||||
self.doctype in ["Purchase Invoice", "Purchase Receipt"]
|
||||
and row.get("rejected_warehouse")
|
||||
and sl_dict.get("warehouse") == row.get("rejected_warehouse")
|
||||
):
|
||||
fieldname = f"rejected_{dimension.source_fieldname}"
|
||||
sl_dict[dimension.target_fieldname] = row.get(fieldname)
|
||||
continue
|
||||
|
||||
if self.doctype in [
|
||||
"Purchase Invoice",
|
||||
"Purchase Receipt",
|
||||
@@ -999,11 +1008,13 @@ class StockController(AccountsController):
|
||||
def validate_qi_presence(self, row):
|
||||
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
|
||||
if not row.quality_inspection:
|
||||
msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
|
||||
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
|
||||
row.idx, frappe.bold(row.item_code)
|
||||
)
|
||||
if self.docstatus == 1:
|
||||
frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
|
||||
frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError)
|
||||
else:
|
||||
frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
|
||||
frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue")
|
||||
|
||||
def validate_qi_submission(self, row):
|
||||
"""Check if QI is submitted on row level, during submission"""
|
||||
@@ -1012,11 +1023,13 @@ class StockController(AccountsController):
|
||||
|
||||
if not qa_docstatus == 1:
|
||||
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
|
||||
msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
|
||||
msg = _("Row #{0}: Quality Inspection {1} is not submitted for the item: {2}").format(
|
||||
row.idx, link, row.item_code
|
||||
)
|
||||
if action == "Stop":
|
||||
frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
|
||||
frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
|
||||
else:
|
||||
frappe.msgprint(_(msg), alert=True, indicator="orange")
|
||||
frappe.msgprint(msg, alert=True, indicator="orange")
|
||||
|
||||
def validate_qi_rejection(self, row):
|
||||
"""Check if QI is rejected on row level, during submission"""
|
||||
@@ -1025,11 +1038,13 @@ class StockController(AccountsController):
|
||||
|
||||
if qa_status == "Rejected":
|
||||
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
|
||||
msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
|
||||
msg = _("Row #{0}: Quality Inspection {1} was rejected for item {2}").format(
|
||||
row.idx, link, row.item_code
|
||||
)
|
||||
if action == "Stop":
|
||||
frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
|
||||
frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
|
||||
else:
|
||||
frappe.msgprint(_(msg), alert=True, indicator="orange")
|
||||
frappe.msgprint(msg, alert=True, indicator="orange")
|
||||
|
||||
def update_blanket_order(self):
|
||||
blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
|
||||
@@ -74,7 +75,7 @@ class calculate_taxes_and_totals:
|
||||
self.calculate_net_total()
|
||||
self.calculate_tax_withholding_net_total()
|
||||
self.calculate_taxes()
|
||||
self.manipulate_grand_total_for_inclusive_tax()
|
||||
self.adjust_grand_total_for_inclusive_tax()
|
||||
self.calculate_totals()
|
||||
self._cleanup()
|
||||
self.calculate_total_net_weight()
|
||||
@@ -286,7 +287,7 @@ class calculate_taxes_and_totals:
|
||||
):
|
||||
amount = flt(item.amount) - total_inclusive_tax_amount_per_qty
|
||||
|
||||
item.net_amount = flt(amount / (1 + cumulated_tax_fraction))
|
||||
item.net_amount = flt(amount / (1 + cumulated_tax_fraction), item.precision("net_amount"))
|
||||
item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate"))
|
||||
item.discount_percentage = flt(
|
||||
item.discount_percentage, item.precision("discount_percentage")
|
||||
@@ -531,7 +532,12 @@ class calculate_taxes_and_totals:
|
||||
tax.base_tax_amount = round(tax.base_tax_amount, 0)
|
||||
tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0)
|
||||
|
||||
@deprecated
|
||||
def manipulate_grand_total_for_inclusive_tax(self):
|
||||
# for backward compatablility - if in case used by an external application
|
||||
return self.adjust_grand_total_for_inclusive_tax()
|
||||
|
||||
def adjust_grand_total_for_inclusive_tax(self):
|
||||
# if fully inclusive taxes and diff
|
||||
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
||||
last_tax = self.doc.get("taxes")[-1]
|
||||
@@ -553,17 +559,21 @@ class calculate_taxes_and_totals:
|
||||
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
||||
|
||||
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
||||
self.doc.rounding_adjustment = diff
|
||||
self.doc.grand_total_diff = diff
|
||||
else:
|
||||
self.doc.grand_total_diff = 0
|
||||
|
||||
def calculate_totals(self):
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment)
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
|
||||
self.doc.get("grand_total_diff")
|
||||
)
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment),
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
|
||||
self.doc.precision("total_taxes_and_charges"),
|
||||
)
|
||||
else:
|
||||
@@ -626,8 +636,8 @@ class calculate_taxes_and_totals:
|
||||
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
|
||||
)
|
||||
|
||||
# if print_in_rate is set, we would have already calculated rounding adjustment
|
||||
self.doc.rounding_adjustment += flt(
|
||||
# rounding adjustment should always be the difference vetween grand and rounded total
|
||||
self.doc.rounding_adjustment = flt(
|
||||
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
|
||||
)
|
||||
|
||||
|
||||
@@ -807,6 +807,7 @@ class TestAccountsController(FrappeTestCase):
|
||||
|
||||
@change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1})
|
||||
def test_16_internal_transfer_at_arms_length_price(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
@@ -840,6 +841,31 @@ class TestAccountsController(FrappeTestCase):
|
||||
# rate should reset to incoming rate
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
si.update_stock = 0
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].rate = arms_length_price
|
||||
pi.items[0].warehouse = target_warehouse
|
||||
pi.items[0].from_warehouse = warehouse
|
||||
pi.save()
|
||||
|
||||
self.assertEqual(pi.items[0].rate, 100)
|
||||
self.assertEqual(pi.items[0].valuation_rate, 100)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 1)
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].rate = arms_length_price
|
||||
pi.items[0].warehouse = target_warehouse
|
||||
pi.items[0].from_warehouse = warehouse
|
||||
pi.save()
|
||||
|
||||
self.assertEqual(pi.items[0].rate, arms_length_price)
|
||||
self.assertEqual(pi.items[0].valuation_rate, 100)
|
||||
|
||||
def test_20_journal_against_sales_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Number Card",
|
||||
"document_type": "Opportunity",
|
||||
"dynamic_filters_json": "[[\"Opportunity\",\"status\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||
"filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]",
|
||||
"function": "Count",
|
||||
"idx": 0,
|
||||
|
||||
0
erpnext/edi/__init__.py
Normal file
0
erpnext/edi/__init__.py
Normal file
0
erpnext/edi/doctype/__init__.py
Normal file
0
erpnext/edi/doctype/__init__.py
Normal file
0
erpnext/edi/doctype/code_list/__init__.py
Normal file
0
erpnext/edi/doctype/code_list/__init__.py
Normal file
51
erpnext/edi/doctype/code_list/code_list.js
Normal file
51
erpnext/edi/doctype/code_list/code_list.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Code List", {
|
||||
refresh: (frm) => {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.add_custom_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(frm);
|
||||
});
|
||||
}
|
||||
},
|
||||
setup: (frm) => {
|
||||
frm.savetrash = () => {
|
||||
frm.validate_form_action("Delete");
|
||||
frappe.confirm(
|
||||
__(
|
||||
"Are you sure you want to delete {0}?<p>This action will also delete all associated Common Code documents.</p>",
|
||||
[frm.docname.bold()]
|
||||
),
|
||||
function () {
|
||||
return frappe.call({
|
||||
method: "frappe.client.delete",
|
||||
args: {
|
||||
doctype: frm.doctype,
|
||||
name: frm.docname,
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Deleting {0} and all associated Common Code documents...", [
|
||||
frm.docname,
|
||||
]),
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.utils.play_sound("delete");
|
||||
frappe.model.clear_doc(frm.doctype, frm.docname);
|
||||
window.history.back();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
frm.set_query("default_common_code", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
code_list: doc.name,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
112
erpnext/edi/doctype/code_list/code_list.json
Normal file
112
erpnext/edi/doctype/code_list/code_list.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_copy": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "prompt",
|
||||
"creation": "2024-09-29 06:55:03.920375",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"canonical_uri",
|
||||
"url",
|
||||
"default_common_code",
|
||||
"column_break_nkls",
|
||||
"version",
|
||||
"publisher",
|
||||
"publisher_id",
|
||||
"section_break_npxp",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "publisher",
|
||||
"fieldtype": "Data",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Publisher"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "version",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Version"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "canonical_uri",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Canonical URI"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_nkls",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_npxp",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "publisher_id",
|
||||
"fieldtype": "Data",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Publisher ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"label": "URL",
|
||||
"options": "URL"
|
||||
},
|
||||
{
|
||||
"description": "This value shall be used when no matching Common Code for a record is found.",
|
||||
"fieldname": "default_common_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Common Code",
|
||||
"options": "Common Code"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Common Code",
|
||||
"link_fieldname": "code_list"
|
||||
}
|
||||
],
|
||||
"modified": "2024-11-16 17:01:40.260293",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Code List",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
125
erpnext/edi/doctype/code_list/code_list.py
Normal file
125
erpnext/edi/doctype/code_list/code_list.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lxml.etree import Element
|
||||
|
||||
|
||||
class CodeList(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
canonical_uri: DF.Data | None
|
||||
default_common_code: DF.Link | None
|
||||
description: DF.SmallText | None
|
||||
publisher: DF.Data | None
|
||||
publisher_id: DF.Data | None
|
||||
title: DF.Data | None
|
||||
url: DF.Data | None
|
||||
version: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def on_trash(self):
|
||||
if not frappe.flags.in_bulk_delete:
|
||||
self.__delete_linked_docs()
|
||||
|
||||
def __delete_linked_docs(self):
|
||||
self.db_set("default_common_code", None)
|
||||
|
||||
linked_docs = frappe.get_all(
|
||||
"Common Code",
|
||||
filters={"code_list": self.name},
|
||||
fields=["name"],
|
||||
)
|
||||
|
||||
for doc in linked_docs:
|
||||
frappe.delete_doc("Common Code", doc.name)
|
||||
|
||||
def get_codes_for(self, doctype: str, name: str) -> tuple[str]:
|
||||
"""Get the applicable codes for a doctype and name"""
|
||||
return get_codes_for(self.name, doctype, name)
|
||||
|
||||
def get_docnames_for(self, doctype: str, code: str) -> tuple[str]:
|
||||
"""Get the mapped docnames for a doctype and code"""
|
||||
return get_docnames_for(self.name, doctype, code)
|
||||
|
||||
def get_default_code(self) -> str | None:
|
||||
"""Get the default common code for this code list"""
|
||||
return (
|
||||
frappe.db.get_value("Common Code", self.default_common_code, "common_code")
|
||||
if self.default_common_code
|
||||
else None
|
||||
)
|
||||
|
||||
def from_genericode(self, root: "Element"):
|
||||
"""Extract Code List details from genericode XML"""
|
||||
self.title = root.find(".//Identification/ShortName").text
|
||||
self.version = root.find(".//Identification/Version").text
|
||||
self.canonical_uri = root.find(".//CanonicalUri").text
|
||||
# optionals
|
||||
self.description = getattr(root.find(".//Identification/LongName"), "text", None)
|
||||
self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None)
|
||||
if not self.publisher:
|
||||
self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None)
|
||||
self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None)
|
||||
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
|
||||
|
||||
|
||||
def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
|
||||
"""Return the common code for a given record"""
|
||||
CommonCode = frappe.qb.DocType("Common Code")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
codes = (
|
||||
frappe.qb.from_(CommonCode)
|
||||
.join(DynamicLink)
|
||||
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
|
||||
.select(CommonCode.common_code)
|
||||
.where(
|
||||
(DynamicLink.link_doctype == doctype)
|
||||
& (DynamicLink.link_name == name)
|
||||
& (CommonCode.code_list == code_list)
|
||||
)
|
||||
.distinct()
|
||||
.orderby(CommonCode.common_code)
|
||||
).run()
|
||||
|
||||
return tuple(c[0] for c in codes) if codes else ()
|
||||
|
||||
|
||||
def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
|
||||
"""Return the record name for a given common code"""
|
||||
CommonCode = frappe.qb.DocType("Common Code")
|
||||
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
docnames = (
|
||||
frappe.qb.from_(CommonCode)
|
||||
.join(DynamicLink)
|
||||
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
|
||||
.select(DynamicLink.link_name)
|
||||
.where(
|
||||
(DynamicLink.link_doctype == doctype)
|
||||
& (CommonCode.common_code == code)
|
||||
& (CommonCode.code_list == code_list)
|
||||
)
|
||||
.distinct()
|
||||
.orderby(DynamicLink.idx)
|
||||
).run()
|
||||
|
||||
return tuple(d[0] for d in docnames) if docnames else ()
|
||||
|
||||
|
||||
def get_default_code(code_list: str) -> str | None:
|
||||
"""Return the default common code for a given code list"""
|
||||
code_id = frappe.db.get_value("Code List", code_list, "default_common_code")
|
||||
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None
|
||||
218
erpnext/edi/doctype/code_list/code_list_import.js
Normal file
218
erpnext/edi/doctype/code_list/code_list_import.js
Normal file
@@ -0,0 +1,218 @@
|
||||
frappe.provide("erpnext.edi");
|
||||
|
||||
erpnext.edi.import_genericode = function (listview_or_form) {
|
||||
let doctype = "Code List";
|
||||
let docname = undefined;
|
||||
if (listview_or_form.doc !== undefined) {
|
||||
docname = listview_or_form.doc.name;
|
||||
}
|
||||
new frappe.ui.FileUploader({
|
||||
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
|
||||
doctype: doctype,
|
||||
docname: docname,
|
||||
allow_toggle_private: false,
|
||||
allow_take_photo: false,
|
||||
on_success: function (_file_doc, r) {
|
||||
listview_or_form.refresh();
|
||||
show_column_selection_dialog(r.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function show_column_selection_dialog(context) {
|
||||
let title_description = __("If there is no title column, use the code column for the title.");
|
||||
let default_title = get_default(context.columns, ["name", "Name", "code-name", "scheme-name"]);
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "code_list_info",
|
||||
options: `<div class="text-muted">${__(
|
||||
"You are importing data for the code list:"
|
||||
)} ${frappe.utils.get_form_link(
|
||||
"Code List",
|
||||
context.code_list,
|
||||
true,
|
||||
context.code_list_title
|
||||
)}</div>`,
|
||||
},
|
||||
{
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldname: "import_column",
|
||||
label: __("Import"),
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
{
|
||||
fieldname: "title_column",
|
||||
label: __("as Title"),
|
||||
fieldtype: "Select",
|
||||
reqd: 1,
|
||||
options: context.columns,
|
||||
default: default_title,
|
||||
description: default_title ? null : title_description,
|
||||
},
|
||||
{
|
||||
fieldname: "code_column",
|
||||
label: __("as Code"),
|
||||
fieldtype: "Select",
|
||||
options: context.columns,
|
||||
reqd: 1,
|
||||
default: get_default(context.columns, ["code", "Code", "value"]),
|
||||
},
|
||||
{
|
||||
fieldname: "filters_column",
|
||||
label: __("Filter"),
|
||||
fieldtype: "Column Break",
|
||||
},
|
||||
];
|
||||
|
||||
if (context.columns.length > 2) {
|
||||
fields.splice(5, 0, {
|
||||
fieldname: "description_column",
|
||||
label: __("as Description"),
|
||||
fieldtype: "Select",
|
||||
options: [null].concat(context.columns),
|
||||
default: get_default(context.columns, [
|
||||
"description",
|
||||
"Description",
|
||||
"remark",
|
||||
__("description"),
|
||||
__("Description"),
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
// Add filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
fields.push({
|
||||
fieldname: `filter_${column}`,
|
||||
label: __("by {}", [column]),
|
||||
fieldtype: "Select",
|
||||
options: [null].concat(context.filterable_columns[column]),
|
||||
});
|
||||
}
|
||||
|
||||
fields.push(
|
||||
{
|
||||
fieldname: "preview_section",
|
||||
label: __("Preview"),
|
||||
fieldtype: "Section Break",
|
||||
},
|
||||
{
|
||||
fieldname: "preview_html",
|
||||
fieldtype: "HTML",
|
||||
}
|
||||
);
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __("Select Columns and Filters"),
|
||||
fields: fields,
|
||||
primary_action_label: __("Import"),
|
||||
size: "large", // This will make the modal wider
|
||||
primary_action(values) {
|
||||
let filters = {};
|
||||
for (let field in values) {
|
||||
if (field.startsWith("filter_") && values[field]) {
|
||||
filters[field.replace("filter_", "")] = values[field];
|
||||
}
|
||||
}
|
||||
frappe
|
||||
.xcall("erpnext.edi.doctype.code_list.code_list_import.process_genericode_import", {
|
||||
code_list_name: context.code_list,
|
||||
file_name: context.file,
|
||||
code_column: values.code_column,
|
||||
title_column: values.title_column,
|
||||
description_column: values.description_column,
|
||||
filters: filters,
|
||||
})
|
||||
.then((count) => {
|
||||
frappe.msgprint(__("Import completed. {0} common codes created.", [count]));
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
});
|
||||
|
||||
d.fields_dict.code_column.df.onchange = () => update_preview(d, context);
|
||||
d.fields_dict.title_column.df.onchange = (e) => {
|
||||
let field = d.fields_dict.title_column;
|
||||
if (!e.target.value) {
|
||||
field.df.description = title_description;
|
||||
field.refresh();
|
||||
} else {
|
||||
field.df.description = null;
|
||||
field.refresh();
|
||||
}
|
||||
update_preview(d, context);
|
||||
};
|
||||
|
||||
// Add onchange events for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
d.fields_dict[`filter_${column}`].df.onchange = () => update_preview(d, context);
|
||||
}
|
||||
|
||||
d.show();
|
||||
update_preview(d, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first key from the keys array that is found in the columns array.
|
||||
*/
|
||||
function get_default(columns, keys) {
|
||||
return keys.find((key) => columns.includes(key));
|
||||
}
|
||||
|
||||
function update_preview(dialog, context) {
|
||||
let code_column = dialog.get_value("code_column");
|
||||
let title_column = dialog.get_value("title_column");
|
||||
let description_column = dialog.get_value("description_column");
|
||||
|
||||
let html = '<table class="table table-bordered"><thead><tr>';
|
||||
if (title_column) html += `<th>${__("Title")}</th>`;
|
||||
if (code_column) html += `<th>${__("Code")}</th>`;
|
||||
if (description_column) html += `<th>${__("Description")}</th>`;
|
||||
|
||||
// Add headers for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
if (dialog.get_value(`filter_${column}`)) {
|
||||
html += `<th>${__(column)}</th>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += "</tr></thead><tbody>";
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
html += "<tr>";
|
||||
if (title_column) {
|
||||
let title = context.example_values[title_column][i] || "";
|
||||
html += `<td title="${title}">${truncate(title)}</td>`;
|
||||
}
|
||||
if (code_column) {
|
||||
let code = context.example_values[code_column][i] || "";
|
||||
html += `<td title="${code}">${truncate(code)}</td>`;
|
||||
}
|
||||
if (description_column) {
|
||||
let description = context.example_values[description_column][i] || "";
|
||||
html += `<td title="${description}">${truncate(description)}</td>`;
|
||||
}
|
||||
|
||||
// Add values for filterable columns
|
||||
for (let column in context.filterable_columns) {
|
||||
if (dialog.get_value(`filter_${column}`)) {
|
||||
let value = context.example_values[column][i] || "";
|
||||
html += `<td title="${value}">${truncate(value)}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += "</tr>";
|
||||
}
|
||||
|
||||
html += "</tbody></table>";
|
||||
|
||||
dialog.fields_dict.preview_html.$wrapper.html(html);
|
||||
}
|
||||
|
||||
function truncate(value, maxLength = 40) {
|
||||
if (typeof value !== "string") return "";
|
||||
return value.length > maxLength ? value.substring(0, maxLength - 3) + "..." : value;
|
||||
}
|
||||
140
erpnext/edi/doctype/code_list/code_list_import.py
Normal file
140
erpnext/edi/doctype/code_list/code_list_import.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
from lxml import etree
|
||||
|
||||
URL_PREFIXES = ("http://", "https://")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_genericode():
|
||||
doctype = "Code List"
|
||||
docname = frappe.form_dict.docname
|
||||
content = frappe.local.uploaded_file
|
||||
|
||||
# recover the content, if it's a link
|
||||
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
|
||||
try:
|
||||
# If it's a URL, fetch the content and make it a local file (for durable audit)
|
||||
response = requests.get(frappe.local.uploaded_file_url)
|
||||
response.raise_for_status()
|
||||
frappe.local.uploaded_file = content = response.content
|
||||
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
|
||||
frappe.local.uploaded_file_url = None
|
||||
except Exception as e:
|
||||
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
|
||||
|
||||
if file_url := frappe.local.uploaded_file_url:
|
||||
file_path = frappe.utils.file_manager.get_file_path(file_url)
|
||||
with open(file_path.encode(), mode="rb") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse the xml content
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
try:
|
||||
root = etree.fromstring(content, parser=parser)
|
||||
except Exception as e:
|
||||
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
|
||||
|
||||
# Extract the name (CanonicalVersionUri) from the parsed XML
|
||||
name = root.find(".//CanonicalVersionUri").text
|
||||
docname = docname or name
|
||||
|
||||
if frappe.db.exists(doctype, docname):
|
||||
code_list = frappe.get_doc(doctype, docname)
|
||||
if code_list.name != name:
|
||||
frappe.throw(_("The uploaded file does not match the selected Code List."))
|
||||
else:
|
||||
# Create a new Code List document with the extracted name
|
||||
code_list = frappe.new_doc(doctype)
|
||||
code_list.name = name
|
||||
|
||||
code_list.from_genericode(root)
|
||||
code_list.save()
|
||||
|
||||
# Attach the file and provide a recoverable identifier
|
||||
file_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"attached_to_doctype": "Code List",
|
||||
"attached_to_name": code_list.name,
|
||||
"folder": "Home/Attachments",
|
||||
"file_name": frappe.local.uploaded_filename,
|
||||
"file_url": frappe.local.uploaded_file_url,
|
||||
"is_private": 1,
|
||||
"content": content,
|
||||
}
|
||||
).save()
|
||||
|
||||
# Get available columns and example values
|
||||
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
|
||||
|
||||
return {
|
||||
"code_list": code_list.name,
|
||||
"code_list_title": code_list.title,
|
||||
"file": file_doc.name,
|
||||
"columns": columns,
|
||||
"example_values": example_values,
|
||||
"filterable_columns": filterable_columns,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def process_genericode_import(
|
||||
code_list_name: str,
|
||||
file_name: str,
|
||||
code_column: str,
|
||||
title_column: str | None = None,
|
||||
description_column: str | None = None,
|
||||
filters: str | None = None,
|
||||
):
|
||||
from erpnext.edi.doctype.common_code.common_code import import_genericode
|
||||
|
||||
column_map = {"code": code_column, "title": title_column, "description": description_column}
|
||||
|
||||
return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)
|
||||
|
||||
|
||||
def get_genericode_columns_and_examples(root):
|
||||
columns = []
|
||||
example_values = {}
|
||||
filterable_columns = {}
|
||||
|
||||
# Get column names
|
||||
for column in root.findall(".//Column"):
|
||||
column_id = column.get("Id")
|
||||
columns.append(column_id)
|
||||
example_values[column_id] = []
|
||||
filterable_columns[column_id] = set()
|
||||
|
||||
# Get all values and count unique occurrences
|
||||
for row in root.findall(".//SimpleCodeList/Row"):
|
||||
for value in row.findall("Value"):
|
||||
column_id = value.get("ColumnRef")
|
||||
if column_id not in columns:
|
||||
# Handle undeclared column
|
||||
columns.append(column_id)
|
||||
example_values[column_id] = []
|
||||
filterable_columns[column_id] = set()
|
||||
|
||||
simple_value = value.find("./SimpleValue")
|
||||
if simple_value is None:
|
||||
continue
|
||||
|
||||
filterable_columns[column_id].add(simple_value.text)
|
||||
|
||||
# Get example values (up to 3) and filter columns with cardinality <= 5
|
||||
for row in root.findall(".//SimpleCodeList/Row")[:3]:
|
||||
for value in row.findall("Value"):
|
||||
column_id = value.get("ColumnRef")
|
||||
simple_value = value.find("./SimpleValue")
|
||||
if simple_value is None:
|
||||
continue
|
||||
|
||||
example_values[column_id].append(simple_value.text)
|
||||
|
||||
filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}
|
||||
|
||||
return columns, example_values, filterable_columns
|
||||
8
erpnext/edi/doctype/code_list/code_list_list.js
Normal file
8
erpnext/edi/doctype/code_list/code_list_list.js
Normal file
@@ -0,0 +1,8 @@
|
||||
frappe.listview_settings["Code List"] = {
|
||||
onload: function (listview) {
|
||||
listview.page.add_inner_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(listview);
|
||||
});
|
||||
},
|
||||
hide_name_column: true,
|
||||
};
|
||||
9
erpnext/edi/doctype/code_list/test_code_list.py
Normal file
9
erpnext/edi/doctype/code_list/test_code_list.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCodeList(FrappeTestCase):
|
||||
pass
|
||||
0
erpnext/edi/doctype/common_code/__init__.py
Normal file
0
erpnext/edi/doctype/common_code/__init__.py
Normal file
8
erpnext/edi/doctype/common_code/common_code.js
Normal file
8
erpnext/edi/doctype/common_code/common_code.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Common Code", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
103
erpnext/edi/doctype/common_code/common_code.json
Normal file
103
erpnext/edi/doctype/common_code/common_code.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2024-09-29 07:01:18.133067",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"code_list",
|
||||
"title",
|
||||
"common_code",
|
||||
"description",
|
||||
"column_break_wxsw",
|
||||
"additional_data",
|
||||
"section_break_rhgh",
|
||||
"applies_to"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "code_list",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Code List",
|
||||
"options": "Code List",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Title",
|
||||
"length": 300,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_wxsw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_rhgh",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "applies_to",
|
||||
"fieldtype": "Table",
|
||||
"label": "Applies To",
|
||||
"options": "Dynamic Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "common_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Common Code",
|
||||
"length": 300,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_data",
|
||||
"fieldtype": "Code",
|
||||
"label": "Additional Data",
|
||||
"max_height": "190px",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"max_height": "60px"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-11-06 07:46:17.175687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Common Code",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "common_code,description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
114
erpnext/edi/doctype/common_code/common_code.py
Normal file
114
erpnext/edi/doctype/common_code/common_code.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import hashlib
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import get_link_to_form
|
||||
from lxml import etree
|
||||
|
||||
|
||||
class CommonCode(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink
|
||||
from frappe.types import DF
|
||||
|
||||
additional_data: DF.Code | None
|
||||
applies_to: DF.Table[DynamicLink]
|
||||
code_list: DF.Link
|
||||
common_code: DF.Data
|
||||
description: DF.SmallText | None
|
||||
title: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_distinct_references()
|
||||
|
||||
def validate_distinct_references(self):
|
||||
"""Ensure no two Common Codes of the same Code List are linked to the same document."""
|
||||
for link in self.applies_to:
|
||||
existing_links = frappe.get_all(
|
||||
"Common Code",
|
||||
filters=[
|
||||
["name", "!=", self.name],
|
||||
["code_list", "=", self.code_list],
|
||||
["Dynamic Link", "link_doctype", "=", link.link_doctype],
|
||||
["Dynamic Link", "link_name", "=", link.link_name],
|
||||
],
|
||||
fields=["name", "common_code"],
|
||||
)
|
||||
|
||||
if existing_links:
|
||||
existing_link = existing_links[0]
|
||||
frappe.throw(
|
||||
_("{0} {1} is already linked to Common Code {2}.").format(
|
||||
link.link_doctype,
|
||||
link.link_name,
|
||||
get_link_to_form("Common Code", existing_link["name"], existing_link["common_code"]),
|
||||
)
|
||||
)
|
||||
|
||||
def from_genericode(self, column_map: dict, xml_element: "etree.Element"):
|
||||
"""Populate the Common Code document from a genericode XML element
|
||||
|
||||
Args:
|
||||
column_map (dict): A mapping of column names to XML column references. Keys: code, title, description
|
||||
code (etree.Element): The XML element representing a code in the genericode file
|
||||
"""
|
||||
title_column = column_map.get("title")
|
||||
code_column = column_map["code"]
|
||||
description_column = column_map.get("description")
|
||||
|
||||
self.common_code = xml_element.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text
|
||||
|
||||
if title_column:
|
||||
simple_value_title = xml_element.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue")
|
||||
self.title = simple_value_title.text if simple_value_title is not None else self.common_code
|
||||
|
||||
if description_column:
|
||||
simple_value_descr = xml_element.find(f"./Value[@ColumnRef='{description_column}']/SimpleValue")
|
||||
self.description = simple_value_descr.text if simple_value_descr is not None else None
|
||||
|
||||
self.additional_data = etree.tostring(xml_element, encoding="unicode", pretty_print=True)
|
||||
|
||||
|
||||
def simple_hash(input_string, length=6):
|
||||
return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest()
|
||||
|
||||
|
||||
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
|
||||
"""Import genericode file and create Common Code entries"""
|
||||
file_path = frappe.utils.file_manager.get_file_path(file_name)
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
tree = etree.parse(file_path, parser=parser)
|
||||
root = tree.getroot()
|
||||
|
||||
# Construct the XPath expression
|
||||
xpath_expr = ".//SimpleCodeList/Row"
|
||||
filter_conditions = [
|
||||
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
|
||||
]
|
||||
if filter_conditions:
|
||||
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
|
||||
|
||||
elements = root.xpath(xpath_expr)
|
||||
total_elements = len(elements)
|
||||
for i, xml_element in enumerate(elements, start=1):
|
||||
common_code: "CommonCode" = frappe.new_doc("Common Code")
|
||||
common_code.code_list = code_list
|
||||
common_code.from_genericode(column_map, xml_element)
|
||||
common_code.save()
|
||||
frappe.publish_progress(i / total_elements * 100, title=_("Importing Common Codes"))
|
||||
|
||||
return total_elements
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Common Code", ["code_list", "common_code"])
|
||||
8
erpnext/edi/doctype/common_code/common_code_list.js
Normal file
8
erpnext/edi/doctype/common_code/common_code_list.js
Normal file
@@ -0,0 +1,8 @@
|
||||
frappe.listview_settings["Common Code"] = {
|
||||
onload: function (listview) {
|
||||
listview.page.add_inner_button(__("Import Genericode File"), function () {
|
||||
erpnext.edi.import_genericode(listview);
|
||||
});
|
||||
},
|
||||
hide_name_column: true,
|
||||
};
|
||||
9
erpnext/edi/doctype/common_code/test_common_code.py
Normal file
9
erpnext/edi/doctype/common_code/test_common_code.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestCommonCode(FrappeTestCase):
|
||||
pass
|
||||
@@ -35,6 +35,14 @@ doctype_js = {
|
||||
"Newsletter": "public/js/newsletter.js",
|
||||
"Contact": "public/js/contact.js",
|
||||
}
|
||||
doctype_list_js = {
|
||||
"Code List": [
|
||||
"edi/doctype/code_list/code_list_import.js",
|
||||
],
|
||||
"Common Code": [
|
||||
"edi/doctype/code_list/code_list_import.js",
|
||||
],
|
||||
}
|
||||
|
||||
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user