mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-22 10:39:41 +00:00
Compare commits
263 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4723fbfd57 | ||
|
|
ab49ee2e05 | ||
|
|
d1295d1c79 | ||
|
|
46a8500361 | ||
|
|
88ba24f1cf | ||
|
|
87bb403159 | ||
|
|
43f59da0a9 | ||
|
|
2c865bcd49 | ||
|
|
fc783f5acf | ||
|
|
24126b03dd | ||
|
|
5fa4fd8825 | ||
|
|
23fb4f348f | ||
|
|
227c912ece | ||
|
|
5bfbdb805c | ||
|
|
ec40131d4d | ||
|
|
859672c419 | ||
|
|
e8c3617628 | ||
|
|
09b21b8cb4 | ||
|
|
356da69179 | ||
|
|
bd0f11ef4f | ||
|
|
9f3dfb3d18 | ||
|
|
24b5b3c8e0 | ||
|
|
a7de8c1143 | ||
|
|
2fcab327aa | ||
|
|
f3cbbef346 | ||
|
|
339beff023 | ||
|
|
85d398efcc | ||
|
|
e09f101336 | ||
|
|
f9b52b292e | ||
|
|
8a94d7bea8 | ||
|
|
f871f08f47 | ||
|
|
313ea3983f | ||
|
|
c3e2ff2fa5 | ||
|
|
33e835c4d3 | ||
|
|
c9fa4af4fe | ||
|
|
36898f6797 | ||
|
|
b9a0f4fed8 | ||
|
|
1019d98c5a | ||
|
|
cc9b22ce9f | ||
|
|
f5135cd4a4 | ||
|
|
137ef78d96 | ||
|
|
5452f8ac4a | ||
|
|
f03e301250 | ||
|
|
c23868a14d | ||
|
|
75d785f6b6 | ||
|
|
a63a0e0d37 | ||
|
|
ddac2c59ab | ||
|
|
030df0d7e8 | ||
|
|
da6c6dcfcb | ||
|
|
cbe92fef0c | ||
|
|
3e2fa16d84 | ||
|
|
44988c8bfa | ||
|
|
2acee05a0a | ||
|
|
d258684b8d | ||
|
|
ea38a1f36a | ||
|
|
7fb5f17100 | ||
|
|
fd9fd8f46f | ||
|
|
558fac6843 | ||
|
|
71a1e50314 | ||
|
|
4f7d534cab | ||
|
|
a8a89f971d | ||
|
|
964ac05e74 | ||
|
|
a98fe7a72b | ||
|
|
599581e09d | ||
|
|
6b149f5ddf | ||
|
|
f2aa71cd6c | ||
|
|
c12d5f613a | ||
|
|
3df783bfff | ||
|
|
d73cc83b91 | ||
|
|
e46834095b | ||
|
|
2f969478e9 | ||
|
|
1633a3176c | ||
|
|
093b4f9845 | ||
|
|
440ada1bdc | ||
|
|
82cd42bbdf | ||
|
|
1446aa3636 | ||
|
|
0cdb4cb927 | ||
|
|
ea634ec230 | ||
|
|
33a9c5e88d | ||
|
|
b1e205f4d1 | ||
|
|
9838eaa332 | ||
|
|
bc9f0f8368 | ||
|
|
478b49cc49 | ||
|
|
5c8af85086 | ||
|
|
876af6dca5 | ||
|
|
657bd3712b | ||
|
|
0a166a813a | ||
|
|
aee8a23626 | ||
|
|
710771edf4 | ||
|
|
e6b97cad89 | ||
|
|
57d35ec20e | ||
|
|
1e2bea82f9 | ||
|
|
0f4c5dd26c | ||
|
|
7906c21bdf | ||
|
|
4f8a8a5e99 | ||
|
|
42c2b84b28 | ||
|
|
80f2f5cd2f | ||
|
|
4e85a79e42 | ||
|
|
55c5f30d54 | ||
|
|
cc85f5de26 | ||
|
|
15881ea718 | ||
|
|
93243b1d95 | ||
|
|
76530de786 | ||
|
|
d04396b697 | ||
|
|
e552df31d6 | ||
|
|
cbe704c73c | ||
|
|
0742e6e193 | ||
|
|
3fa392ebdf | ||
|
|
a3c2121870 | ||
|
|
aaae81f7e8 | ||
|
|
a7cc7b28c0 | ||
|
|
51c1d032ba | ||
|
|
c695959902 | ||
|
|
83f5a24520 | ||
|
|
627832fe5b | ||
|
|
78db8c0029 | ||
|
|
bd34a67871 | ||
|
|
fd63ace6c7 | ||
|
|
36fdd540c0 | ||
|
|
b81dc2bf5e | ||
|
|
7e7cc807a5 | ||
|
|
895f1bff01 | ||
|
|
53c4b5f752 | ||
|
|
2f3c3d5519 | ||
|
|
6ddff1b168 | ||
|
|
d3e29fe828 | ||
|
|
3ace876c1f | ||
|
|
8c7ed37afd | ||
|
|
9da08df3ca | ||
|
|
5b0a8fd2ce | ||
|
|
f5f6c7609a | ||
|
|
ad04610c94 | ||
|
|
1a4a329c1d | ||
|
|
411f7c3a2c | ||
|
|
c55d473c92 | ||
|
|
3d082c93bc | ||
|
|
a3e8cc6abe | ||
|
|
0dcb5a4fbb | ||
|
|
14a589eb2f | ||
|
|
2af36383cc | ||
|
|
719565c897 | ||
|
|
c18beaea6b | ||
|
|
3d5c15df0a | ||
|
|
0cf172c0da | ||
|
|
5289022351 | ||
|
|
bbce58524e | ||
|
|
725b6beff3 | ||
|
|
5333f33c21 | ||
|
|
beb5318446 | ||
|
|
35c257a7ae | ||
|
|
083382399f | ||
|
|
a05d6de2be | ||
|
|
98362d9183 | ||
|
|
e401ad5b8f | ||
|
|
9106c2fdd8 | ||
|
|
2e6ead30e0 | ||
|
|
70050081ff | ||
|
|
bb774cec58 | ||
|
|
7c549e60d6 | ||
|
|
59c314c98a | ||
|
|
0f738de5e5 | ||
|
|
8701cb73ef | ||
|
|
32f6eb7ee7 | ||
|
|
1262c322d1 | ||
|
|
aa85c3abee | ||
|
|
5d9474c98c | ||
|
|
c7c2bdd159 | ||
|
|
1913caf2b0 | ||
|
|
5aff4242e3 | ||
|
|
e7f630fc74 | ||
|
|
9697cb066c | ||
|
|
be78e17e8c | ||
|
|
dcbfd4ae1d | ||
|
|
a585a7029f | ||
|
|
7eac9cc1db | ||
|
|
a8ad608fd8 | ||
|
|
699b09846a | ||
|
|
dc2483bb92 | ||
|
|
c520a7c6c1 | ||
|
|
475c9b8450 | ||
|
|
2cce2cfdbe | ||
|
|
065465626e | ||
|
|
fc7207bcd3 | ||
|
|
3b1f0c191e | ||
|
|
ae47fb9617 | ||
|
|
16d9ca72e8 | ||
|
|
01d6c1081b | ||
|
|
7404911940 | ||
|
|
136adcb793 | ||
|
|
ea0f044c48 | ||
|
|
1f099752d2 | ||
|
|
e9dbbbe324 | ||
|
|
4b1e3c96c2 | ||
|
|
8714ec608b | ||
|
|
e7518414cb | ||
|
|
d5c82ccbab | ||
|
|
f5a7c8b0b0 | ||
|
|
de15728741 | ||
|
|
d6f9909ba5 | ||
|
|
9d950a9b77 | ||
|
|
0336e7ac45 | ||
|
|
078eb46eb8 | ||
|
|
1d21f428c1 | ||
|
|
71ee88dc79 | ||
|
|
f6b91969b2 | ||
|
|
3f5f37fa73 | ||
|
|
d4258fff9d | ||
|
|
54608bdff9 | ||
|
|
4d00d2b87b | ||
|
|
d828ea6a1a | ||
|
|
677aadde7c | ||
|
|
3a51cf4e1a | ||
|
|
7d0a094515 | ||
|
|
8f5c191136 | ||
|
|
d4421dade1 | ||
|
|
36b4f68566 | ||
|
|
ef52be2f17 | ||
|
|
2fc776d0f5 | ||
|
|
4586f03bdc | ||
|
|
96cb08dba8 | ||
|
|
b18ad94071 | ||
|
|
31ecdb2104 | ||
|
|
3b550ca419 | ||
|
|
97edadfc4b | ||
|
|
824ccfb10c | ||
|
|
02ce528501 | ||
|
|
b4706db816 | ||
|
|
f608ba55ab | ||
|
|
b18b4c1e35 | ||
|
|
1c0843caee | ||
|
|
7b158accec | ||
|
|
3cb85028d0 | ||
|
|
619550942f | ||
|
|
57925a1315 | ||
|
|
a201c56b38 | ||
|
|
44263842f5 | ||
|
|
20f9549247 | ||
|
|
644ada8225 | ||
|
|
4146bdf324 | ||
|
|
c36b40ad8c | ||
|
|
79f95ba507 | ||
|
|
5ba9fb73bb | ||
|
|
43ba79f0e5 | ||
|
|
5ce4dae204 | ||
|
|
50e0f56d18 | ||
|
|
ef7d4fe84a | ||
|
|
8f81fdba4f | ||
|
|
5434d4bb1c | ||
|
|
54d0162fef | ||
|
|
c0dfda6ccc | ||
|
|
6becaba672 | ||
|
|
92438f8dba | ||
|
|
79235cd688 | ||
|
|
32d4f96e02 | ||
|
|
1bdbcc6703 | ||
|
|
8abc1f4da9 | ||
|
|
2e2a4d9c70 | ||
|
|
e43c357c08 | ||
|
|
66349d0877 | ||
|
|
7ac39dc233 | ||
|
|
02f414e0aa | ||
|
|
b6ce9222aa | ||
|
|
a8b5bcfe02 |
16
CODEOWNERS
16
CODEOWNERS
@@ -4,22 +4,22 @@
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
||||
erpnext/assets/ @khushi8112 @deepeshgarg007
|
||||
erpnext/loan_management/ @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 @ankush
|
||||
pyproject.toml @akhilnarang
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.74.1"
|
||||
__version__ = "14.77.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -58,7 +58,7 @@ def build_conditions(process_type, account, company):
|
||||
)
|
||||
|
||||
if account:
|
||||
conditions += f"AND {deferred_account}='{account}'"
|
||||
conditions += f"AND {deferred_account}={frappe.db.escape(account)}"
|
||||
elif company:
|
||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||
|
||||
|
||||
@@ -208,8 +208,49 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-02-02 17:50:09.768835",
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Payment Request",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Payment Order",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Bank Guarantee",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Transactions",
|
||||
"link_doctype": "Bank Transaction",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Accounting",
|
||||
"link_doctype": "Payment Entry",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Accounting",
|
||||
"link_doctype": "Journal Entry",
|
||||
"link_fieldname": "bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Party",
|
||||
"link_doctype": "Customer",
|
||||
"link_fieldname": "default_bank_account"
|
||||
},
|
||||
{
|
||||
"group": "Party",
|
||||
"link_doctype": "Supplier",
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -246,4 +287,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "bank_account",
|
||||
"non_standard_fieldnames": {
|
||||
"Customer": "default_bank_account",
|
||||
"Supplier": "default_bank_account",
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Payments"),
|
||||
"items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
|
||||
},
|
||||
{"label": _("Party"), "items": ["Customer", "Supplier"]},
|
||||
{"items": ["Bank Guarantee"]},
|
||||
{"items": ["Journal Entry"]},
|
||||
],
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class BankClearance(Document):
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount) as debit,
|
||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
|
||||
@@ -84,7 +84,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "frankfurter.app/{transaction_date}"
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -129,19 +129,31 @@ def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dunning_letter_text(dunning_type, doc, language=None):
|
||||
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
|
||||
DOCTYPE = "Dunning Letter Text"
|
||||
FIELDS = ["body_text", "closing_text", "language"]
|
||||
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
|
||||
if not language:
|
||||
language = doc.get("language")
|
||||
|
||||
if language:
|
||||
filters = {"parent": dunning_type, "language": language}
|
||||
else:
|
||||
filters = {"parent": dunning_type, "is_default_language": 1}
|
||||
letter_text = frappe.db.get_value(
|
||||
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
|
||||
)
|
||||
if letter_text:
|
||||
return {
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
|
||||
)
|
||||
|
||||
if not letter_text:
|
||||
letter_text = frappe.db.get_value(
|
||||
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
|
||||
)
|
||||
|
||||
if not letter_text:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||
"language": letter_text.language,
|
||||
}
|
||||
|
||||
@@ -52,6 +52,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"
|
||||
|
||||
@@ -226,23 +241,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]:
|
||||
@@ -266,23 +281,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
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
|
||||
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
||||
for account in stock_accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
account, self.posting_date, self.company
|
||||
|
||||
@@ -146,12 +146,12 @@ class TestJournalEntry(unittest.TestCase):
|
||||
"credit_in_account_currency": 0 if diff > 0 else abs(diff),
|
||||
},
|
||||
)
|
||||
jv.insert()
|
||||
|
||||
if account_bal == stock_bal:
|
||||
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
|
||||
self.assertRaises(StockAccountInvalidTransaction, jv.insert)
|
||||
frappe.db.rollback()
|
||||
else:
|
||||
jv.insert()
|
||||
jv.submit()
|
||||
jv.cancel()
|
||||
|
||||
|
||||
@@ -15,6 +15,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) {
|
||||
@@ -154,6 +158,17 @@ frappe.ui.form.on('Payment Entry', {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
return {
|
||||
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
|
||||
filters: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||
return {
|
||||
filters: {
|
||||
@@ -171,7 +186,15 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.add_fetch(
|
||||
"payment_request",
|
||||
"outstanding_amount",
|
||||
"payment_request_outstanding",
|
||||
"Payment Entry Reference"
|
||||
);
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
erpnext.hide_company(frm);
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
@@ -184,6 +207,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
}
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
frappe.flags.allocate_payment_amount = true;
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
@@ -300,6 +324,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
payment_type: function(frm) {
|
||||
set_default_party_type(frm);
|
||||
if(frm.doc.payment_type == "Internal Transfer") {
|
||||
$.each(["party", "party_balance", "paid_from", "paid_to",
|
||||
"references", "total_allocated_amount"], function(i, field) {
|
||||
@@ -633,10 +658,9 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_value("base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate));
|
||||
|
||||
if(frm.doc.payment_type == "Pay")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
|
||||
else
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
if (frm.doc.payment_type == "Pay")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
|
||||
frm.set_paid_amount_based_on_received_amount = false;
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
@@ -654,10 +678,9 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
|
||||
if(frm.doc.payment_type == "Receive")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
|
||||
else
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
if (frm.doc.payment_type == "Receive")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true);
|
||||
else frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
@@ -801,6 +824,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
c.outstanding_amount = d.outstanding_amount;
|
||||
c.bill_no = d.bill_no;
|
||||
c.payment_term = d.payment_term;
|
||||
c.payment_term_outstanding = d.payment_term_outstanding;
|
||||
c.allocated_amount = d.allocated_amount;
|
||||
|
||||
if(!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) {
|
||||
@@ -842,10 +866,12 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
}
|
||||
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm,
|
||||
(frm.doc.payment_type=="Receive" ? frm.doc.paid_amount : frm.doc.received_amount));
|
||||
|
||||
}
|
||||
frm.events.allocate_party_amount_against_ref_docs(
|
||||
frm,
|
||||
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount,
|
||||
false
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -857,76 +883,13 @@ frappe.ui.form.on('Payment Entry', {
|
||||
return ["Sales Invoice", "Purchase Invoice"];
|
||||
},
|
||||
|
||||
allocate_party_amount_against_ref_docs: function(frm, paid_amount, paid_amount_change) {
|
||||
var total_positive_outstanding_including_order = 0;
|
||||
var total_negative_outstanding = 0;
|
||||
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
|
||||
function(d) { return flt(d.amount) }));
|
||||
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||
await frm.call("allocate_amount_to_references", {
|
||||
paid_amount: paid_amount,
|
||||
paid_amount_change: paid_amount_change,
|
||||
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||
});
|
||||
|
||||
paid_amount -= total_deductions;
|
||||
|
||||
$.each(frm.doc.references || [], function(i, row) {
|
||||
if(flt(row.outstanding_amount) > 0)
|
||||
total_positive_outstanding_including_order += flt(row.outstanding_amount);
|
||||
else
|
||||
total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
|
||||
})
|
||||
var allocated_negative_outstanding = 0;
|
||||
if (
|
||||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
|
||||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
|
||||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee")
|
||||
) {
|
||||
if(total_positive_outstanding_including_order > paid_amount) {
|
||||
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
|
||||
allocated_negative_outstanding = total_negative_outstanding < remaining_outstanding ?
|
||||
total_negative_outstanding : remaining_outstanding;
|
||||
}
|
||||
|
||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"))
|
||||
if(paid_amount > total_negative_outstanding) {
|
||||
if(total_negative_outstanding == 0) {
|
||||
frappe.msgprint(
|
||||
__("Cannot {0} {1} {2} without any negative outstanding invoice", [frm.doc.payment_type,
|
||||
(frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])
|
||||
);
|
||||
return false
|
||||
} else {
|
||||
frappe.msgprint(
|
||||
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
allocated_positive_outstanding = total_negative_outstanding - paid_amount;
|
||||
allocated_negative_outstanding = paid_amount +
|
||||
(total_positive_outstanding_including_order < allocated_positive_outstanding ?
|
||||
total_positive_outstanding_including_order : allocated_positive_outstanding)
|
||||
}
|
||||
}
|
||||
|
||||
$.each(frm.doc.references || [], function(i, row) {
|
||||
if (frappe.flags.allocate_payment_amount == 0) {
|
||||
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
|
||||
row.allocated_amount = 0;
|
||||
|
||||
} else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) {
|
||||
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
|
||||
row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ?
|
||||
allocated_positive_outstanding : row.outstanding_amount;
|
||||
allocated_positive_outstanding -= flt(row.allocated_amount);
|
||||
|
||||
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
|
||||
row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ?
|
||||
-1*allocated_negative_outstanding : row.outstanding_amount;
|
||||
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
frm.refresh_fields()
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
},
|
||||
|
||||
@@ -1409,6 +1372,62 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
return current_tax_amount;
|
||||
},
|
||||
|
||||
cost_center: function (frm) {
|
||||
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
date: frm.doc.posting_date,
|
||||
paid_from: frm.doc.paid_from,
|
||||
paid_to: frm.doc.paid_to,
|
||||
ptype: frm.doc.party_type,
|
||||
pty: frm.doc.party,
|
||||
cost_center: frm.doc.cost_center,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
frm.set_value(
|
||||
"paid_from_account_balance",
|
||||
r.message.paid_from_account_balance
|
||||
);
|
||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||
frm.set_value("party_balance", r.message.party_balance);
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
after_save: function (frm) {
|
||||
const { matched_payment_requests } = frappe.last_response;
|
||||
if (!matched_payment_requests) return;
|
||||
|
||||
const COLUMN_LABEL = [
|
||||
[__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")],
|
||||
];
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Unset Matched Payment Request"),
|
||||
message: COLUMN_LABEL.concat(matched_payment_requests),
|
||||
as_table: true,
|
||||
wide: true,
|
||||
primary_action: {
|
||||
label: __("Allocate Payment Request"),
|
||||
action() {
|
||||
frappe.hide_msgprint();
|
||||
frm.call("set_matched_payment_requests", { matched_payment_requests }, () => {
|
||||
frm.dirty();
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1495,35 +1514,18 @@ frappe.ui.form.on('Payment Entry Deduction', {
|
||||
|
||||
deductions_remove: function(frm) {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
})
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
cost_center: function(frm){
|
||||
if (frm.doc.posting_date && (frm.doc.paid_from||frm.doc.paid_to)) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
date: frm.doc.posting_date,
|
||||
paid_from: frm.doc.paid_from,
|
||||
paid_to: frm.doc.paid_to,
|
||||
ptype: frm.doc.party_type,
|
||||
pty: frm.doc.party,
|
||||
cost_center: frm.doc.cost_center
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message) {
|
||||
frappe.run_serially([
|
||||
() => {
|
||||
frm.set_value("paid_from_account_balance", r.message.paid_from_account_balance);
|
||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||
frm.set_value("party_balance", r.message.party_balance);
|
||||
}
|
||||
]);
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.query_builder import Tuple
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
from frappe.utils.data import comma_and, fmt_money
|
||||
from pypika.functions import Sum
|
||||
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
|
||||
from pypika.functions import Coalesce, Sum
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -91,13 +93,17 @@ class PaymentEntry(AccountsController):
|
||||
self.set_tax_withholding()
|
||||
self.set_status()
|
||||
|
||||
def before_save(self):
|
||||
self.set_matched_unset_payment_requests_to_response()
|
||||
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
self.make_gl_entries()
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
self.update_payment_schedule()
|
||||
self.update_payment_requests()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -115,30 +121,34 @@ class PaymentEntry(AccountsController):
|
||||
super().on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.set_payment_req_status()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def set_payment_req_status(self):
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
|
||||
def update_payment_requests(self, cancel=False):
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
update_payment_requests_as_per_pe_references,
|
||||
)
|
||||
|
||||
update_payment_req_status(self, None)
|
||||
update_payment_requests_as_per_pe_references(self.references, cancel=cancel)
|
||||
|
||||
def update_outstanding_amounts(self):
|
||||
self.set_missing_ref_details(force=True)
|
||||
|
||||
def validate_duplicate_entry(self):
|
||||
reference_names = []
|
||||
reference_names = set()
|
||||
for d in self.get("references"):
|
||||
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
|
||||
key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request)
|
||||
if key in reference_names:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Duplicate entry in References {1} {2}").format(
|
||||
d.idx, d.reference_doctype, d.reference_name
|
||||
)
|
||||
)
|
||||
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
|
||||
|
||||
reference_names.add(key)
|
||||
|
||||
def set_bank_account_data(self):
|
||||
if self.bank_account:
|
||||
@@ -164,6 +174,8 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
self.validate_allocated_amount_as_per_payment_request()
|
||||
|
||||
if self.party_type in ("Customer", "Supplier"):
|
||||
self.validate_allocated_amount_with_latest_data()
|
||||
else:
|
||||
@@ -176,6 +188,27 @@ class PaymentEntry(AccountsController):
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def validate_allocated_amount_as_per_payment_request(self):
|
||||
"""
|
||||
Allocated amount should not be greater than the outstanding amount of the Payment Request.
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references)
|
||||
|
||||
if not pr_outstanding_amounts:
|
||||
return
|
||||
|
||||
for ref in self.references:
|
||||
if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]:
|
||||
frappe.throw(
|
||||
msg=_(
|
||||
"Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}"
|
||||
).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)),
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
|
||||
def term_based_allocation_enabled_for_reference(
|
||||
self, reference_doctype: str, reference_name: str
|
||||
) -> bool:
|
||||
@@ -1422,6 +1455,380 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
return current_tax_fraction
|
||||
|
||||
def set_matched_unset_payment_requests_to_response(self):
|
||||
"""
|
||||
Find matched Payment Requests for those references which have no Payment Request set.\n
|
||||
And set to `frappe.response` to show in the frontend for allocation.
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
matched_payment_requests = get_matched_payment_request_of_references(
|
||||
[row for row in self.references if not row.payment_request]
|
||||
)
|
||||
|
||||
if not matched_payment_requests:
|
||||
return
|
||||
|
||||
frappe.response["matched_payment_requests"] = matched_payment_requests
|
||||
|
||||
@frappe.whitelist()
|
||||
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
|
||||
"""
|
||||
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
|
||||
:param paid_amount: Paid Amount / Received Amount.
|
||||
:param paid_amount_change: Flag to check if `Paid Amount` is changed or not.
|
||||
:param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag)
|
||||
"""
|
||||
if not self.references:
|
||||
return
|
||||
|
||||
if not allocate_payment_amount:
|
||||
for ref in self.references:
|
||||
ref.allocated_amount = 0
|
||||
return
|
||||
|
||||
# calculating outstanding amounts
|
||||
precision = self.precision("paid_amount")
|
||||
total_positive_outstanding_including_order = 0
|
||||
total_negative_outstanding = 0
|
||||
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
||||
|
||||
for ref in self.references:
|
||||
reference_outstanding_amount = ref.outstanding_amount
|
||||
abs_outstanding_amount = abs(reference_outstanding_amount)
|
||||
|
||||
if reference_outstanding_amount > 0:
|
||||
total_positive_outstanding_including_order += abs_outstanding_amount
|
||||
else:
|
||||
total_negative_outstanding += abs_outstanding_amount
|
||||
|
||||
# calculating allocated outstanding amounts
|
||||
allocated_negative_outstanding = 0
|
||||
allocated_positive_outstanding = 0
|
||||
|
||||
# checking party type and payment type
|
||||
if (self.payment_type == "Receive" and self.party_type == "Customer") or (
|
||||
self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee")
|
||||
):
|
||||
if total_positive_outstanding_including_order > paid_amount:
|
||||
remaining_outstanding = flt(
|
||||
total_positive_outstanding_including_order - paid_amount, precision
|
||||
)
|
||||
allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding)
|
||||
|
||||
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||
|
||||
elif self.party_type in ("Supplier", "Employee"):
|
||||
if paid_amount > total_negative_outstanding:
|
||||
if total_negative_outstanding == 0:
|
||||
frappe.msgprint(
|
||||
_("Cannot {0} from {2} without any negative outstanding invoice").format(
|
||||
self.payment_type,
|
||||
self.party_type,
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.msgprint(
|
||||
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
|
||||
total_negative_outstanding
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
else:
|
||||
allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision)
|
||||
allocated_negative_outstanding = paid_amount + min(
|
||||
total_positive_outstanding_including_order, allocated_positive_outstanding
|
||||
)
|
||||
|
||||
# inner function to set `allocated_amount` to those row which have no PR
|
||||
def _allocation_to_unset_pr_row(
|
||||
row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding
|
||||
):
|
||||
if outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||
row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount)
|
||||
allocated_positive_outstanding = flt(
|
||||
allocated_positive_outstanding - row.allocated_amount, precision
|
||||
)
|
||||
elif outstanding_amount < 0 and allocated_negative_outstanding:
|
||||
row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1
|
||||
allocated_negative_outstanding = flt(
|
||||
allocated_negative_outstanding - abs(row.allocated_amount), precision
|
||||
)
|
||||
return allocated_positive_outstanding, allocated_negative_outstanding
|
||||
|
||||
# allocate amount based on `paid_amount` is changed or not
|
||||
if not paid_amount_change:
|
||||
for ref in self.references:
|
||||
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||
ref,
|
||||
ref.outstanding_amount,
|
||||
allocated_positive_outstanding,
|
||||
allocated_negative_outstanding,
|
||||
)
|
||||
|
||||
allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount"))
|
||||
|
||||
else:
|
||||
payment_request_outstanding_amounts = (
|
||||
get_payment_request_outstanding_set_in_references(self.references) or {}
|
||||
)
|
||||
references_outstanding_amounts = get_references_outstanding_amount(self.references) or {}
|
||||
remaining_references_allocated_amounts = references_outstanding_amounts.copy()
|
||||
|
||||
# Re allocate amount to those references which have PR set (Higher priority)
|
||||
for ref in self.references:
|
||||
if not ref.payment_request:
|
||||
continue
|
||||
|
||||
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
reference_outstanding_amount = references_outstanding_amounts[key]
|
||||
pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request]
|
||||
|
||||
if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||
# allocate amount according to outstanding amounts
|
||||
outstanding_amounts = (
|
||||
allocated_positive_outstanding,
|
||||
reference_outstanding_amount,
|
||||
pr_outstanding_amount,
|
||||
)
|
||||
|
||||
ref.allocated_amount = min(outstanding_amounts)
|
||||
|
||||
# update amounts to track allocation
|
||||
allocated_amount = ref.allocated_amount
|
||||
allocated_positive_outstanding = flt(
|
||||
allocated_positive_outstanding - allocated_amount, precision
|
||||
)
|
||||
remaining_references_allocated_amounts[key] = flt(
|
||||
remaining_references_allocated_amounts[key] - allocated_amount, precision
|
||||
)
|
||||
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||
)
|
||||
|
||||
elif reference_outstanding_amount < 0 and allocated_negative_outstanding:
|
||||
# allocate amount according to outstanding amounts
|
||||
outstanding_amounts = (
|
||||
allocated_negative_outstanding,
|
||||
abs(reference_outstanding_amount),
|
||||
pr_outstanding_amount,
|
||||
)
|
||||
|
||||
ref.allocated_amount = min(outstanding_amounts) * -1
|
||||
|
||||
# update amounts to track allocation
|
||||
allocated_amount = abs(ref.allocated_amount)
|
||||
allocated_negative_outstanding = flt(
|
||||
allocated_negative_outstanding - allocated_amount, precision
|
||||
)
|
||||
remaining_references_allocated_amounts[key] += allocated_amount # negative amount
|
||||
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||
)
|
||||
# Re allocate amount to those references which have no PR (Lower priority)
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||
reference_outstanding_amount = remaining_references_allocated_amounts[key]
|
||||
|
||||
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||
ref,
|
||||
reference_outstanding_amount,
|
||||
allocated_positive_outstanding,
|
||||
allocated_negative_outstanding,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_matched_payment_requests(self, matched_payment_requests):
|
||||
"""
|
||||
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
|
||||
:param matched_payment_requests: List of tuple of matched Payment Requests.
|
||||
|
||||
---
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
if not self.references or not matched_payment_requests:
|
||||
return
|
||||
|
||||
if isinstance(matched_payment_requests, str):
|
||||
matched_payment_requests = json.loads(matched_payment_requests)
|
||||
|
||||
# modify matched_payment_requests
|
||||
# like (reference_doctype, reference_name, allocated_amount): payment_request
|
||||
payment_requests = {}
|
||||
|
||||
for row in matched_payment_requests:
|
||||
key = tuple(row[:3])
|
||||
payment_requests[key] = row[3]
|
||||
|
||||
for ref in self.references:
|
||||
if ref.payment_request:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount)
|
||||
|
||||
if key in payment_requests:
|
||||
ref.payment_request = payment_requests[key]
|
||||
del payment_requests[key] # to avoid duplicate allocation
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
# to fetch matched rows
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name, row.allocated_amount)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
# query to group by reference_doctype, reference_name, outstanding_amount
|
||||
subquery = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(
|
||||
PR.reference_doctype,
|
||||
PR.reference_name,
|
||||
PR.outstanding_amount.as_("allocated_amount"),
|
||||
PR.name.as_("payment_request"),
|
||||
Count("*").as_("count"),
|
||||
)
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||
)
|
||||
|
||||
# query to fetch matched rows which are single
|
||||
matched_prs = (
|
||||
frappe.qb.from_(subquery)
|
||||
.select(
|
||||
subquery.reference_doctype,
|
||||
subquery.reference_name,
|
||||
subquery.allocated_amount,
|
||||
subquery.payment_request,
|
||||
)
|
||||
.where(subquery.count == 1)
|
||||
.run()
|
||||
)
|
||||
|
||||
return matched_prs if matched_prs else None
|
||||
|
||||
|
||||
def get_references_outstanding_amount(references=None):
|
||||
"""
|
||||
Fetch accurate outstanding amount of `References`.\n
|
||||
- If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`.
|
||||
- If `Payment Term` is not set, then fetch outstanding amount from `References` it self.
|
||||
|
||||
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {}
|
||||
refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {}
|
||||
|
||||
return {**refs_with_payment_term, **refs_without_payment_term}
|
||||
|
||||
|
||||
def get_outstanding_of_references_with_payment_term(references=None):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have `Payment Term` set.\n
|
||||
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name, row.payment_term)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.payment_term
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PS = frappe.qb.DocType("Payment Schedule")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PS)
|
||||
.select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding)
|
||||
.where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs))
|
||||
).run(as_dict=True)
|
||||
|
||||
if not response:
|
||||
return
|
||||
|
||||
return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response}
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Note: `None` is used for allocation of `Payment Request`
|
||||
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
outstanding_amounts = {}
|
||||
|
||||
for ref in references:
|
||||
if ref.payment_term:
|
||||
continue
|
||||
|
||||
key = (ref.reference_doctype, ref.reference_name, None)
|
||||
|
||||
if key not in outstanding_amounts:
|
||||
outstanding_amounts[key] = ref.outstanding_amount
|
||||
|
||||
return outstanding_amounts
|
||||
|
||||
|
||||
def get_payment_request_outstanding_set_in_references(references=None):
|
||||
"""
|
||||
Fetch outstanding amount of `Payment Request` which are set in `References`.\n
|
||||
Example: {payment_request: outstanding_amount, ...}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
referenced_payment_requests = {row.payment_request for row in references if row.payment_request}
|
||||
|
||||
if not referenced_payment_requests:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(PR.name, PR.outstanding_amount)
|
||||
.where(PR.name.isin(referenced_payment_requests))
|
||||
).run()
|
||||
|
||||
return dict(response) if response else None
|
||||
|
||||
|
||||
def validate_inclusive_tax(tax, doc):
|
||||
def _on_previous_row_error(row_range):
|
||||
@@ -1863,7 +2270,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
|
||||
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
|
||||
party_name = frappe.db.get_value(party_type, party, _party_name)
|
||||
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
|
||||
party_balance = get_balance_on(
|
||||
party_type=party_type, party=party, company=company, cost_center=cost_center
|
||||
)
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
bank_account = get_party_bank_account(party_type, party)
|
||||
|
||||
@@ -2010,6 +2419,8 @@ def get_payment_entry(
|
||||
party_type=None,
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
ignore_permissions=False,
|
||||
created_from_payment_request=False,
|
||||
):
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
@@ -2160,9 +2571,179 @@ def get_payment_entry(
|
||||
|
||||
pe.set_difference_amount()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
if not created_from_payment_request:
|
||||
allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount"))
|
||||
|
||||
return pe
|
||||
|
||||
|
||||
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
|
||||
|
||||
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
refs = {
|
||||
(row.reference_doctype, row.reference_name)
|
||||
for row in references
|
||||
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||
}
|
||||
|
||||
if not refs:
|
||||
return
|
||||
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
frappe.qb.from_(PR)
|
||||
.select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||
).run(as_dict=True)
|
||||
|
||||
if not response:
|
||||
return
|
||||
|
||||
reference_payment_requests = {}
|
||||
|
||||
for row in response:
|
||||
key = (row.reference_doctype, row.reference_name)
|
||||
|
||||
if key not in reference_payment_requests:
|
||||
reference_payment_requests[key] = {row.name: row.outstanding_amount}
|
||||
else:
|
||||
reference_payment_requests[key][row.name] = row.outstanding_amount
|
||||
|
||||
return reference_payment_requests
|
||||
|
||||
|
||||
def allocate_open_payment_requests_to_references(references=None, precision=None):
|
||||
"""
|
||||
Allocate unpaid Payment Requests to the references. \n
|
||||
---
|
||||
- Allocation based on below factors
|
||||
- Reference Allocated Amount
|
||||
- Reference Outstanding Amount (With Payment Terms or without Payment Terms)
|
||||
- Reference Payment Request's outstanding amount
|
||||
---
|
||||
- Allocation based on below scenarios
|
||||
- Reference's Allocated Amount == Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- This PR will not be allocated further
|
||||
- Reference's Allocated Amount < Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- Reduce the PR's outstanding amount by the allocated amount
|
||||
- This PR can be allocated further
|
||||
- Reference's Allocated Amount > Payment Request's Outstanding Amount
|
||||
- Allocate the Payment Request to the reference
|
||||
- Reduce Allocated Amount of the reference by the PR's outstanding amount
|
||||
- Create a new row for the remaining amount until the Allocated Amount is 0
|
||||
- Allocate PR if available
|
||||
---
|
||||
- Note:
|
||||
- Priority is given to the first Payment Request of respective references.
|
||||
- Single Reference can have multiple rows.
|
||||
- With Payment Terms or without Payment Terms
|
||||
- With Payment Request or without Payment Request
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
# get all unpaid payment requests for the references
|
||||
references_open_payment_requests = get_open_payment_requests_for_references(references)
|
||||
|
||||
if not references_open_payment_requests:
|
||||
return
|
||||
|
||||
if not precision:
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
# to manage new rows
|
||||
row_number = 1
|
||||
MOVE_TO_NEXT_ROW = 1
|
||||
TO_SKIP_NEW_ROW = 2
|
||||
|
||||
while row_number <= len(references):
|
||||
row = references[row_number - 1]
|
||||
reference_key = (row.reference_doctype, row.reference_name)
|
||||
|
||||
# update the idx to maintain the order
|
||||
row.idx = row_number
|
||||
|
||||
# unpaid payment requests for the reference
|
||||
reference_payment_requests = references_open_payment_requests.get(reference_key)
|
||||
|
||||
if not reference_payment_requests:
|
||||
row_number += MOVE_TO_NEXT_ROW # to move to next reference row
|
||||
continue
|
||||
|
||||
# get the first payment request and its outstanding amount
|
||||
payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items()))
|
||||
allocated_amount = row.allocated_amount
|
||||
|
||||
# allocate the payment request to the reference and PR's outstanding amount
|
||||
row.payment_request = payment_request
|
||||
|
||||
if pr_outstanding_amount == allocated_amount:
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
elif pr_outstanding_amount > allocated_amount:
|
||||
# reduce the outstanding amount of the payment request
|
||||
reference_payment_requests[payment_request] -= allocated_amount
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
else:
|
||||
# split the reference row to allocate the remaining amount
|
||||
del reference_payment_requests[payment_request]
|
||||
row.allocated_amount = pr_outstanding_amount
|
||||
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||
|
||||
# set the remaining amount to the next row
|
||||
while allocated_amount:
|
||||
# create a new row for the remaining amount
|
||||
new_row = frappe.copy_doc(row)
|
||||
references.insert(row_number, new_row)
|
||||
|
||||
# get the first payment request and its outstanding amount
|
||||
payment_request, pr_outstanding_amount = next(
|
||||
iter(reference_payment_requests.items()), (None, None)
|
||||
)
|
||||
|
||||
# update new row
|
||||
new_row.idx = row_number + 1
|
||||
new_row.payment_request = payment_request
|
||||
new_row.allocated_amount = min(
|
||||
pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount
|
||||
)
|
||||
|
||||
if not payment_request or not pr_outstanding_amount:
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
elif pr_outstanding_amount == allocated_amount:
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
elif pr_outstanding_amount > allocated_amount:
|
||||
reference_payment_requests[payment_request] -= allocated_amount
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
break
|
||||
|
||||
else:
|
||||
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||
del reference_payment_requests[payment_request]
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
|
||||
|
||||
def update_accounting_dimensions(pe, doc):
|
||||
"""
|
||||
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
"due_date",
|
||||
"bill_no",
|
||||
"payment_term",
|
||||
"payment_term_outstanding",
|
||||
"column_break_4",
|
||||
"total_amount",
|
||||
"outstanding_amount",
|
||||
"allocated_amount",
|
||||
"exchange_rate",
|
||||
"exchange_gain_loss"
|
||||
"exchange_gain_loss",
|
||||
"payment_request",
|
||||
"payment_request_outstanding"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -101,12 +104,33 @@
|
||||
"label": "Exchange Gain/Loss",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_request",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Request",
|
||||
"options": "Payment Request"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_term",
|
||||
"fieldname": "payment_term_outstanding",
|
||||
"fieldtype": "Float",
|
||||
"label": "Payment Term Outstanding",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_request && doc.payment_request_outstanding",
|
||||
"fieldname": "payment_request_outstanding",
|
||||
"fieldtype": "Float",
|
||||
"is_virtual": 1,
|
||||
"label": "Payment Request Outstanding",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-12 12:31:44.919895",
|
||||
"modified": "2024-09-16 18:11:50.019343",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PaymentEntryReference(Document):
|
||||
pass
|
||||
@property
|
||||
def payment_request_outstanding(self):
|
||||
if not self.payment_request:
|
||||
return
|
||||
|
||||
return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount")
|
||||
|
||||
@@ -144,12 +144,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}%%"))
|
||||
@@ -164,7 +166,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"),
|
||||
@@ -256,6 +258,7 @@ class PaymentReconciliation(Document):
|
||||
"posting_date": inv.posting_date,
|
||||
"currency": inv.currency,
|
||||
"cost_center": inv.cost_center,
|
||||
"remarks": inv.remarks,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -298,6 +301,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):
|
||||
|
||||
@@ -615,6 +615,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"
|
||||
@@ -937,6 +973,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(
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"amount",
|
||||
"difference_amount",
|
||||
"sec_break1",
|
||||
"remark",
|
||||
"remarks",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"cost_center"
|
||||
@@ -74,12 +74,6 @@
|
||||
"fieldname": "sec_break1",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Remark",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
@@ -105,12 +99,18 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Remarks",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-17 17:33:34.818530",
|
||||
"modified": "2024-10-29 16:24:43.021230",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Payment",
|
||||
|
||||
@@ -48,8 +48,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
}
|
||||
|
||||
if (
|
||||
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
|
||||
frm.doc.status == "Initiated"
|
||||
frm.doc.payment_request_type == "Outward" &&
|
||||
["Initiated", "Partially Paid"].includes(frm.doc.status)
|
||||
) {
|
||||
frm.add_custom_button(__("Create Payment Entry"), function () {
|
||||
frappe.call({
|
||||
|
||||
@@ -18,9 +18,11 @@
|
||||
"reference_name",
|
||||
"transaction_details",
|
||||
"grand_total",
|
||||
"currency",
|
||||
"is_a_subscription",
|
||||
"column_break_18",
|
||||
"currency",
|
||||
"outstanding_amount",
|
||||
"party_account_currency",
|
||||
"subscription_section",
|
||||
"subscription_plans",
|
||||
"bank_account_details",
|
||||
@@ -68,6 +70,7 @@
|
||||
{
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"in_preview": 1,
|
||||
"label": "Transaction Date"
|
||||
},
|
||||
{
|
||||
@@ -132,7 +135,8 @@
|
||||
"no_copy": 1,
|
||||
"options": "reference_doctype",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_details",
|
||||
@@ -140,11 +144,14 @@
|
||||
"label": "Transaction Details"
|
||||
},
|
||||
{
|
||||
"description": "Amount in customer's currency",
|
||||
"description": "Amount in transaction currency",
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"in_preview": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
"non_negative": 1,
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -388,13 +395,31 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.docstatus === 1",
|
||||
"description": "Amount in party's bank account currency",
|
||||
"fieldname": "outstanding_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_preview": 1,
|
||||
"label": "Outstanding Amount",
|
||||
"non_negative": 1,
|
||||
"options": "party_account_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Party Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-21 16:56:40.115737",
|
||||
"modified": "2024-09-16 17:50:54.440090",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -429,7 +454,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, get_url, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
@@ -45,6 +47,12 @@ class PaymentRequest(Document):
|
||||
frappe.throw(_("To create a Payment Request reference document is required"))
|
||||
|
||||
def validate_payment_request_amount(self):
|
||||
if self.grand_total == 0:
|
||||
frappe.throw(
|
||||
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
|
||||
title=_("Invalid Amount"),
|
||||
)
|
||||
|
||||
existing_payment_request_amount = flt(
|
||||
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
||||
)
|
||||
@@ -92,6 +100,29 @@ class PaymentRequest(Document):
|
||||
).format(self.grand_total, amount)
|
||||
)
|
||||
|
||||
def before_submit(self):
|
||||
company = frappe.get_value(self.reference_doctype, self.reference_name, "company")
|
||||
if (
|
||||
self.currency != self.party_account_currency
|
||||
and self.party_account_currency == get_company_currency(company)
|
||||
):
|
||||
# set outstanding amount in party account currency
|
||||
invoice = frappe.get_value(
|
||||
self.reference_doctype,
|
||||
self.reference_name,
|
||||
["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
|
||||
as_dict=1,
|
||||
)
|
||||
grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
|
||||
base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
|
||||
self.outstanding_amount = flt(
|
||||
self.grand_total / grand_total * base_grand_total,
|
||||
self.precision("outstanding_amount"),
|
||||
)
|
||||
|
||||
else:
|
||||
self.outstanding_amount = self.grand_total
|
||||
|
||||
def on_submit(self):
|
||||
if self.payment_request_type == "Outward":
|
||||
self.db_set("status", "Initiated")
|
||||
@@ -207,12 +238,12 @@ class PaymentRequest(Document):
|
||||
return controller.get_payment_url(
|
||||
**{
|
||||
"amount": flt(self.grand_total, self.precision("grand_total")),
|
||||
"title": data.company.encode("utf-8"),
|
||||
"description": self.subject.encode("utf-8"),
|
||||
"title": data.company,
|
||||
"description": self.subject,
|
||||
"reference_doctype": "Payment Request",
|
||||
"reference_docname": self.name,
|
||||
"payer_email": self.email_to or frappe.session.user,
|
||||
"payer_name": frappe.safe_encode(data.customer_name),
|
||||
"payer_name": data.customer_name,
|
||||
"order_id": self.name,
|
||||
"currency": self.currency,
|
||||
}
|
||||
@@ -220,7 +251,7 @@ class PaymentRequest(Document):
|
||||
|
||||
def set_as_paid(self):
|
||||
if self.payment_channel == "Phone":
|
||||
self.db_set("status", "Paid")
|
||||
self.db_set({"status": "Paid", "outstanding_amount": 0})
|
||||
|
||||
else:
|
||||
payment_entry = self.create_payment_entry()
|
||||
@@ -241,26 +272,32 @@ class PaymentRequest(Document):
|
||||
else:
|
||||
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
||||
|
||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
|
||||
party_account_currency = (
|
||||
self.get("party_account_currency")
|
||||
or ref_doc.get("party_account_currency")
|
||||
or get_account_currency(party_account)
|
||||
)
|
||||
|
||||
party_amount = bank_amount = self.outstanding_amount
|
||||
|
||||
bank_amount = self.grand_total
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
exchange_rate = ref_doc.get("conversion_rate")
|
||||
bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
|
||||
|
||||
# outstanding amount is already in Part's account currency
|
||||
payment_entry = get_payment_entry(
|
||||
self.reference_doctype,
|
||||
self.reference_name,
|
||||
party_amount=party_amount,
|
||||
bank_account=self.payment_account,
|
||||
bank_amount=bank_amount,
|
||||
created_from_payment_request=True,
|
||||
)
|
||||
|
||||
payment_entry.update(
|
||||
{
|
||||
"mode_of_payment": self.mode_of_payment,
|
||||
"reference_no": self.name,
|
||||
"reference_no": self.name, # to prevent validation error
|
||||
"reference_date": nowdate(),
|
||||
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
||||
self.reference_doctype, self.reference_name, self.name
|
||||
@@ -268,6 +305,9 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
|
||||
self._allocate_payment_request_to_pe_references(references=payment_entry.references)
|
||||
|
||||
# Update dimensions
|
||||
payment_entry.update(
|
||||
{
|
||||
@@ -276,14 +316,6 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
amount = payment_entry.base_paid_amount
|
||||
else:
|
||||
amount = self.grand_total
|
||||
|
||||
payment_entry.received_amount = amount
|
||||
payment_entry.get("references")[0].allocated_amount = amount
|
||||
|
||||
# Update 'Paid Amount' on Forex transactions
|
||||
if self.currency != ref_doc.company_currency:
|
||||
if (
|
||||
@@ -397,6 +429,62 @@ class PaymentRequest(Document):
|
||||
if payment_provider == "stripe":
|
||||
return create_stripe_subscription(gateway_controller, data)
|
||||
|
||||
def _allocate_payment_request_to_pe_references(self, references):
|
||||
"""
|
||||
Allocate the Payment Request to the Payment Entry references based on\n
|
||||
- Allocated Amount.
|
||||
- Outstanding Amount of Payment Request.\n
|
||||
Payment Request is doc itself and references are the rows of Payment Entry.
|
||||
"""
|
||||
if len(references) == 1:
|
||||
references[0].payment_request = self.name
|
||||
return
|
||||
|
||||
precision = references[0].precision("allocated_amount")
|
||||
outstanding_amount = self.outstanding_amount
|
||||
|
||||
# to manage rows
|
||||
row_number = 1
|
||||
MOVE_TO_NEXT_ROW = 1
|
||||
TO_SKIP_NEW_ROW = 2
|
||||
NEW_ROW_ADDED = False
|
||||
|
||||
while row_number <= len(references):
|
||||
row = references[row_number - 1]
|
||||
|
||||
# update the idx to maintain the order
|
||||
row.idx = row_number
|
||||
|
||||
if outstanding_amount == 0:
|
||||
if not NEW_ROW_ADDED:
|
||||
break
|
||||
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
continue
|
||||
|
||||
# allocate the payment request to the row
|
||||
row.payment_request = self.name
|
||||
|
||||
if row.allocated_amount <= outstanding_amount:
|
||||
outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
|
||||
row_number += MOVE_TO_NEXT_ROW
|
||||
else:
|
||||
remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
|
||||
row.allocated_amount = outstanding_amount
|
||||
outstanding_amount = 0
|
||||
|
||||
# create a new row without PR for remaining unallocated amount
|
||||
new_row = frappe.copy_doc(row)
|
||||
references.insert(row_number, new_row)
|
||||
|
||||
# update new row
|
||||
new_row.idx = row_number + 1
|
||||
new_row.payment_request = None
|
||||
new_row.allocated_amount = remaining_allocated_amount
|
||||
|
||||
NEW_ROW_ADDED = True
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def make_payment_request(**args):
|
||||
@@ -427,11 +515,15 @@ def make_payment_request(**args):
|
||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
||||
)
|
||||
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
||||
# 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
|
||||
|
||||
if not grand_total:
|
||||
frappe.throw(_("Payment Request is already created"))
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
||||
@@ -445,6 +537,13 @@ def make_payment_request(**args):
|
||||
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||
)
|
||||
|
||||
party_type = args.get("party_type") or "Customer"
|
||||
party_account_currency = ref_doc.get("party_account_currency")
|
||||
|
||||
if not party_account_currency:
|
||||
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
|
||||
pr.update(
|
||||
{
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
@@ -453,6 +552,7 @@ def make_payment_request(**args):
|
||||
"payment_channel": gateway_account.get("payment_channel"),
|
||||
"payment_request_type": args.get("payment_request_type"),
|
||||
"currency": ref_doc.currency,
|
||||
"party_account_currency": party_account_currency,
|
||||
"grand_total": grand_total,
|
||||
"mode_of_payment": args.mode_of_payment,
|
||||
"email_to": args.recipient_id or ref_doc.owner,
|
||||
@@ -460,7 +560,7 @@ def make_payment_request(**args):
|
||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||
"reference_doctype": args.dt,
|
||||
"reference_name": args.dn,
|
||||
"party_type": args.get("party_type") or "Customer",
|
||||
"party_type": party_type,
|
||||
"party": args.get("party") or ref_doc.get("customer"),
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
@@ -503,9 +603,11 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.grand_total)
|
||||
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
|
||||
else:
|
||||
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
grand_total = flt(
|
||||
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
)
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
@@ -527,24 +629,20 @@ def get_amount(ref_doc, payment_account=None):
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
"""
|
||||
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
|
||||
and get the summation of existing paid payment request for Phone payment channel.
|
||||
Return the total amount of Payment Requests against a reference document.
|
||||
"""
|
||||
existing_payment_request_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(grand_total)
|
||||
from `tabPayment Request`
|
||||
where
|
||||
reference_doctype = %s
|
||||
and reference_name = %s
|
||||
and docstatus = 1
|
||||
and (status != 'Paid'
|
||||
or (payment_channel = 'Phone'
|
||||
and status = 'Paid'))
|
||||
""",
|
||||
(ref_dt, ref_dn),
|
||||
PR = frappe.qb.DocType("Payment Request")
|
||||
|
||||
response = (
|
||||
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()
|
||||
)
|
||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
@@ -592,41 +690,66 @@ def make_payment_entry(docname):
|
||||
return doc.create_payment_entry(submit=False).as_dict()
|
||||
|
||||
|
||||
def update_payment_req_status(doc, method):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
|
||||
def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
"""
|
||||
Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
|
||||
"""
|
||||
if not references:
|
||||
return
|
||||
|
||||
for ref in doc.references:
|
||||
payment_request_name = frappe.db.get_value(
|
||||
"Payment Request",
|
||||
{
|
||||
"reference_doctype": ref.reference_doctype,
|
||||
"reference_name": ref.reference_name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
referenced_payment_requests = frappe.get_all(
|
||||
"Payment Request",
|
||||
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||
fields=[
|
||||
"name",
|
||||
"grand_total",
|
||||
"outstanding_amount",
|
||||
"payment_request_type",
|
||||
],
|
||||
)
|
||||
|
||||
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||
|
||||
for ref in references:
|
||||
if not ref.payment_request:
|
||||
continue
|
||||
|
||||
payment_request = referenced_payment_requests[ref.payment_request]
|
||||
pr_outstanding = payment_request["outstanding_amount"]
|
||||
|
||||
# update outstanding amount
|
||||
new_outstanding_amount = flt(
|
||||
pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
|
||||
precision,
|
||||
)
|
||||
|
||||
if payment_request_name:
|
||||
ref_details = get_reference_details(
|
||||
ref.reference_doctype,
|
||||
ref.reference_name,
|
||||
doc.party_account_currency,
|
||||
doc.party_type,
|
||||
doc.party,
|
||||
# to handle same payment request for the multiple allocations
|
||||
payment_request["outstanding_amount"] = new_outstanding_amount
|
||||
|
||||
if not cancel and new_outstanding_amount < 0:
|
||||
frappe.throw(
|
||||
msg=_(
|
||||
"The allocated amount is greater than the outstanding amount of Payment Request {0}"
|
||||
).format(ref.payment_request),
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
||||
status = pay_req_doc.status
|
||||
|
||||
if status != "Paid" and not ref_details.outstanding_amount:
|
||||
status = "Paid"
|
||||
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
|
||||
status = "Partially Paid"
|
||||
elif ref_details.outstanding_amount == ref_details.total_amount:
|
||||
if pay_req_doc.payment_request_type == "Outward":
|
||||
status = "Initiated"
|
||||
elif pay_req_doc.payment_request_type == "Inward":
|
||||
status = "Requested"
|
||||
# update status
|
||||
if new_outstanding_amount == payment_request["grand_total"]:
|
||||
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||
elif new_outstanding_amount == 0:
|
||||
status = "Paid"
|
||||
elif new_outstanding_amount > 0:
|
||||
status = "Partially Paid"
|
||||
|
||||
pay_req_doc.db_set("status", status)
|
||||
# update database
|
||||
frappe.db.set_value(
|
||||
"Payment Request",
|
||||
ref.payment_request,
|
||||
{"outstanding_amount": new_outstanding_amount, "status": status},
|
||||
)
|
||||
|
||||
|
||||
def get_dummy_message(doc):
|
||||
@@ -710,3 +833,36 @@ def validate_payment(doc, method=None):
|
||||
doc.reference_docname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# permission checks in `get_list()`
|
||||
filters = frappe._dict(filters)
|
||||
|
||||
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={
|
||||
**filters,
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["name", "grand_total", "outstanding_amount"],
|
||||
order_by="transaction_date ASC,creation ASC",
|
||||
)
|
||||
|
||||
return [
|
||||
(
|
||||
pr.name,
|
||||
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
|
||||
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
|
||||
)
|
||||
for pr in open_payment_requests
|
||||
]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import re
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
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
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
@@ -15,6 +17,7 @@ from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
||||
|
||||
|
||||
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
||||
|
||||
payment_method = [
|
||||
@@ -278,3 +281,246 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
self.assertEqual(pe.paid_amount, 800)
|
||||
self.assertEqual(pe.base_received_amount, 800)
|
||||
self.assertEqual(pe.received_amount, 10)
|
||||
|
||||
def test_multiple_payment_if_partially_paid_for_same_currency(self):
|
||||
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
# to make partial payment
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].allocated_amount = 200
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 800)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# complete payment
|
||||
pe = pr.create_payment_entry()
|
||||
|
||||
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 800)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# creating a more payment Request must not allowed
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
make_payment_request,
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||
def test_multiple_payment_if_partially_paid_for_multi_currency(self):
|
||||
pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1)
|
||||
pi.credit_to = "Creditors - _TC"
|
||||
pi.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
# 100 USD -> 5000 INR
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
self.assertEqual(pr.outstanding_amount, 5000)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
self.assertEqual(pr.party_account_currency, "INR")
|
||||
self.assertEqual(pr.status, "Initiated")
|
||||
|
||||
# to make partial payment
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 2000
|
||||
pe.references[0].allocated_amount = 2000
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 3000)
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
|
||||
# complete payment
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 3000)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 100)
|
||||
|
||||
# creating a more payment Request must not allowed
|
||||
self.assertRaisesRegex(
|
||||
frappe.exceptions.ValidationError,
|
||||
re.compile(r"Payment Request is already created"),
|
||||
make_payment_request,
|
||||
dt="Purchase Invoice",
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
def test_single_payment_with_payment_term_for_same_currency(self):
|
||||
create_payment_terms_template()
|
||||
|
||||
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000)
|
||||
po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Order",
|
||||
dn=po.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 20000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||
self.assertEqual(pr.status, "Initiated")
|
||||
|
||||
po.load_from_db()
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(pe.paid_amount, 20000)
|
||||
|
||||
# check 1st payment term
|
||||
self.assertEqual(pe.references[0].allocated_amount, 16949.2)
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
# check 2nd payment term
|
||||
self.assertEqual(pe.references[1].allocated_amount, 3050.8)
|
||||
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||
|
||||
po.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 20000)
|
||||
|
||||
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||
def test_single_payment_with_payment_term_for_multi_currency(self):
|
||||
create_payment_terms_template()
|
||||
|
||||
si = create_sales_invoice(
|
||||
do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50
|
||||
)
|
||||
si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Invoice",
|
||||
dn=si.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
# 200 USD -> 10000 INR
|
||||
self.assertEqual(pr.grand_total, 200)
|
||||
self.assertEqual(pr.outstanding_amount, 10000)
|
||||
self.assertEqual(pr.currency, "USD")
|
||||
self.assertEqual(pr.party_account_currency, "INR")
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(pe.paid_amount, 10000)
|
||||
|
||||
# check 1st payment term
|
||||
# convert it via dollar and conversion_rate
|
||||
self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
# check 2nd payment term
|
||||
self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion
|
||||
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 0)
|
||||
self.assertEqual(pr.grand_total, 200)
|
||||
|
||||
def test_payment_cancel_process(self):
|
||||
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
mute_email=1,
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pe = pr.create_payment_entry(submit=False)
|
||||
pe.paid_amount = 800
|
||||
pe.references[0].allocated_amount = 800
|
||||
pe.submit()
|
||||
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Partially Paid")
|
||||
self.assertEqual(pr.outstanding_amount, 200)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
# cancelling PE
|
||||
pe.cancel()
|
||||
|
||||
pr.load_from_db()
|
||||
self.assertEqual(pr.status, "Requested")
|
||||
self.assertEqual(pr.outstanding_amount, 1000)
|
||||
self.assertEqual(pr.grand_total, 1000)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
@@ -40,7 +40,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
)
|
||||
if gle_count > 5000:
|
||||
frappe.enqueue(
|
||||
make_reverse_gl_entries,
|
||||
process_cancellation,
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
@@ -51,9 +51,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
self.delete_closing_entries()
|
||||
process_cancellation(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
def validate_future_closing_vouchers(self):
|
||||
if frappe.db.exists(
|
||||
@@ -66,12 +64,6 @@ class PeriodClosingVoucher(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
def delete_closing_entries(self):
|
||||
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||
frappe.qb.from_(closing_balance).delete().where(
|
||||
closing_balance.period_closing_voucher == self.name
|
||||
).run()
|
||||
|
||||
def validate_account_head(self):
|
||||
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
||||
|
||||
@@ -136,14 +128,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
||||
if len(gl_entries + closing_entries) > 3000:
|
||||
frappe.enqueue(
|
||||
process_gl_entries,
|
||||
gl_entries=gl_entries,
|
||||
voucher_name=self.name,
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.enqueue(
|
||||
process_closing_entries,
|
||||
process_gl_and_closing_entries,
|
||||
gl_entries=gl_entries,
|
||||
closing_entries=closing_entries,
|
||||
voucher_name=self.name,
|
||||
@@ -157,8 +142,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_entries(gl_entries, self.name)
|
||||
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
||||
process_gl_and_closing_entries(
|
||||
gl_entries, closing_entries, self.name, self.company, self.posting_date
|
||||
)
|
||||
|
||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
||||
closing_entries = []
|
||||
@@ -323,9 +309,10 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
if get_opening_entries:
|
||||
query = query.where(
|
||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
||||
| gl_entry.is_opening
|
||||
== "Yes"
|
||||
( # noqa: UP034
|
||||
(gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date))
|
||||
| (gl_entry.is_opening == "Yes")
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = query.where(
|
||||
@@ -343,12 +330,16 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def process_gl_entries(gl_entries, voucher_name):
|
||||
def process_gl_and_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
@@ -356,26 +347,21 @@ def process_gl_entries(gl_entries, voucher_name):
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
|
||||
try:
|
||||
if gl_entries + closing_entries:
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
|
||||
|
||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
||||
def process_cancellation(voucher_type, voucher_no):
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
try:
|
||||
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
|
||||
delete_closing_entries(voucher_no)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def delete_closing_entries(voucher_no):
|
||||
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||
frappe.qb.from_(closing_balance).delete().where(
|
||||
closing_balance.period_closing_voucher == voucher_no
|
||||
).run()
|
||||
|
||||
@@ -80,8 +80,10 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
) {
|
||||
reset_values(frm);
|
||||
frappe.run_serially([
|
||||
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
|
||||
() => frm.trigger("set_opening_amounts"),
|
||||
() => frm.trigger("get_pos_invoices"),
|
||||
() => frappe.dom.unfreeze(),
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -379,7 +379,7 @@ class POSInvoice(SalesInvoice):
|
||||
if self.is_return and entry.amount > 0:
|
||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
||||
|
||||
if self.is_return:
|
||||
if self.is_return and self.docstatus != 0:
|
||||
invoice_total = self.rounded_total or self.grand_total
|
||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
@@ -389,7 +389,9 @@ def split_invoices(invoices):
|
||||
if not item.serial_no:
|
||||
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
|
||||
|
||||
|
||||
@@ -359,7 +359,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
|
||||
@@ -386,9 +399,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)
|
||||
|
||||
@@ -981,6 +981,12 @@ class TestPricingRule(FrappeTestCase):
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||
self.assertEqual(so.items[1].qty, 3)
|
||||
|
||||
so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=1)
|
||||
so.items[0].qty = 1
|
||||
del so.items[-1]
|
||||
so.save()
|
||||
self.assertEqual(len(so.items), 1)
|
||||
|
||||
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")
|
||||
|
||||
@@ -644,6 +644,9 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
if pricing_rule.round_free_qty:
|
||||
qty = math.floor(qty)
|
||||
|
||||
if not qty:
|
||||
return
|
||||
|
||||
free_item_data_args = {
|
||||
"item_code": free_item,
|
||||
"qty": qty,
|
||||
@@ -712,14 +715,11 @@ def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
||||
|
||||
def validate_coupon_code(coupon_name):
|
||||
coupon = frappe.get_doc("Coupon Code", coupon_name)
|
||||
|
||||
if coupon.valid_from:
|
||||
if coupon.valid_from > getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has not started"))
|
||||
elif coupon.valid_upto:
|
||||
if coupon.valid_upto < getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
||||
elif coupon.used >= coupon.maximum_use:
|
||||
if coupon.valid_from and coupon.valid_from > getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has not started"))
|
||||
elif coupon.valid_upto and coupon.valid_upto < getdate(today()):
|
||||
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
||||
elif coupon.maximum_use and coupon.used >= coupon.maximum_use:
|
||||
frappe.throw(_("Sorry, this coupon code is no longer valid"))
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"payment_terms_template",
|
||||
"sales_partner",
|
||||
"sales_person",
|
||||
"show_remarks",
|
||||
"based_on_payment_terms",
|
||||
"section_break_3",
|
||||
"customer_collection",
|
||||
@@ -388,10 +389,16 @@
|
||||
"fieldname": "ignore_cr_dr_notes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore System Generated Credit / Debit Notes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_remarks",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Remarks"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2024-08-13 10:41:18.381165",
|
||||
"modified": "2024-10-18 17:51:39.108481",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -132,6 +132,7 @@ def get_common_filters(doc):
|
||||
"finance_book": doc.finance_book if doc.finance_book else None,
|
||||
"account": [doc.account] if doc.account else None,
|
||||
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
|
||||
"show_remarks": doc.show_remarks,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
pricing_rule_fields = [
|
||||
"apply_on",
|
||||
@@ -91,22 +93,50 @@ class PromotionalScheme(Document):
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
transaction_exists = False
|
||||
docnames = []
|
||||
invalid_pricing_rule = self.get_invalid_pricing_rules()
|
||||
|
||||
# If user has changed applicable for
|
||||
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
|
||||
if not invalid_pricing_rule:
|
||||
return
|
||||
|
||||
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
|
||||
if frappe.db.exists(
|
||||
"Pricing Rule Detail",
|
||||
{
|
||||
"pricing_rule": ["in", invalid_pricing_rule],
|
||||
"docstatus": ["<", 2],
|
||||
},
|
||||
):
|
||||
raise_for_transaction_exists(self.name)
|
||||
|
||||
for docname in docnames:
|
||||
if frappe.db.exists("Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}):
|
||||
raise_for_transaction_exists(self.name)
|
||||
for doc in invalid_pricing_rule:
|
||||
frappe.delete_doc("Pricing Rule", doc)
|
||||
|
||||
if docnames and not transaction_exists:
|
||||
for docname in docnames:
|
||||
frappe.delete_doc("Pricing Rule", docname.name)
|
||||
frappe.msgprint(
|
||||
_("The following invalid Pricing Rules are deleted:")
|
||||
+ "<br><br><ul><li>"
|
||||
+ "</li><li>".join(invalid_pricing_rule)
|
||||
+ "</li></ul>"
|
||||
)
|
||||
|
||||
def get_invalid_pricing_rules(self):
|
||||
pr = frappe.qb.DocType("Pricing Rule")
|
||||
conditions = []
|
||||
conditions.append(pr.promotional_scheme == self.name)
|
||||
|
||||
if self.applicable_for:
|
||||
applicable_for = frappe.scrub(self.applicable_for)
|
||||
applicable_for_list = [d.get(applicable_for) for d in self.get(applicable_for)]
|
||||
|
||||
conditions.append(
|
||||
(IfNull(pr.applicable_for, "") != self.applicable_for)
|
||||
| (
|
||||
(IfNull(pr.applicable_for, "") == self.applicable_for)
|
||||
& IfNull(pr[applicable_for], "").notin(applicable_for_list)
|
||||
)
|
||||
)
|
||||
else:
|
||||
conditions.append(IfNull(pr.applicable_for, "") != "")
|
||||
|
||||
return frappe.qb.from_(pr).select(pr.name).where(Criterion.all(conditions)).run(pluck=True)
|
||||
|
||||
def on_update(self):
|
||||
self.validate()
|
||||
|
||||
@@ -90,6 +90,31 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_change_applicable_for_values_in_promotional_scheme(self):
|
||||
ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer")
|
||||
ps.append("customer", {"customer": "_Test Customer 2"})
|
||||
ps.save()
|
||||
|
||||
price_rules = frappe.get_all(
|
||||
"Pricing Rule", filters={"promotional_scheme": ps.name, "applicable_for": "Customer"}
|
||||
)
|
||||
self.assertTrue(len(price_rules), 2)
|
||||
|
||||
ps.set("customer", [])
|
||||
ps.append("customer", {"customer": "_Test Customer 2"})
|
||||
ps.save()
|
||||
|
||||
price_rules = frappe.get_all(
|
||||
"Pricing Rule",
|
||||
filters={
|
||||
"promotional_scheme": ps.name,
|
||||
"applicable_for": "Customer",
|
||||
"customer": "_Test Customer",
|
||||
},
|
||||
)
|
||||
self.assertEqual(price_rules, [])
|
||||
frappe.delete_doc("Promotional Scheme", ps.name)
|
||||
|
||||
def test_min_max_amount_configuration(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.price_discount_slabs[0].min_amount = 10
|
||||
|
||||
@@ -25,6 +25,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: { company: doc.company },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onload() {
|
||||
@@ -303,8 +310,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
party_type: "Supplier",
|
||||
account: this.frm.doc.credit_to,
|
||||
price_list: this.frm.doc.buying_price_list,
|
||||
fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template)
|
||||
}, function() {
|
||||
fetch_payment_terms_template: cint(
|
||||
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
|
||||
),
|
||||
},
|
||||
function () {
|
||||
me.apply_pricing_rule();
|
||||
me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0;
|
||||
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
|
||||
@@ -469,13 +479,6 @@ cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn)
|
||||
}
|
||||
}
|
||||
|
||||
cur_frm.set_query("expense_account", "items", function(doc) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: {'company': doc.company }
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.set_query("wip_composite_asset", "items", function() {
|
||||
return {
|
||||
filters: {'is_composite_asset': 1, 'docstatus': 0 }
|
||||
|
||||
@@ -1128,12 +1128,14 @@
|
||||
"label": "Payment Terms"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
|
||||
"fieldname": "payment_terms_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Terms Template",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
|
||||
"fieldname": "payment_schedule",
|
||||
"fieldtype": "Table",
|
||||
"label": "Payment Schedule",
|
||||
@@ -1611,7 +1613,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-09-11 12:59:19.130593",
|
||||
"modified": "2024-10-25 18:13:01.944477",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -280,8 +280,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
party_type: "Customer",
|
||||
account: this.frm.doc.debit_to,
|
||||
price_list: this.frm.doc.selling_price_list,
|
||||
pos_profile: pos_profile
|
||||
}, function() {
|
||||
pos_profile: pos_profile,
|
||||
fetch_payment_terms_template: cint(
|
||||
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
|
||||
),
|
||||
},
|
||||
function () {
|
||||
me.apply_pricing_rule();
|
||||
});
|
||||
|
||||
|
||||
@@ -170,6 +170,7 @@ class SalesInvoice(SellingController):
|
||||
):
|
||||
validate_loyalty_points(self, self.loyalty_points)
|
||||
|
||||
self.allow_write_off_only_on_pos()
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
|
||||
def validate_accounts(self):
|
||||
@@ -850,6 +851,10 @@ class SalesInvoice(SellingController):
|
||||
raise_exception=1,
|
||||
)
|
||||
|
||||
def allow_write_off_only_on_pos(self):
|
||||
if not self.is_pos and self.write_off_account:
|
||||
self.write_off_account = None
|
||||
|
||||
def validate_write_off_account(self):
|
||||
if flt(self.write_off_amount) and not self.write_off_account:
|
||||
self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account")
|
||||
|
||||
@@ -295,7 +295,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
else:
|
||||
# if no TCS has been charged in FY,
|
||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
||||
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
@@ -382,6 +382,9 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
"""
|
||||
|
||||
if party_type == "Supplier":
|
||||
return []
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
conditions = []
|
||||
@@ -479,7 +482,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
payment_entry_filters.pop("apply_tax_withholding_amount", None)
|
||||
payment_entry_filters.pop("tax_withholding_category", None)
|
||||
|
||||
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||
|
||||
supp_jv_credit_amt = (
|
||||
frappe.db.get_value(
|
||||
@@ -503,7 +506,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
group_by="payment_type",
|
||||
)
|
||||
|
||||
supp_credit_amt += supp_jv_credit_amt
|
||||
supp_credit_amt = supp_jv_credit_amt
|
||||
supp_credit_amt += inv.tax_withholding_net_total
|
||||
|
||||
for type in payment_entry_amounts:
|
||||
@@ -521,19 +524,19 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and supp_credit_amt >= cumulative_threshold
|
||||
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||
):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
|
||||
)
|
||||
supp_credit_amt += net_total
|
||||
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
tax_details.tax_on_excess_amount
|
||||
):
|
||||
# Get net total again as TDS is calculated on net total
|
||||
# Grand is used to just check for threshold breach
|
||||
net_total = (
|
||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)")
|
||||
or 0.0
|
||||
)
|
||||
net_total += inv.tax_withholding_net_total
|
||||
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(
|
||||
@@ -575,8 +578,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
conditions.append(ple.company == inv.company)
|
||||
|
||||
(qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1))
|
||||
|
||||
advance_amt = (
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||
)
|
||||
@@ -599,9 +600,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
)
|
||||
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
advance_adjusted = get_advance_adjusted_in_invoice(inv)
|
||||
|
||||
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
|
||||
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
|
||||
total_invoiced_amt = (
|
||||
current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted
|
||||
)
|
||||
|
||||
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
|
||||
chargeable_amt = total_invoiced_amt - cumulative_threshold
|
||||
@@ -610,6 +614,14 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
return tcs_amount
|
||||
|
||||
|
||||
def get_advance_adjusted_in_invoice(inv):
|
||||
advances_adjusted = 0
|
||||
for row in inv.get("advances", []):
|
||||
advances_adjusted += row.allocated_amount
|
||||
|
||||
return advances_adjusted
|
||||
|
||||
|
||||
def get_invoice_total_without_tcs(inv, tax_details):
|
||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
||||
|
||||
@@ -121,6 +121,85 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_with_party_ledger_amount_on_net_total(self):
|
||||
invoices = []
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier3", "tax_withholding_category", "Advance 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=1000, 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=6000)
|
||||
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.assertEqual(pi1.taxes[0].tax_amount, 800)
|
||||
|
||||
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"
|
||||
@@ -214,6 +293,50 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
d.reload()
|
||||
d.cancel()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"unlink_payment_on_cancellation_of_invoice": 1},
|
||||
)
|
||||
def test_tcs_on_allocated_advance_payments(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
|
||||
vouchers = []
|
||||
|
||||
# create advance payment
|
||||
pe = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=30000
|
||||
)
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_to = "Cash - _TC"
|
||||
pe.submit()
|
||||
vouchers.append(pe)
|
||||
|
||||
si = create_sales_invoice(customer="Test TCS Customer", rate=50000)
|
||||
advances = si.get_advance_entries()
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"advance_amount": advances[0].amount,
|
||||
"allocated_amount": 30000,
|
||||
},
|
||||
)
|
||||
si.submit()
|
||||
vouchers.append(si)
|
||||
|
||||
# assert tax collection on total invoice ,advance payment adjusted should be excluded.
|
||||
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"])
|
||||
# tcs = (inv amt)50000+(adv amt)30000-(adv adj) 30000 - threshold(30000) * rate 10%
|
||||
self.assertEqual(tcs_charged, 2000)
|
||||
|
||||
# cancel invoice and payments to avoid clashing
|
||||
for d in reversed(vouchers):
|
||||
d.reload()
|
||||
d.cancel()
|
||||
|
||||
def test_tds_calculation_on_net_total(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:UNREC-{#####}",
|
||||
"creation": "2023-08-22 10:26:34.421423",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
@@ -58,11 +56,10 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-28 17:42:50.261377",
|
||||
"modified": "2024-10-10 12:03:50.022444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payment",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -305,8 +305,8 @@ class ReceivablePayableReport:
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision
|
||||
if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
@@ -500,7 +500,8 @@ class ReceivablePayableReport:
|
||||
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
si.name = %s
|
||||
si.name = %s and
|
||||
si.is_return = 0
|
||||
order by ps.paid_amount desc, due_date
|
||||
""",
|
||||
row.voucher_no,
|
||||
|
||||
@@ -95,7 +95,7 @@ def execute(filters=None):
|
||||
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
|
||||
)
|
||||
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity)
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
@@ -219,7 +219,7 @@ def get_report_summary(
|
||||
], (net_asset - net_liability + net_equity)
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, asset, liability, equity):
|
||||
def get_chart_data(filters, columns, asset, liability, equity, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
|
||||
asset_data, liability_data, equity_data = [], [], []
|
||||
@@ -247,4 +247,8 @@ def get_chart_data(filters, columns, asset, liability, equity):
|
||||
else:
|
||||
chart["type"] = "line"
|
||||
|
||||
chart["fieldtype"] = "Currency"
|
||||
chart["options"] = "currency"
|
||||
chart["currency"] = currency
|
||||
|
||||
return chart
|
||||
|
||||
@@ -47,7 +47,7 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter, filter) {
|
||||
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
|
||||
if (column.fieldname == "payment_entry" && value == __("Cheques and Deposits incorrectly cleared")) {
|
||||
column.link_onclick =
|
||||
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ def execute(filters=None):
|
||||
)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
|
||||
chart = get_chart_data(columns, data)
|
||||
chart = get_chart_data(columns, data, company_currency)
|
||||
|
||||
report_summary = get_report_summary(summary_data, company_currency)
|
||||
|
||||
@@ -257,7 +257,7 @@ def get_report_summary(summary_data, currency):
|
||||
return report_summary
|
||||
|
||||
|
||||
def get_chart_data(columns, data):
|
||||
def get_chart_data(columns, data, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
datasets = [
|
||||
{
|
||||
@@ -272,5 +272,7 @@ def get_chart_data(columns, data):
|
||||
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}
|
||||
|
||||
chart["fieldtype"] = "Currency"
|
||||
chart["options"] = "currency"
|
||||
chart["currency"] = currency
|
||||
|
||||
return chart
|
||||
|
||||
@@ -120,7 +120,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
True,
|
||||
)
|
||||
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity)
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency)
|
||||
|
||||
return data, message, chart, report_summary
|
||||
|
||||
@@ -178,7 +178,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
if net_profit_loss:
|
||||
data.append(net_profit_loss)
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency)
|
||||
|
||||
report_summary, primitive_summary = get_pl_summary(
|
||||
companies, "", income, expense, net_profit_loss, company_currency, filters, True
|
||||
@@ -474,10 +474,13 @@ def update_parent_account_names(accounts):
|
||||
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
account_name = d.account_number + " - " + d.account_name
|
||||
account_key = d.account_number + " - " + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
name_to_account_map[d.name] = account_name
|
||||
account_key = d.account_name
|
||||
|
||||
d.account_key = account_key
|
||||
|
||||
name_to_account_map[d.name] = account_key
|
||||
|
||||
for account in accounts:
|
||||
if account.parent_account:
|
||||
@@ -510,33 +513,26 @@ def get_subsidiary_companies(company):
|
||||
|
||||
def get_accounts(root_type, companies):
|
||||
accounts = []
|
||||
added_accounts = []
|
||||
|
||||
for company in companies:
|
||||
for account in frappe.get_all(
|
||||
"Account",
|
||||
fields=[
|
||||
"name",
|
||||
"is_group",
|
||||
"company",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"account_number",
|
||||
],
|
||||
filters={"company": company, "root_type": root_type},
|
||||
):
|
||||
if account.account_number:
|
||||
account_key = account.account_number + "-" + account.account_name
|
||||
else:
|
||||
account_key = account.account_name
|
||||
|
||||
if account_key not in added_accounts:
|
||||
accounts.append(account)
|
||||
added_accounts.append(account_key)
|
||||
accounts.extend(
|
||||
frappe.get_all(
|
||||
"Account",
|
||||
fields=[
|
||||
"name",
|
||||
"is_group",
|
||||
"company",
|
||||
"parent_account",
|
||||
"lft",
|
||||
"rgt",
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_name",
|
||||
"account_number",
|
||||
],
|
||||
filters={"company": company, "root_type": root_type},
|
||||
)
|
||||
)
|
||||
|
||||
return accounts
|
||||
|
||||
@@ -775,15 +771,17 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
|
||||
def filter_accounts(accounts, depth=10):
|
||||
parent_children_map = {}
|
||||
accounts_by_name = {}
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
account_name = d.account_number + " - " + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
d["company_wise_opening_bal"] = defaultdict(float)
|
||||
accounts_by_name[account_name] = d
|
||||
added_accounts = []
|
||||
|
||||
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
||||
for d in accounts:
|
||||
if d.account_key in added_accounts:
|
||||
continue
|
||||
|
||||
added_accounts.append(d.account_key)
|
||||
d["company_wise_opening_bal"] = defaultdict(float)
|
||||
accounts_by_name[d.account_key] = d
|
||||
|
||||
parent_children_map.setdefault(d.parent_account_name or None, []).append(d)
|
||||
|
||||
filtered_accounts = []
|
||||
|
||||
@@ -795,7 +793,7 @@ def filter_accounts(accounts, depth=10):
|
||||
for child in children:
|
||||
child.indent = level
|
||||
filtered_accounts.append(child)
|
||||
add_to_list(child.name, level + 1)
|
||||
add_to_list(child.account_key, level + 1)
|
||||
|
||||
add_to_list(None, 0)
|
||||
|
||||
|
||||
@@ -122,21 +122,24 @@ class Deferred_Item:
|
||||
"""
|
||||
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
||||
"""
|
||||
if self.service_start_date != self.service_end_date:
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
if (
|
||||
self.service_start_date != self.service_end_date
|
||||
and add_days(self.last_entry_date, 1) < self.service_end_date
|
||||
):
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.service_end_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
|
||||
def calculate_item_revenue_expense_for_period(self):
|
||||
"""
|
||||
|
||||
@@ -199,8 +199,7 @@ class General_Payment_Ledger_Comparison:
|
||||
dict(
|
||||
label=_("Voucher Type"),
|
||||
fieldname="voucher_type",
|
||||
fieldtype="Link",
|
||||
options="DocType",
|
||||
fieldtype="Data",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
@@ -219,8 +218,7 @@ class General_Payment_Ledger_Comparison:
|
||||
dict(
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Link",
|
||||
options="DocType",
|
||||
fieldtype="Data",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -337,9 +337,17 @@ def get_accounts_with_children(accounts):
|
||||
return list(set(all_accounts)) if all_accounts else None
|
||||
|
||||
|
||||
def set_bill_no(gl_entries):
|
||||
inv_details = get_supplier_invoice_details()
|
||||
for gl in gl_entries:
|
||||
gl["bill_no"] = inv_details.get(gl.get("against_voucher"), "")
|
||||
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
data = []
|
||||
|
||||
set_bill_no(gl_entries)
|
||||
|
||||
gle_map = initialize_gle_map(gl_entries, filters)
|
||||
|
||||
totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map)
|
||||
@@ -513,7 +521,6 @@ def get_account_type_map(company):
|
||||
|
||||
def get_result_as_list(data, filters):
|
||||
balance, _balance_in_account_currency = 0, 0
|
||||
inv_details = get_supplier_invoice_details()
|
||||
|
||||
for d in data:
|
||||
if not d.get("posting_date"):
|
||||
@@ -523,7 +530,6 @@ def get_result_as_list(data, filters):
|
||||
d["balance"] = balance
|
||||
|
||||
d["account_currency"] = filters.account_currency
|
||||
d["bill_no"] = inv_details.get(d.get("against_voucher"), "")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ class PaymentLedger:
|
||||
)
|
||||
)
|
||||
self.columns.append(
|
||||
dict(label=_("Currency"), fieldname="currency", fieldtype="Currency", hidden=True)
|
||||
dict(label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", hidden=True)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -59,11 +59,11 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
|
||||
chart = get_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_currency"
|
||||
)
|
||||
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
)
|
||||
@@ -141,7 +141,7 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
||||
return net_profit_loss
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss):
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
|
||||
income_data, expense_data, net_profit = [], [], []
|
||||
@@ -170,5 +170,7 @@ def get_chart_data(filters, columns, income, expense, net_profit_loss):
|
||||
chart["type"] = "line"
|
||||
|
||||
chart["fieldtype"] = "Currency"
|
||||
chart["options"] = "currency"
|
||||
chart["currency"] = currency
|
||||
|
||||
return chart
|
||||
|
||||
@@ -311,6 +311,7 @@ def get_account_columns(invoice_list, include_payments):
|
||||
"""select distinct expense_account
|
||||
from `tabPurchase Invoice Item` where docstatus = 1
|
||||
and (expense_account is not null and expense_account != '')
|
||||
and parenttype='Purchase Invoice'
|
||||
and parent in (%s) order by expense_account"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple([inv.name for inv in invoice_list]),
|
||||
@@ -443,7 +444,7 @@ def get_invoice_expense_map(invoice_list):
|
||||
"""
|
||||
select parent, expense_account, sum(base_net_amount) as amount
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
group by parent, expense_account
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
@@ -514,7 +515,7 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
"""
|
||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
where parent in (%s) and parenttype='Purchase Invoice'
|
||||
"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
|
||||
@@ -522,7 +522,8 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, inclu
|
||||
tax_details = frappe.db.sql(
|
||||
"""select parent, account_head,
|
||||
sum(base_tax_amount_after_discount_amount) as tax_amount
|
||||
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head"""
|
||||
from `tabSales Taxes and Charges` where parent in (%s) and parenttype = 'Sales Invoice'
|
||||
group by parent, account_head"""
|
||||
% ", ".join(["%s"] * len(invoice_list)),
|
||||
tuple(inv.name for inv in invoice_list),
|
||||
as_dict=1,
|
||||
|
||||
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"):
|
||||
@@ -326,6 +330,7 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
|
||||
|
||||
if join_required:
|
||||
query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent)
|
||||
query = query.where(child_doc.parenttype == doctype)
|
||||
query = query.distinct()
|
||||
|
||||
if parent_doc.get_table_name() != "tabJournal Entry":
|
||||
|
||||
@@ -579,6 +579,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"):
|
||||
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
|
||||
|
||||
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"])
|
||||
@@ -1524,12 +1534,16 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
|
||||
return matched
|
||||
|
||||
|
||||
def get_stock_accounts(company, voucher_type=None, voucher_no=None):
|
||||
def get_stock_accounts(company, voucher_type=None, voucher_no=None, accounts=None):
|
||||
stock_accounts = [
|
||||
d.name
|
||||
for d in frappe.db.get_all("Account", {"account_type": "Stock", "company": company, "is_group": 0})
|
||||
]
|
||||
if voucher_type and voucher_no:
|
||||
|
||||
if accounts:
|
||||
stock_accounts = [row.account for row in accounts if row.account in stock_accounts]
|
||||
|
||||
elif voucher_type and voucher_no:
|
||||
if voucher_type == "Journal Entry":
|
||||
stock_accounts = [
|
||||
d.account
|
||||
@@ -1926,6 +1940,7 @@ class QueryPaymentLedger:
|
||||
ple.cost_center.as_("cost_center"),
|
||||
Sum(ple.amount).as_("amount"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
ple.remarks,
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_voucher_no))
|
||||
@@ -1988,6 +2003,7 @@ class QueryPaymentLedger:
|
||||
Table("vouchers").due_date,
|
||||
Table("vouchers").currency,
|
||||
Table("vouchers").cost_center.as_("cost_center"),
|
||||
Table("vouchers").remarks,
|
||||
)
|
||||
.where(Criterion.all(filter_on_outstanding_amount))
|
||||
)
|
||||
|
||||
@@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", {
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<span class="indicator whitespace-nowrap red">
|
||||
<span>Failed to post depreciation entries</span>
|
||||
<span>${__("Failed to post depreciation entries")}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -456,9 +456,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
|
||||
)
|
||||
|
||||
@@ -14,15 +14,26 @@ def get_data():
|
||||
"Material Request": ["items", "material_request"],
|
||||
"Supplier Quotation": ["items", "supplier_quotation"],
|
||||
"Project": ["items", "project"],
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
"BOM": ["items", "bom"],
|
||||
"Production Plan": ["items", "production_plan"],
|
||||
"Blanket Order": ["items", "blanket_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice"]},
|
||||
{"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice", "Sales Order"]},
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry", "Payment Request"]},
|
||||
{
|
||||
"label": _("Reference"),
|
||||
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
|
||||
"items": ["Supplier Quotation", "Project", "Auto Repeat"],
|
||||
},
|
||||
{
|
||||
"label": _("Manufacturing"),
|
||||
"items": ["Material Request", "BOM", "Production Plan", "Blanket Order"],
|
||||
},
|
||||
{
|
||||
"label": _("Sub-contracting"),
|
||||
"items": ["Subcontracting Order", "Stock Entry"],
|
||||
},
|
||||
{"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]},
|
||||
{"label": _("Internal"), "items": ["Sales Order"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -137,8 +137,8 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
return;
|
||||
}
|
||||
},
|
||||
"Download PDF for Supplier",
|
||||
"Download"
|
||||
__("Download PDF for Supplier"),
|
||||
__("Download")
|
||||
);
|
||||
},
|
||||
__("Tools")
|
||||
@@ -257,8 +257,10 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
});
|
||||
};
|
||||
|
||||
dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
|
||||
automatically be attached with the email.</p>`);
|
||||
const msg = __(
|
||||
"This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email."
|
||||
);
|
||||
dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">${msg}</p>`);
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@@ -392,6 +392,7 @@ def create_rfq_items(sq_doc, supplier, data):
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"stock_qty",
|
||||
"uom",
|
||||
]:
|
||||
args[field] = data.get(field)
|
||||
|
||||
|
||||
@@ -357,12 +357,15 @@ class AccountsController(TransactionBase):
|
||||
def validate_return_against_account(self):
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
|
||||
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
|
||||
cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To"
|
||||
cr_dr_account = self.get(cr_dr_account_field)
|
||||
if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account:
|
||||
original_account = frappe.get_value(self.doctype, self.return_against, cr_dr_account_field)
|
||||
if original_account != self.get(cr_dr_account_field):
|
||||
frappe.throw(
|
||||
_("'{0}' account: '{1}' should match the Return Against Invoice").format(
|
||||
frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account)
|
||||
_(
|
||||
"Please set {0} to {1}, the same account that was used in the original invoice {2}."
|
||||
).format(
|
||||
frappe.bold(_(self.meta.get_label(cr_dr_account_field), context=self.doctype)),
|
||||
frappe.bold(original_account),
|
||||
frappe.bold(self.return_against),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -412,6 +415,11 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_invoice_documents_schedule(self):
|
||||
if self.is_return:
|
||||
self.payment_terms_template = ""
|
||||
self.payment_schedule = []
|
||||
return
|
||||
|
||||
self.validate_payment_schedule_dates()
|
||||
self.set_due_date()
|
||||
self.set_payment_schedule()
|
||||
@@ -426,7 +434,7 @@ class AccountsController(TransactionBase):
|
||||
self.validate_payment_schedule_amount()
|
||||
|
||||
def validate_all_documents_schedule(self):
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return:
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
self.validate_invoice_documents_schedule()
|
||||
elif self.doctype in ("Quotation", "Purchase Order", "Sales Order"):
|
||||
self.validate_non_invoice_documents_schedule()
|
||||
@@ -1844,7 +1852,7 @@ class AccountsController(TransactionBase):
|
||||
).format(formatted_advance_paid, self.name, formatted_order_total)
|
||||
)
|
||||
|
||||
frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid)
|
||||
self.db_set("advance_paid", advance_paid)
|
||||
|
||||
@property
|
||||
def company_abbr(self):
|
||||
@@ -3124,7 +3132,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||
any_conversion_factor_changed = False
|
||||
|
||||
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_doc_permissions(parent, "write")
|
||||
@@ -3226,25 +3233,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
# if rate is greater than price_list_rate, set margin
|
||||
# or set discount
|
||||
child_item.discount_percentage = 0
|
||||
|
||||
if parent_doctype in sales_doctypes:
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.discount_percentage = flt(
|
||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||
child_item.precision("discount_percentage"),
|
||||
)
|
||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||
|
||||
if parent_doctype in sales_doctypes:
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
|
||||
child_item.flags.ignore_validate_update_after_submit = True
|
||||
if new_child_flag:
|
||||
@@ -3310,6 +3313,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
|
||||
parent.validate_uom_is_integer("uom", "qty")
|
||||
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
|
||||
|
||||
def check_if_child_table_updated(child_table_before_update, child_table_after_update, fields_to_check):
|
||||
accounting_dimensions = [*get_accounting_dimensions(), "cost_center", "project"]
|
||||
|
||||
@@ -78,6 +78,9 @@ def validate_returned_items(doc):
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
select_fields += ",rejected_qty, received_qty"
|
||||
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
select_fields += ",name"
|
||||
|
||||
for d in frappe.db.sql(
|
||||
f"""select {select_fields} from `tab{doc.doctype} Item` where parent = %s""",
|
||||
doc.return_against,
|
||||
@@ -103,15 +106,24 @@ def validate_returned_items(doc):
|
||||
|
||||
items_returned = False
|
||||
for d in doc.get("items"):
|
||||
key = d.item_code
|
||||
raise_exception = False
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
field = frappe.scrub(doc.doctype) + "_item"
|
||||
if d.get(field):
|
||||
key = (d.item_code, d.get(field))
|
||||
raise_exception = True
|
||||
|
||||
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
|
||||
if d.item_code not in valid_items:
|
||||
frappe.throw(
|
||||
if key not in valid_items:
|
||||
frappe.msgprint(
|
||||
_("Row # {0}: Returned Item {1} does not exist in {2} {3}").format(
|
||||
d.idx, d.item_code, doc.doctype, doc.return_against
|
||||
)
|
||||
),
|
||||
raise_exception=raise_exception,
|
||||
)
|
||||
else:
|
||||
ref = valid_items.get(d.item_code, frappe._dict())
|
||||
ref = valid_items.get(key, frappe._dict())
|
||||
validate_quantity(doc, d, ref, valid_items, already_returned_items)
|
||||
|
||||
if (
|
||||
@@ -206,8 +218,12 @@ def validate_quantity(doc, args, ref, valid_items, already_returned_items):
|
||||
def get_ref_item_dict(valid_items, ref_item_row):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
key = ref_item_row.item_code
|
||||
if ref_item_row.get("name"):
|
||||
key = (ref_item_row.item_code, ref_item_row.name)
|
||||
|
||||
valid_items.setdefault(
|
||||
ref_item_row.item_code,
|
||||
key,
|
||||
frappe._dict(
|
||||
{
|
||||
"qty": 0,
|
||||
@@ -221,7 +237,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
|
||||
}
|
||||
),
|
||||
)
|
||||
item_dict = valid_items[ref_item_row.item_code]
|
||||
item_dict = valid_items[key]
|
||||
item_dict["qty"] += ref_item_row.qty
|
||||
item_dict["stock_qty"] += ref_item_row.get("stock_qty", 0)
|
||||
if ref_item_row.get("rate", 0) > item_dict["rate"]:
|
||||
|
||||
@@ -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:
|
||||
@@ -456,6 +473,16 @@ class SellingController(StockController):
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
if (
|
||||
not d.incoming_rate
|
||||
and self.get("return_against")
|
||||
and self.get("is_return")
|
||||
and get_valuation_method(d.item_code) == "Moving Average"
|
||||
):
|
||||
d.incoming_rate = get_rate_for_return(
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
|
||||
# For internal transfers use incoming rate as the valuation rate
|
||||
if self.is_internal_transfer():
|
||||
if self.doctype == "Delivery Note" or self.get("update_stock"):
|
||||
|
||||
@@ -91,7 +91,8 @@ status_map = {
|
||||
],
|
||||
"Purchase Receipt": [
|
||||
["Draft", None],
|
||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
||||
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
|
||||
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
|
||||
@@ -50,6 +50,21 @@ class StockController(AccountsController):
|
||||
self.set_rate_of_stock_uom()
|
||||
self.validate_internal_transfer()
|
||||
self.validate_putaway_capacity()
|
||||
self.reset_conversion_factor()
|
||||
|
||||
def reset_conversion_factor(self):
|
||||
for row in self.get("items"):
|
||||
if row.uom != row.stock_uom:
|
||||
continue
|
||||
|
||||
if row.conversion_factor != 1.0:
|
||||
row.conversion_factor = 1.0
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}."
|
||||
).format(bold(row.item_code), bold(row.uom), bold(row.stock_uom)),
|
||||
alert=True,
|
||||
)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False, via_landed_cost_voucher=False):
|
||||
if self.docstatus == 2:
|
||||
|
||||
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
|
||||
@@ -25,6 +25,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"}
|
||||
|
||||
@@ -354,7 +362,6 @@ doc_events = {
|
||||
"Payment Entry": {
|
||||
"on_submit": [
|
||||
"erpnext.regional.create_transaction_log",
|
||||
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
|
||||
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
||||
],
|
||||
"on_trash": "erpnext.regional.check_deletion_permission",
|
||||
|
||||
@@ -521,7 +521,7 @@ class JobCard(Document):
|
||||
self.set_transferred_qty()
|
||||
|
||||
def validate_transfer_qty(self):
|
||||
if self.items and self.transferred_qty < self.for_quantity:
|
||||
if not self.is_corrective_job_card and self.items and self.transferred_qty < self.for_quantity:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
|
||||
@@ -767,26 +767,19 @@ class JobCard(Document):
|
||||
|
||||
qty = 0
|
||||
if self.work_order:
|
||||
doc = frappe.get_doc("Work Order", self.work_order)
|
||||
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
|
||||
completed = True
|
||||
min_qty = []
|
||||
for d in doc.operations:
|
||||
if d.status != "Completed":
|
||||
completed = False
|
||||
if d.completed_qty:
|
||||
min_qty.append(d.completed_qty)
|
||||
else:
|
||||
min_qty = []
|
||||
break
|
||||
|
||||
if completed:
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
|
||||
fields="sum(transferred_qty) as qty",
|
||||
group_by="operation_id",
|
||||
)
|
||||
if min_qty:
|
||||
qty = min(min_qty)
|
||||
|
||||
if job_cards:
|
||||
qty = min(d.qty for d in job_cards)
|
||||
|
||||
doc.db_set("material_transferred_for_manufacturing", qty)
|
||||
doc.db_set("material_transferred_for_manufacturing", qty)
|
||||
|
||||
self.set_status(update_status)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Completed Qty",
|
||||
"reqd": 1
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
@@ -74,4 +74,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ Loan Management
|
||||
Telephony
|
||||
Bulk Transaction
|
||||
E-commerce
|
||||
Subcontracting
|
||||
Subcontracting
|
||||
EDI
|
||||
|
||||
@@ -366,3 +366,4 @@ execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1)
|
||||
erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
|
||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
|
||||
|
||||
@@ -38,7 +38,7 @@ def execute():
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company
|
||||
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company, creation
|
||||
FROM
|
||||
`tabStock Ledger Entry`
|
||||
WHERE
|
||||
@@ -67,6 +67,7 @@ def execute():
|
||||
"voucher_type": d.voucher_type,
|
||||
"voucher_no": d.voucher_no,
|
||||
"sle_id": d.name,
|
||||
"creation": d.creation,
|
||||
},
|
||||
allow_negative_stock=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
settings = frappe.get_doc("Currency Exchange Settings")
|
||||
if settings.service_provider != "frankfurter.app":
|
||||
return
|
||||
|
||||
settings.set_parameters_and_result()
|
||||
settings.flags.ignore_validate = True
|
||||
settings.save()
|
||||
@@ -47,6 +47,7 @@ frappe.ui.form.on("Project", {
|
||||
frm.set_query("sales_order", function () {
|
||||
var filters = {
|
||||
project: ["in", frm.doc.__islocal ? [""] : [frm.doc.name, ""]],
|
||||
company: frm.doc.company,
|
||||
};
|
||||
|
||||
if (frm.doc.customer) {
|
||||
|
||||
@@ -118,14 +118,14 @@ class Task(NestedSet):
|
||||
def validate_parent_template_task(self):
|
||||
if self.parent_task:
|
||||
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
|
||||
parent_task_format = f"""<a href="#Form/Task/{self.parent_task}">{self.parent_task}</a>"""
|
||||
parent_task_format = f"""<a href="/app/task/{self.parent_task}">{self.parent_task}</a>"""
|
||||
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
|
||||
|
||||
def validate_depends_on_tasks(self):
|
||||
if self.depends_on:
|
||||
for task in self.depends_on:
|
||||
if not frappe.db.get_value("Task", task.task, "is_template"):
|
||||
dependent_task_format = f"""<a href="#Form/Task/{task.task}">{task.task}</a>"""
|
||||
dependent_task_format = f"""<a href="/app/task/{task.task}">{task.task}</a>"""
|
||||
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
|
||||
|
||||
def validate_completed_on(self):
|
||||
|
||||
@@ -15,6 +15,7 @@ def execute(filters=None):
|
||||
filters=filters,
|
||||
fields=[
|
||||
"name",
|
||||
"project_name",
|
||||
"status",
|
||||
"percent_complete",
|
||||
"expected_start_date",
|
||||
@@ -48,6 +49,11 @@ def get_columns():
|
||||
"options": "Project",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"fieldname": "project_name",
|
||||
"label": _("Project Name"),
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"fieldname": "project_type",
|
||||
"label": _("Type"),
|
||||
@@ -82,7 +88,7 @@ def get_chart_data(data):
|
||||
overdue = []
|
||||
|
||||
for project in data:
|
||||
labels.append(project.name)
|
||||
labels.append(project.project_name)
|
||||
total.append(project.total_tasks)
|
||||
completed.append(project.completed_tasks)
|
||||
overdue.append(project.overdue_tasks)
|
||||
|
||||
@@ -822,9 +822,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
|
||||
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
|
||||
erpnext.utils.get_shipping_address(this.frm, function() {
|
||||
set_party_account(set_pricing);
|
||||
});
|
||||
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
|
||||
|
||||
if (!is_drop_ship) {
|
||||
console.log('get_shipping_address');
|
||||
erpnext.utils.get_shipping_address(this.frm, function() {
|
||||
set_party_account(set_pricing);
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
set_party_account(set_pricing);
|
||||
@@ -1099,6 +1104,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
|
||||
me.apply_price_list(item, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2213,7 +2219,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
payment_terms_template() {
|
||||
var me = this;
|
||||
const doc = this.frm.doc;
|
||||
if(doc.payment_terms_template && doc.doctype !== 'Delivery Note') {
|
||||
if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && !doc.is_return) {
|
||||
var posting_date = doc.posting_date || doc.transaction_date;
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.accounts_controller.get_payment_terms",
|
||||
|
||||
@@ -4,7 +4,8 @@ erpnext.accounts.unreconcile_payment = {
|
||||
add_unreconcile_btn(frm) {
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if (
|
||||
(frm.doc.doctype == "Journal Entry" && frm.doc.voucher_type != "Journal Entry") ||
|
||||
(frm.doc.doctype == "Journal Entry" &&
|
||||
!["Journal Entry", "Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) ||
|
||||
!["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(
|
||||
frm.doc.doctype
|
||||
)
|
||||
@@ -99,6 +100,7 @@ erpnext.accounts.unreconcile_payment = {
|
||||
fieldtype: "Table",
|
||||
read_only: 1,
|
||||
fields: child_table_fields,
|
||||
cannot_add_rows: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -122,7 +124,6 @@ erpnext.accounts.unreconcile_payment = {
|
||||
title: "UnReconcile Allocations",
|
||||
fields: unreconcile_dialog_fields,
|
||||
size: "large",
|
||||
cannot_add_rows: true,
|
||||
primary_action_label: "UnReconcile",
|
||||
primary_action(values) {
|
||||
let selected_allocations = values.allocations.filter((x) => x.__checked);
|
||||
|
||||
@@ -778,8 +778,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
]
|
||||
}
|
||||
],
|
||||
primary_action_label: 'Create Purchase Order',
|
||||
primary_action (args) {
|
||||
primary_action_label: __("Create Purchase Order"),
|
||||
primary_action(args) {
|
||||
if (!args) return;
|
||||
|
||||
let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children();
|
||||
|
||||
@@ -30,7 +30,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate
|
||||
from erpnext.stock.get_item_details import get_bin_details, get_default_bom, get_price_list_rate
|
||||
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
|
||||
|
||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
@@ -607,6 +607,9 @@ def make_material_request(source_name, target_doc=None):
|
||||
target.project = source_parent.project
|
||||
target.qty = get_remaining_qty(source)
|
||||
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
|
||||
target.actual_qty = get_bin_details(
|
||||
target.item_code, target.warehouse, source_parent.company, True
|
||||
).get("actual_qty", 0)
|
||||
|
||||
args = target.as_dict().copy()
|
||||
args.update(
|
||||
@@ -915,7 +918,10 @@ def get_events(start, end, filters=None):
|
||||
""",
|
||||
{"start": start, "end": end},
|
||||
as_dict=True,
|
||||
update={"allDay": 0},
|
||||
update={
|
||||
"allDay": 0,
|
||||
"convertToUserTz": 0,
|
||||
},
|
||||
)
|
||||
return data
|
||||
|
||||
@@ -1032,6 +1038,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"pricing_rules",
|
||||
"margin_type",
|
||||
"margin_rate_or_amount",
|
||||
],
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.ordered_qty < doc.stock_qty
|
||||
@@ -1085,9 +1093,17 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
target.payment_schedule = []
|
||||
|
||||
if is_drop_ship_order(target):
|
||||
target.customer = source.customer
|
||||
target.customer_name = source.customer_name
|
||||
target.shipping_address = source.shipping_address_name
|
||||
if source.shipping_address_name:
|
||||
target.shipping_address = source.shipping_address_name
|
||||
target.shipping_address_display = source.shipping_address
|
||||
else:
|
||||
target.shipping_address = source.customer_address
|
||||
target.shipping_address_display = source.address_display
|
||||
|
||||
target.customer_contact_person = source.contact_person
|
||||
target.customer_contact_display = source.contact_display
|
||||
target.customer_contact_mobile = source.contact_mobile
|
||||
target.customer_contact_email = source.contact_email
|
||||
else:
|
||||
target.customer = target.customer_name = target.shipping_address = None
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ frappe.views.calendar["Sales Order"] = {
|
||||
id: "name",
|
||||
title: "customer_name",
|
||||
allDay: "allDay",
|
||||
convertToUserTz: "convertToUserTz",
|
||||
},
|
||||
gantt: true,
|
||||
filters: [
|
||||
|
||||
@@ -14,6 +14,8 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Quotation": ["items", "prevdoc_docname"],
|
||||
"BOM": ["items", "bom_no"],
|
||||
"Blanket Order": ["items", "blanket_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{
|
||||
@@ -22,8 +24,8 @@ def get_data():
|
||||
},
|
||||
{"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]},
|
||||
{"label": _("Projects"), "items": ["Project"]},
|
||||
{"label": _("Manufacturing"), "items": ["Work Order"]},
|
||||
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat"]},
|
||||
{"label": _("Manufacturing"), "items": ["Work Order", "BOM", "Blanket Order"]},
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.get_item_details import get_bin_details
|
||||
|
||||
|
||||
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
@@ -99,6 +100,12 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(mr.material_request_type, "Purchase")
|
||||
self.assertEqual(len(mr.get("items")), len(so.get("items")))
|
||||
|
||||
for item in mr.get("items"):
|
||||
actual_qty = get_bin_details(item.item_code, item.warehouse, mr.company, True).get(
|
||||
"actual_qty", 0
|
||||
)
|
||||
self.assertEqual(flt(item.actual_qty), actual_qty)
|
||||
|
||||
def test_make_delivery_note(self):
|
||||
so = make_sales_order(do_not_submit=True)
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
};
|
||||
this.warehouse_control.df.get_query = () => {
|
||||
return {
|
||||
filters: { company: this.events.get_frm().doc.company },
|
||||
filters: { company: this.events.get_frm().doc.company, is_group: 0 },
|
||||
};
|
||||
};
|
||||
this.warehouse_control.refresh();
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
extend_cscript(cur_frm.cscript, {
|
||||
onload: function () {
|
||||
if (cur_frm.doc.__islocal) {
|
||||
cur_frm.set_value("to_currency", frappe.defaults.get_global_default("currency"));
|
||||
frappe.ui.form.on("Currency Exchange", {
|
||||
onload: function (frm) {
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value("to_currency", frappe.defaults.get_global_default("currency"));
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
cur_frm.cscript.set_exchange_rate_label();
|
||||
refresh: function (frm) {
|
||||
// Don't trigger on Quick Entry form
|
||||
if (typeof frm.is_dialog === "undefined") {
|
||||
frm.trigger("set_exchange_rate_label");
|
||||
}
|
||||
},
|
||||
|
||||
from_currency: function () {
|
||||
cur_frm.cscript.set_exchange_rate_label();
|
||||
from_currency: function (frm) {
|
||||
frm.trigger("set_exchange_rate_label");
|
||||
},
|
||||
|
||||
to_currency: function () {
|
||||
cur_frm.cscript.set_exchange_rate_label();
|
||||
to_currency: function (frm) {
|
||||
frm.trigger("set_exchange_rate_label");
|
||||
},
|
||||
|
||||
set_exchange_rate_label: function () {
|
||||
if (cur_frm.doc.from_currency && cur_frm.doc.to_currency) {
|
||||
var default_label = __(frappe.meta.docfield_map[cur_frm.doctype]["exchange_rate"].label);
|
||||
cur_frm.fields_dict.exchange_rate.set_label(
|
||||
default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", cur_frm.doc)
|
||||
set_exchange_rate_label: function (frm) {
|
||||
if (frm.doc.from_currency && frm.doc.to_currency) {
|
||||
var default_label = __(frappe.meta.docfield_map[frm.doctype]["exchange_rate"].label);
|
||||
frm.fields_dict.exchange_rate.set_label(
|
||||
default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", frm.doc)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user