mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-09 15:29:30 +00:00
Compare commits
1 Commits
v15.121.2
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c92bef7c |
2
.github/CONTRIBUTING.md
vendored
2
.github/CONTRIBUTING.md
vendored
@@ -6,7 +6,7 @@ Feature requests are also a great way to take the product forward. New ideas can
|
||||
|
||||
When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want.
|
||||
|
||||
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.frappe.io](https://discuss.frappe.io/c/erpnext/6).
|
||||
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com).
|
||||
|
||||
### Reply and Closing Policy
|
||||
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -9,7 +9,7 @@ body:
|
||||
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
||||
|
||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6)
|
||||
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
|
||||
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
|
||||
2. When making a bug report, make sure you provide all required information. The easier it is for
|
||||
maintainers to reproduce, the faster it'll be fixed.
|
||||
@@ -60,7 +60,7 @@ body:
|
||||
description: Share exact version number of Frappe and ERPNext you are using.
|
||||
placeholder: |
|
||||
Frappe version -
|
||||
ERPNext version -
|
||||
ERPNext Verion -
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Forum
|
||||
url: https://discuss.frappe.io/c/erpnext/6
|
||||
url: https://discuss.erpnext.com/
|
||||
about: For general QnA, discussions and community help.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
4
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -11,7 +11,7 @@ assignees: ''
|
||||
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
|
||||
|
||||
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
|
||||
- For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6
|
||||
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
|
||||
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
|
||||
the original discussion.
|
||||
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
|
||||
@@ -21,7 +21,7 @@ Please keep in mind that we get many many requests and we can't possibly work on
|
||||
|
||||
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
|
||||
1. Certified ERPNext partners: https://erpnext.com/partners
|
||||
2. Developer community on ERPNext forums: https://discuss.frappe.io/c/framework/5
|
||||
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
|
||||
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
|
||||
|
||||
-->
|
||||
|
||||
14
.github/workflows/linters.yml
vendored
14
.github/workflows/linters.yml
vendored
@@ -18,19 +18,7 @@ jobs:
|
||||
cache: pip
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v3.0.1
|
||||
|
||||
semgrep:
|
||||
name: semgrep
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: pip
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"disabledLabels": [
|
||||
"conflicts"
|
||||
],
|
||||
"context": {
|
||||
"repos": [
|
||||
"frappe/frappe"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ repos:
|
||||
cypress/.*|
|
||||
.*node_modules.*|
|
||||
.*boilerplate.*|
|
||||
erpnext/public/js/controllers/.*|
|
||||
erpnext/templates/pages/order.js|
|
||||
erpnext/templates/includes/.*
|
||||
)$
|
||||
|
||||
@@ -64,7 +64,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
|
||||
|
||||
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
|
||||
3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with community of ERPNext users and service providers.
|
||||
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
|
||||
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.121.2"
|
||||
__version__ = "15.72.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
126
erpnext/accounts/custom/address.json
Normal file
126
erpnext/accounts/custom/address.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"custom_fields": [
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2018-12-28 22:29:21.828090",
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 15,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "fax",
|
||||
"label": "Tax Category",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2018-12-28 22:29:21.828090",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-tax_category",
|
||||
"no_copy": 0,
|
||||
"options": "Tax Category",
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2020-10-14 17:41:40.878179",
|
||||
"default": "0",
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "is_your_company_address",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 20,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "linked_with",
|
||||
"label": "Is Your Company Address",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2020-10-14 17:41:40.878179",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-is_your_company_address",
|
||||
"no_copy": 0,
|
||||
"options": null,
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"custom_perms": [],
|
||||
"doctype": "Address",
|
||||
"property_setters": [],
|
||||
"sync_on_migrate": 1
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class ERPNextAddress(Address):
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
if self.get("is_your_company_address"):
|
||||
if self.is_your_company_address:
|
||||
return
|
||||
|
||||
return super().link_address()
|
||||
@@ -26,9 +26,7 @@ class ERPNextAddress(Address):
|
||||
self.is_your_company_address = 1
|
||||
|
||||
def validate_reference(self):
|
||||
if self.get("is_your_company_address") and not [
|
||||
row for row in self.links if row.link_doctype == "Company"
|
||||
]:
|
||||
if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Address needs to be linked to a Company. Please add a row for Company in the Links table."
|
||||
@@ -65,6 +63,4 @@ def get_shipping_address(company, address=None):
|
||||
if address:
|
||||
address_as_dict = address[0]
|
||||
name, address_template = get_address_templates(address_as_dict)
|
||||
return address_as_dict.get("name"), frappe.render_template(
|
||||
address_template, address_as_dict, restrict_globals=True
|
||||
)
|
||||
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict)
|
||||
|
||||
@@ -46,8 +46,7 @@ def validate_service_stop_date(doc):
|
||||
if (
|
||||
old_stop_dates
|
||||
and old_stop_dates.get(item.name)
|
||||
and item.service_stop_date
|
||||
and getdate(item.service_stop_date) != getdate(old_stop_dates.get(item.name))
|
||||
and item.service_stop_date != old_stop_dates.get(item.name)
|
||||
):
|
||||
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@ class Account(NestedSet):
|
||||
self.validate_parent_child_account_type()
|
||||
self.validate_root_details()
|
||||
self.validate_account_number()
|
||||
self.validate_disabled()
|
||||
self.validate_group_or_ledger()
|
||||
self.set_root_and_report_type()
|
||||
self.validate_mandatory()
|
||||
@@ -119,7 +118,6 @@ class Account(NestedSet):
|
||||
self.validate_account_currency()
|
||||
self.validate_root_company_and_sync_account_to_children()
|
||||
self.validate_receivable_payable_account_type()
|
||||
self.validate_stock_account_type_change()
|
||||
|
||||
def validate_parent_child_account_type(self):
|
||||
if self.parent_account:
|
||||
@@ -208,36 +206,6 @@ class Account(NestedSet):
|
||||
frappe.msgprint(msg)
|
||||
self.add_comment("Comment", msg)
|
||||
|
||||
def validate_stock_account_type_change(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not (doc_before_save and doc_before_save.account_type == "Stock"):
|
||||
return
|
||||
|
||||
if self.account_type == "Stock":
|
||||
return
|
||||
|
||||
if self.stock_ledger_entry_exists():
|
||||
frappe.throw(
|
||||
_(
|
||||
"The account type of {0} cannot be changed from {1} because stock ledger entries exist against it."
|
||||
).format(frappe.bold(self.name), frappe.bold(_("Stock")))
|
||||
)
|
||||
|
||||
def stock_ledger_entry_exists(self):
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
|
||||
warehouse_account = get_warehouse_account_map(self.company)
|
||||
warehouses = [wh for wh, details in warehouse_account.items() if details.account == self.name]
|
||||
if not warehouses:
|
||||
return False
|
||||
|
||||
return bool(
|
||||
frappe.db.count(
|
||||
"Stock Ledger Entry",
|
||||
filters={"warehouse": ("in", warehouses), "is_cancelled": 0},
|
||||
)
|
||||
)
|
||||
|
||||
def validate_root_details(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
|
||||
@@ -286,14 +254,6 @@ class Account(NestedSet):
|
||||
|
||||
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
|
||||
|
||||
def validate_disabled(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or cint(doc_before_save.disabled) == cint(self.disabled):
|
||||
return
|
||||
|
||||
if cint(self.disabled):
|
||||
self.validate_default_accounts_in_company()
|
||||
|
||||
def validate_group_or_ledger(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group):
|
||||
@@ -304,32 +264,9 @@ class Account(NestedSet):
|
||||
elif cint(self.is_group):
|
||||
if self.account_type and not self.flags.exclude_account_type_check:
|
||||
throw(_("Cannot covert to Group because Account Type is selected."))
|
||||
self.validate_default_accounts_in_company()
|
||||
elif self.check_if_child_exists():
|
||||
throw(_("Account with child nodes cannot be set as ledger"))
|
||||
|
||||
def validate_default_accounts_in_company(self):
|
||||
default_account_fields = get_company_default_account_fields()
|
||||
|
||||
company_default_accounts = frappe.db.get_value(
|
||||
"Company", self.company, list(default_account_fields.keys()), as_dict=1
|
||||
)
|
||||
|
||||
msg = _("Account {0} cannot be disabled as it is already set as {1} for {2}.")
|
||||
|
||||
if not self.disabled:
|
||||
msg = _("Account {0} cannot be converted to Group as it is already set as {1} for {2}.")
|
||||
|
||||
for d in default_account_fields:
|
||||
if company_default_accounts.get(d) == self.name:
|
||||
throw(
|
||||
msg.format(
|
||||
frappe.bold(self.name),
|
||||
frappe.bold(default_account_fields.get(d)),
|
||||
frappe.bold(self.company),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_frozen_accounts_modifier(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
|
||||
@@ -548,7 +485,6 @@ def get_account_autoname(account_number, account_name, company):
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.get_cached_doc("Account", name)
|
||||
account.check_permission("write")
|
||||
if not account:
|
||||
return
|
||||
|
||||
@@ -610,12 +546,10 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
@frappe.whitelist()
|
||||
def merge_account(old, new):
|
||||
_ensure_idle_system()
|
||||
# Validate properties before merging
|
||||
new_account = frappe.get_cached_doc("Account", new)
|
||||
old_account = frappe.get_cached_doc("Account", old)
|
||||
|
||||
new_account.check_permission("write")
|
||||
old_account.check_permission("write")
|
||||
|
||||
if not new_account:
|
||||
throw(_("Account {0} does not exist").format(new))
|
||||
|
||||
@@ -693,27 +627,3 @@ def _ensure_idle_system():
|
||||
).format(pretty_date(last_gl_update)),
|
||||
title=_("System In Use"),
|
||||
)
|
||||
|
||||
|
||||
def get_company_default_account_fields():
|
||||
return {
|
||||
"default_bank_account": "Default Bank Account",
|
||||
"default_cash_account": "Default Cash Account",
|
||||
"default_receivable_account": "Default Receivable Account",
|
||||
"default_payable_account": "Default Payable Account",
|
||||
"default_expense_account": "Default Expense Account",
|
||||
"default_income_account": "Default Income Account",
|
||||
"stock_received_but_not_billed": "Stock Received But Not Billed Account",
|
||||
"stock_adjustment_account": "Stock Adjustment Account",
|
||||
"write_off_account": "Write Off Account",
|
||||
"default_discount_account": "Default Payment Discount Account",
|
||||
"unrealized_profit_loss_account": "Unrealized Profit / Loss Account",
|
||||
"exchange_gain_loss_account": "Exchange Gain / Loss Account",
|
||||
"unrealized_exchange_gain_loss_account": "Unrealized Exchange Gain / Loss Account",
|
||||
"round_off_account": "Round Off Account",
|
||||
"default_deferred_revenue_account": "Default Deferred Revenue Account",
|
||||
"default_deferred_expense_account": "Default Deferred Expense Account",
|
||||
"accumulated_depreciation_account": "Accumulated Depreciation Account",
|
||||
"depreciation_expense_account": "Depreciation Expense Account",
|
||||
"disposal_account": "Gain/Loss Account on Asset Disposal",
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,7 @@
|
||||
"account_number": "11530"
|
||||
},
|
||||
"account_number": "115",
|
||||
"is_group": 1,
|
||||
"account_type": "Bank"
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Receivables": {
|
||||
"Trade Debtors": {
|
||||
@@ -530,13 +529,6 @@
|
||||
"account_number": "630",
|
||||
"is_group": 1
|
||||
},
|
||||
"Accrued Manufacturing Expenses": {
|
||||
"Accrued Expenses - Manufacturing": {
|
||||
"account_number": "63510"
|
||||
},
|
||||
"account_number": "635",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "63",
|
||||
"is_group": 1
|
||||
},
|
||||
@@ -822,4 +814,4 @@
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -34,13 +34,6 @@
|
||||
"account_number": "0430",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Anlagen im Bau": {
|
||||
"is_group": 1,
|
||||
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
|
||||
"account_number": "0498",
|
||||
"account_type": "Capital Work in Progress"
|
||||
}
|
||||
},
|
||||
"Accumulated Depreciation": {
|
||||
"account_type": "Accumulated Depreciation"
|
||||
}
|
||||
@@ -324,21 +317,13 @@
|
||||
"account_number": "3800",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
|
||||
"account_number": "3960",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Herstellungskosten": {
|
||||
"account_number": "4996",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
|
||||
"account_number": "2310",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
|
||||
"account_number": "2320",
|
||||
"account_type": "Expense Account"
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Verwaltungskosten": {
|
||||
"account_number": "4997",
|
||||
@@ -355,7 +340,7 @@
|
||||
"is_group": 1,
|
||||
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
|
||||
"account_number": "4830",
|
||||
"account_type": "Depreciation"
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"Abschreibungen auf Gebäude": {
|
||||
"account_number": "4831",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,17 +33,6 @@
|
||||
},
|
||||
"account_number": "1151.000"
|
||||
},
|
||||
"Pajak Dibayar di Muka": {
|
||||
"PPN Masukan": {
|
||||
"account_number": "1152.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"PPh 23 Dibayar di Muka": {
|
||||
"account_number": "1152.002",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "1152.000"
|
||||
},
|
||||
"account_number": "1150.000"
|
||||
},
|
||||
"Kas": {
|
||||
|
||||
@@ -1,449 +0,0 @@
|
||||
{
|
||||
"country_code": "nz",
|
||||
"name": "New Zealand - Chart of Accounts with Account Numbers",
|
||||
"disabled": "No",
|
||||
"tree": {
|
||||
"Application of Funds (Assets)": {
|
||||
"Current Assets": {
|
||||
"Bank Accounts": {
|
||||
"Business Transaction Account": {
|
||||
"account_number": "11011",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Business Savings Account": {
|
||||
"account_number": "11012",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"account_number": "11010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Cash on Hand": {
|
||||
"account_number": "11020",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Accounts Receivable": {
|
||||
"Debtors": {
|
||||
"account_number": "11210",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Provision for Doubtful Debts": {
|
||||
"account_number": "11220"
|
||||
},
|
||||
"account_number": "11200",
|
||||
"is_group": 1
|
||||
},
|
||||
"Inventory": {
|
||||
"Stock on Hand": {
|
||||
"account_number": "11311",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"Work In Progress": {
|
||||
"account_number": "11312",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"account_number": "11310",
|
||||
"account_type": "Stock",
|
||||
"is_group": 1
|
||||
},
|
||||
"Prepayments": {
|
||||
"Prepayments": {
|
||||
"account_number": "11411"
|
||||
},
|
||||
"Supplier Advances": {
|
||||
"account_number": "11412"
|
||||
},
|
||||
"Deferred Expense": {
|
||||
"account_number": "11413"
|
||||
},
|
||||
"account_number": "11410",
|
||||
"is_group": 1
|
||||
},
|
||||
"GST Receivable": {
|
||||
"account_number": "11510",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Income Tax Receivable": {
|
||||
"account_number": "11520",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "11000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Fixed Assets": {
|
||||
"Plant & Equipment": {
|
||||
"Plant & Equipment": {
|
||||
"account_number": "16011",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Plant & Equipment": {
|
||||
"account_number": "16012",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Motor Vehicles": {
|
||||
"Motor Vehicles": {
|
||||
"account_number": "16021",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Motor Vehicles": {
|
||||
"account_number": "16022",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16020",
|
||||
"is_group": 1
|
||||
},
|
||||
"Office Equipment": {
|
||||
"Office Equipment": {
|
||||
"account_number": "16031",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Office Equipment": {
|
||||
"account_number": "16032",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16030",
|
||||
"is_group": 1
|
||||
},
|
||||
"Buildings": {
|
||||
"Buildings": {
|
||||
"account_number": "16041",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Buildings": {
|
||||
"account_number": "16042",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16040",
|
||||
"is_group": 1
|
||||
},
|
||||
"Computer Equipment": {
|
||||
"Computer Equipment": {
|
||||
"account_number": "16051",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Computer Equipment": {
|
||||
"account_number": "16052",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16050",
|
||||
"is_group": 1
|
||||
},
|
||||
"Capital Work in Progress": {
|
||||
"account_number": "16090",
|
||||
"account_type": "Capital Work in Progress"
|
||||
},
|
||||
"account_number": "16000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "10000",
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Source of Funds (Liabilities)": {
|
||||
"Current Liabilities": {
|
||||
"Accounts Payable": {
|
||||
"Creditors": {
|
||||
"account_number": "21010",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_number": "21000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Goods Received Not Invoiced": {
|
||||
"account_number": "21100",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Asset Received Not Invoiced": {
|
||||
"account_number": "21110",
|
||||
"account_type": "Asset Received But Not Billed"
|
||||
},
|
||||
"Service Received Not Invoiced": {
|
||||
"account_number": "21120",
|
||||
"account_type": "Service Received But Not Billed"
|
||||
},
|
||||
"Accrued Expenses": {
|
||||
"account_number": "21200"
|
||||
},
|
||||
"Wages Payable": {
|
||||
"account_number": "21300"
|
||||
},
|
||||
"PAYE Payable": {
|
||||
"account_number": "22010"
|
||||
},
|
||||
"KiwiSaver Payable": {
|
||||
"account_number": "22020"
|
||||
},
|
||||
"ACC Payable": {
|
||||
"account_number": "22030"
|
||||
},
|
||||
"Credit Cards": {
|
||||
"Business Credit Card": {
|
||||
"account_number": "22110"
|
||||
},
|
||||
"account_number": "22100",
|
||||
"is_group": 1
|
||||
},
|
||||
"Customer Advances": {
|
||||
"account_number": "22200"
|
||||
},
|
||||
"Deferred Revenue": {
|
||||
"account_number": "22210"
|
||||
},
|
||||
"Provisional Account": {
|
||||
"account_number": "22220"
|
||||
},
|
||||
"Tax Liabilities": {
|
||||
"GST Payable": {
|
||||
"account_number": "22310",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"GST Suspense": {
|
||||
"account_number": "22320",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"FBT Payable": {
|
||||
"account_number": "22330",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Income Tax Payable": {
|
||||
"account_number": "22340",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "22300",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "21500",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non-Current Liabilities": {
|
||||
"Bank Loans": {
|
||||
"Bank Loan": {
|
||||
"account_number": "25011"
|
||||
},
|
||||
"account_number": "25010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities": {
|
||||
"Lease Liability": {
|
||||
"account_number": "25021"
|
||||
},
|
||||
"account_number": "25020",
|
||||
"is_group": 1
|
||||
},
|
||||
"Shareholder Loans": {
|
||||
"Shareholder Loan": {
|
||||
"account_number": "25031"
|
||||
},
|
||||
"account_number": "25030",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "25000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "20000",
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Equity": {
|
||||
"Share Capital": {
|
||||
"account_number": "31010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Drawings": {
|
||||
"account_number": "31020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Current Year Earnings": {
|
||||
"account_number": "35010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Retained Earnings": {
|
||||
"account_number": "35020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "30000",
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"Income": {
|
||||
"Sales": {
|
||||
"account_number": "41010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Other Income": {
|
||||
"Interest Income": {
|
||||
"account_number": "47010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Rounding Gain/Loss": {
|
||||
"account_number": "47020",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Foreign Exchange Gain": {
|
||||
"account_number": "47030",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"account_number": "47000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "40000",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Expenses": {
|
||||
"Cost of Goods Sold": {
|
||||
"Purchases": {
|
||||
"account_number": "51010",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Freight Inwards": {
|
||||
"account_number": "51020",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Duty and Landing Costs": {
|
||||
"account_number": "51030",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Stock Adjustment": {
|
||||
"account_number": "51040",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Write Off": {
|
||||
"account_number": "51050",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"account_number": "51000",
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"is_group": 1
|
||||
},
|
||||
"Operating Expenses": {
|
||||
"Wages & Salaries": {
|
||||
"account_number": "61010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"KiwiSaver Employer Contribution": {
|
||||
"account_number": "61020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"ACC Levies": {
|
||||
"account_number": "61030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Rent": {
|
||||
"account_number": "65010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Power": {
|
||||
"account_number": "65020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Telephone": {
|
||||
"account_number": "66010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Insurance": {
|
||||
"account_number": "64010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Accounting Fees": {
|
||||
"account_number": "64020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Legal Fees": {
|
||||
"account_number": "64030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Advertising and Marketing": {
|
||||
"account_number": "65030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Repairs and Maintenance": {
|
||||
"account_number": "65040",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Freight and Courier": {
|
||||
"account_number": "65050",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Operating Costs": {
|
||||
"account_number": "65060",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "60000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Depreciation and Amortisation": {
|
||||
"Depreciation - Plant & Equipment": {
|
||||
"account_number": "62010",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Motor Vehicles": {
|
||||
"account_number": "62020",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Office Equipment": {
|
||||
"account_number": "62030",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Computer Equipment": {
|
||||
"account_number": "62040",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"account_number": "62000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Finance Costs": {
|
||||
"Bank Charges": {
|
||||
"account_number": "67010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Interest Expense": {
|
||||
"account_number": "67020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Rounding Off": {
|
||||
"account_number": "67030",
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
"Payment Discounts": {
|
||||
"account_number": "67040",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "67000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Income Tax Expense": {
|
||||
"account_number": "81010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Foreign Exchange": {
|
||||
"Exchange Gain/Loss": {
|
||||
"account_number": "82010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Unrealized Exchange Gain/Loss": {
|
||||
"account_number": "82020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "82000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bad Debts": {
|
||||
"account_number": "83010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Write Off": {
|
||||
"account_number": "83020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Gain/Loss on Asset Disposal": {
|
||||
"account_number": "83030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Expenses Included In Asset Valuation": {
|
||||
"account_number": "84010",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"account_number": "50000",
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,839 +0,0 @@
|
||||
{
|
||||
"name": "Philippines",
|
||||
"country": "Philippines",
|
||||
"tree": {
|
||||
"Asset": {
|
||||
"account_number": "1000",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Current Assets": {
|
||||
"account_number": "1001",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Cash": {
|
||||
"account_number": "1100",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Cash",
|
||||
"Cash on Hand": {
|
||||
"account_number": "1101",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Petty Cash Fund": {
|
||||
"account_number": "1200",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Cash",
|
||||
"Petty Cash Fund": {
|
||||
"account_number": "1201",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Cash"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bank Accounts": {
|
||||
"account_number": "1102",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Advances to Officers & Employees": {
|
||||
"account_number": "1290",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Advances to Officers & Employees": {
|
||||
"account_number": "1291",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"Accounts Receivable Trade": {
|
||||
"account_number": "1300",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Accounts Receivable - Trade": {
|
||||
"account_number": "1301",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Receivable"
|
||||
}
|
||||
},
|
||||
"Accounts Receivable - Affiliates": {
|
||||
"account_number": "1310",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Due from Company": {
|
||||
"account_number": "1311",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"Accounts Receivable - Others": {
|
||||
"account_number": "1400",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Accounts Receivable - Others": {
|
||||
"account_number": "1401",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"Parts, Materials and Supplies": {
|
||||
"account_number": "1500",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Parts, Materials and Supplies": {
|
||||
"account_number": "1501",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Raw Materials - Demo": {
|
||||
"account_number": "1502",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"Project in Progress": {
|
||||
"account_number": "1510",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Project in Progress": {
|
||||
"account_number": "1511",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Factory Overhead Variance": {
|
||||
"account_number": "1512",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"Finished Goods": {
|
||||
"account_number": "1520",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Finished Goods Inventory": {
|
||||
"account_number": "1531",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"Inventory in Transit": {
|
||||
"account_number": "1532",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Stock Adjustment"
|
||||
}
|
||||
},
|
||||
"Prepayments": {
|
||||
"account_number": "1600",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Prepaid Insurance & Bonds": {
|
||||
"account_number": "1601",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Prepaid Rent": {
|
||||
"account_number": "1602",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"VAT Input Tax": {
|
||||
"account_number": "1610",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"VAT Input Tax - Goods": {
|
||||
"account_number": "1611",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Non - Current Assets": {
|
||||
"account_number": "1002",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Property, Plants And Equipments": {
|
||||
"account_number": "1700",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Land": {
|
||||
"account_number": "1701",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Buildings & Improvements": {
|
||||
"account_number": "1702",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Delivery & Trans Equipment": {
|
||||
"account_number": "1703",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Furniture & Fixtures": {
|
||||
"account_number": "1704",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Machinery & Equipment": {
|
||||
"account_number": "1705",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Fixed Asset"
|
||||
}
|
||||
},
|
||||
"Accum Depr. - Property, Plants and Equipment": {
|
||||
"account_number": "1800",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Accumulated Dep Bdgs & Improv": {
|
||||
"account_number": "1801",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"Accumulated Dep Delivery & Trans": {
|
||||
"account_number": "1802",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"Accumulated Dep Furniture & Fixture": {
|
||||
"account_number": "1803",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"Accumulated Depreciation - Machinery & Equipment": {
|
||||
"account_number": "1804",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Other Assets": {
|
||||
"account_number": "1003",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Advances To Supplier": {
|
||||
"account_number": "1900",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Advances To Supplier": {
|
||||
"account_number": "1901",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"Miscellaneous Deposits": {
|
||||
"account_number": "1910",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Miscellaneous Deposits": {
|
||||
"account_number": "1911",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"Retirement Fund": {
|
||||
"account_number": "1920",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Retirement Fund": {
|
||||
"account_number": "1921",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"Investment": {
|
||||
"account_number": "1930",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"Investment": {
|
||||
"account_number": "1931",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
},
|
||||
"System Development": {
|
||||
"account_number": "1940",
|
||||
"is_group": 1,
|
||||
"root_type": "Asset",
|
||||
"System Development": {
|
||||
"account_number": "1941",
|
||||
"is_group": 0,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Liability": {
|
||||
"account_number": "2000",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Current Liabilities": {
|
||||
"account_number": "2001",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Accounts Payable Trade": {
|
||||
"account_number": "2100",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Accounts Payable - Trade": {
|
||||
"account_number": "2101",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability",
|
||||
"account_type": "Payable"
|
||||
}
|
||||
},
|
||||
"Accounts Payable Others": {
|
||||
"account_number": "2110",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Accounts Payable - Payroll": {
|
||||
"account_number": "2111",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
}
|
||||
},
|
||||
"VAT Output Tax": {
|
||||
"account_number": "2200",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"VAT Output Tax": {
|
||||
"account_number": "2201",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
},
|
||||
"Withholding Taxes Payable Wages": {
|
||||
"account_number": "2210",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Withholding Taxes Payable Wages": {
|
||||
"account_number": "2211",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
},
|
||||
"Withholding Taxes Payable Expanded": {
|
||||
"account_number": "2220",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Withholding Taxes Payable Expanded": {
|
||||
"account_number": "2221",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability",
|
||||
"account_type": "Tax"
|
||||
}
|
||||
},
|
||||
"Accruals And Other Current Payables": {
|
||||
"account_number": "2300",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Stock Received But Not Billed": {
|
||||
"account_number": "2301",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
}
|
||||
},
|
||||
"Payable to Government and Other Institutions": {
|
||||
"account_number": "2400",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"SSS Premium Payable": {
|
||||
"account_number": "2401",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"SSS Salary Loan Payable": {
|
||||
"account_number": "2402",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"PhilHealth Premium": {
|
||||
"account_number": "2403",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Pag-ibig Loan Payable": {
|
||||
"account_number": "2404",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Coop Loans": {
|
||||
"account_number": "2405",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Coop Contributions": {
|
||||
"account_number": "2406",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Canteen": {
|
||||
"account_number": "2407",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"AUB Loan Payable": {
|
||||
"account_number": "2408",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"HSBC Loan Payable": {
|
||||
"account_number": "2409",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
}
|
||||
},
|
||||
"Customer Deposits": {
|
||||
"account_number": "2500",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
}
|
||||
},
|
||||
"Non Current Liabilities": {
|
||||
"account_number": "2002",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Due To Associated Company": {
|
||||
"account_number": "2600",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Due To Associated Company": {
|
||||
"account_number": "2601",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
}
|
||||
},
|
||||
"Deferred Income": {
|
||||
"account_number": "2700",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Deferred Income": {
|
||||
"account_number": "2701",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
}
|
||||
},
|
||||
"Notes Payable": {
|
||||
"account_number": "2800",
|
||||
"is_group": 1,
|
||||
"root_type": "Liability",
|
||||
"Notes Payable": {
|
||||
"account_number": "2801",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
}
|
||||
},
|
||||
"Dividends Payable": {
|
||||
"account_number": "2900",
|
||||
"is_group": 0,
|
||||
"root_type": "Liability"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Equity": {
|
||||
"account_number": "3000",
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"STOCKHOLDER'S EQUITY": {
|
||||
"account_number": "3001",
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Capital Stocks": {
|
||||
"account_number": "3100",
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Capital Stocks": {
|
||||
"account_number": "3101",
|
||||
"is_group": 0,
|
||||
"root_type": "Equity"
|
||||
}
|
||||
},
|
||||
"Subscription Receivable": {
|
||||
"account_number": "3200",
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Subscription Receivable": {
|
||||
"account_number": "3201",
|
||||
"is_group": 0,
|
||||
"root_type": "Equity"
|
||||
}
|
||||
},
|
||||
"Retained Earnings": {
|
||||
"account_number": "3300",
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Retained Earnings": {
|
||||
"account_number": "3301",
|
||||
"is_group": 0,
|
||||
"root_type": "Equity"
|
||||
}
|
||||
},
|
||||
"Current Year (Profit/Loss)": {
|
||||
"account_number": "3400",
|
||||
"is_group": 0,
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"Drawings": {
|
||||
"account_number": "3500",
|
||||
"is_group": 1,
|
||||
"root_type": "Equity",
|
||||
"Drawings": {
|
||||
"account_number": "3501",
|
||||
"is_group": 0,
|
||||
"root_type": "Equity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Income": {
|
||||
"account_number": "4000",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Gross Sales": {
|
||||
"account_number": "4100",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Sales": {
|
||||
"account_number": "4101",
|
||||
"is_group": 0,
|
||||
"root_type": "Income"
|
||||
}
|
||||
},
|
||||
"Sales Adjustment": {
|
||||
"account_number": "4200",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Sales Return And Allowance": {
|
||||
"account_number": "4201",
|
||||
"is_group": 0,
|
||||
"root_type": "Income"
|
||||
}
|
||||
},
|
||||
"Sales Discount": {
|
||||
"account_number": "4300",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Sales Discount": {
|
||||
"account_number": "4301",
|
||||
"is_group": 0,
|
||||
"root_type": "Income"
|
||||
}
|
||||
},
|
||||
"Other Income": {
|
||||
"account_number": "6000",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Interest Income Bank": {
|
||||
"account_number": "6010",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Interest Income Bank": {
|
||||
"account_number": "6011",
|
||||
"is_group": 0,
|
||||
"root_type": "Income"
|
||||
}
|
||||
},
|
||||
"Dividend Income": {
|
||||
"account_number": "6020",
|
||||
"is_group": 1,
|
||||
"root_type": "Income",
|
||||
"Dividend Income": {
|
||||
"account_number": "6021",
|
||||
"is_group": 0,
|
||||
"root_type": "Income"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Expense": {
|
||||
"account_number": "5000",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Operating Expenses": {
|
||||
"account_number": "5100",
|
||||
"is_group": 1,
|
||||
"root_type": "Expense",
|
||||
"Salaries, Wages": {
|
||||
"account_number": "5101",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"13th Month Pay & Bonus": {
|
||||
"account_number": "5102",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Overtime & Night Diff": {
|
||||
"account_number": "5103",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Incentive/Performance Bonus": {
|
||||
"account_number": "5104",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Employees Benefits": {
|
||||
"account_number": "5105",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Advertising & Promotions": {
|
||||
"account_number": "5106",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Amortization of Leasehold Improvement": {
|
||||
"account_number": "5107",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Amortization of Pre-Operating": {
|
||||
"account_number": "5108",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Amortization of System Development": {
|
||||
"account_number": "5109",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Audit & Legal Fee": {
|
||||
"account_number": "5110",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Bad Debts Expenses": {
|
||||
"account_number": "5111",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Client Service & Maintenance": {
|
||||
"account_number": "5112",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Commission Expenses": {
|
||||
"account_number": "5113",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Communications": {
|
||||
"account_number": "5114",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Contractual Services": {
|
||||
"account_number": "5115",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Depreciation Expenses": {
|
||||
"account_number": "5116",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Donation & Contribution": {
|
||||
"account_number": "5117",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Dues & Subscription": {
|
||||
"account_number": "5118",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Employee Med/Dental/Hosp Expenses": {
|
||||
"account_number": "5119",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Employee Uniforms": {
|
||||
"account_number": "5120",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Equipage": {
|
||||
"account_number": "5121",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Expenses for Reclassification": {
|
||||
"account_number": "5122",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Gas & Oil": {
|
||||
"account_number": "5123",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Insurance Expenses": {
|
||||
"account_number": "5124",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Light & Water": {
|
||||
"account_number": "5125",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Local/Overseas Travel": {
|
||||
"account_number": "5126",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Meals & Transportation Expenses": {
|
||||
"account_number": "5127",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Meeting & Conferences": {
|
||||
"account_number": "5128",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Miscellaneous Expenses": {
|
||||
"account_number": "5129",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Mockup Expenses": {
|
||||
"account_number": "5130",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Obsolescence Expenses": {
|
||||
"account_number": "5131",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Other Support Cost": {
|
||||
"account_number": "5132",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Pag-ibig Contribution": {
|
||||
"account_number": "5133",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Performance Bonds": {
|
||||
"account_number": "5134",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Pre Employment Expenses": {
|
||||
"account_number": "5135",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Professional Fees": {
|
||||
"account_number": "5136",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Recruitment & Employment": {
|
||||
"account_number": "5137",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Rent Expenses": {
|
||||
"account_number": "5138",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Rent Expenses Others": {
|
||||
"account_number": "5139",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Repairs & Maintenance": {
|
||||
"account_number": "5140",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Representation Expenses": {
|
||||
"account_number": "5141",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Research & Development": {
|
||||
"account_number": "5142",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Security Expenses": {
|
||||
"account_number": "5143",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Shared Services Fee": {
|
||||
"account_number": "5144",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"SSS/Medicare/EC Contributions": {
|
||||
"account_number": "5145",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Stationery & Supplies": {
|
||||
"account_number": "5146",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Taxes & Licenses": {
|
||||
"account_number": "5147",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Training & Seminar": {
|
||||
"account_number": "5148",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense"
|
||||
}
|
||||
},
|
||||
"Stock Adjustment": {
|
||||
"account_number": "5200",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Round Off": {
|
||||
"account_number": "5300",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense",
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
"Expenses Included In Valuation": {
|
||||
"account_number": "5400",
|
||||
"is_group": 0,
|
||||
"root_type": "Expense",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,31 +313,6 @@ class TestAccount(unittest.TestCase):
|
||||
acc.account_currency = "USD"
|
||||
self.assertRaises(frappe.ValidationError, acc.save)
|
||||
|
||||
def test_stock_account_type_change_with_ledger_entries(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
warehouse = "Stores - TCP1"
|
||||
stock_account = get_warehouse_account(frappe.get_doc("Warehouse", warehouse))
|
||||
|
||||
make_stock_entry(
|
||||
item_code="_Test Item",
|
||||
target=warehouse,
|
||||
company=company,
|
||||
qty=5,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
account = frappe.get_doc("Account", stock_account)
|
||||
self.assertEqual(account.account_type, "Stock")
|
||||
|
||||
account.account_type = ""
|
||||
self.assertRaises(frappe.ValidationError, account.save)
|
||||
|
||||
account.reload()
|
||||
account.account_name = f"{account.account_name} Updated"
|
||||
account.save() # non-type change stays allowed
|
||||
|
||||
def test_account_balance(self):
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
|
||||
|
||||
@@ -82,15 +82,13 @@ class AccountingDimension(Document):
|
||||
else:
|
||||
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
|
||||
|
||||
def on_update(self):
|
||||
def after_insert(self):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
|
||||
)
|
||||
frappe.flags.accounting_dimensions = None
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.flags.in_test:
|
||||
@@ -105,6 +103,10 @@ class AccountingDimension(Document):
|
||||
if not self.fieldname:
|
||||
self.fieldname = scrub(self.label)
|
||||
|
||||
def on_update(self):
|
||||
frappe.flags.accounting_dimensions = None
|
||||
frappe.flags.accounting_dimensions_details = None
|
||||
|
||||
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if not doclist:
|
||||
@@ -307,8 +309,8 @@ def get_dimensions(with_cost_center_and_project=False):
|
||||
if with_cost_center_and_project:
|
||||
dimension_filters.extend(
|
||||
[
|
||||
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}),
|
||||
frappe._dict({"fieldname": "project", "document_type": "Project"}),
|
||||
{"fieldname": "cost_center", "document_type": "Cost Center"},
|
||||
{"fieldname": "project", "document_type": "Project"},
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate, nowdate
|
||||
|
||||
|
||||
class OverlapError(frappe.ValidationError):
|
||||
@@ -35,20 +34,8 @@ class AccountingPeriod(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
|
||||
def validate_dates(self):
|
||||
if getdate(self.start_date) > getdate(self.end_date):
|
||||
frappe.throw(_("Start Date cannot be after End Date"))
|
||||
|
||||
if getdate(self.end_date) > getdate(nowdate()):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Accounting Period cannot be created for a future date. End Date {0} is after today."
|
||||
).format(frappe.bold(frappe.format(self.end_date, "Date")))
|
||||
)
|
||||
|
||||
def before_insert(self):
|
||||
self.bootstrap_doctypes_for_closing()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
from frappe.utils import add_months, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_period.accounting_period import (
|
||||
ClosedAccountingPeriod,
|
||||
@@ -47,7 +47,7 @@ def create_accounting_period(**args):
|
||||
|
||||
accounting_period = frappe.new_doc("Accounting Period")
|
||||
accounting_period.start_date = args.start_date or nowdate()
|
||||
accounting_period.end_date = args.end_date or nowdate()
|
||||
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
|
||||
accounting_period.company = args.company or "_Test Company"
|
||||
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
|
||||
accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
|
||||
|
||||
@@ -30,6 +30,16 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
add_taxes_from_item_tax_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_item_tax_template");
|
||||
},
|
||||
|
||||
drop_ar_procedures: function (frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "drop_ar_sql_procedures",
|
||||
callback: function (r) {
|
||||
frappe.show_alert(__("Procedures dropped"), 5);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function toggle_tax_settings(frm, field_name) {
|
||||
|
||||
@@ -56,9 +56,6 @@
|
||||
"reconciliation_queue_size",
|
||||
"column_break_resa",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"payment_entry_settings",
|
||||
"show_account_balance",
|
||||
"show_party_balance",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -78,8 +75,6 @@
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"pcv_job_timeout",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
@@ -89,19 +84,17 @@
|
||||
"enable_fuzzy_matching",
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"disable_include_dimensions",
|
||||
"column_break_lvjk",
|
||||
"general_ledger_remarks_length",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"default_ageing_range",
|
||||
"column_break_ntmi",
|
||||
"receivable_payable_remarks_length",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status",
|
||||
"column_break_xrnd"
|
||||
"create_pr_in_draft_status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -217,14 +210,13 @@
|
||||
"description": "Payment Terms from orders will be fetched into the invoices as is",
|
||||
"fieldname": "automatically_fetch_payment_terms",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Fetch Payment Terms from Order/Quotation"
|
||||
"label": "Automatically Fetch Payment Terms from Order"
|
||||
},
|
||||
{
|
||||
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
|
||||
"fieldname": "over_billing_allowance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Over Billing Allowance (%)",
|
||||
"non_negative": 1
|
||||
"label": "Over Billing Allowance (%)"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
@@ -485,7 +477,7 @@
|
||||
{
|
||||
"fieldname": "remarks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "General Ledger Report"
|
||||
"label": "Remarks Column Length"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -563,12 +555,12 @@
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor"
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Receivable / Payable Report"
|
||||
"label": "Accounts Receivable / Payable Tuning"
|
||||
},
|
||||
{
|
||||
"fieldname": "legacy_section",
|
||||
@@ -625,54 +617,18 @@
|
||||
"fieldname": "column_break_ntmi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
|
||||
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_entry_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Entry Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_account_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Account Balance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_party_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Party Balance"
|
||||
},
|
||||
{
|
||||
"default": "3600",
|
||||
"depends_on": "eval: !doc.use_legacy_controller_for_pcv",
|
||||
"description": "Timeout (in seconds) for each background job enqueued by Process Period Closing Voucher",
|
||||
"fieldname": "pcv_job_timeout",
|
||||
"fieldtype": "Int",
|
||||
"label": "PCV Job Timeout (seconds)"
|
||||
},
|
||||
{
|
||||
"default": "30, 60, 90, 120",
|
||||
"fieldname": "default_ageing_range",
|
||||
"fieldtype": "Data",
|
||||
"label": "Default Ageing Range"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_include_dimensions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable \"Consider Accounting Dimension\" Filter"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -680,7 +636,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-04 10:08:30.115003",
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -705,7 +661,6 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
|
||||
@@ -41,10 +41,8 @@ class AccountsSettings(Document):
|
||||
check_supplier_invoice_uniqueness: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
default_ageing_range: DF.Data | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
disable_include_dimensions: DF.Check
|
||||
enable_common_party_accounting: DF.Check
|
||||
enable_fuzzy_matching: DF.Check
|
||||
enable_immutable_ledger: DF.Check
|
||||
@@ -61,24 +59,20 @@ class AccountsSettings(Document):
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
pcv_job_timeout: DF.Int
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor"]
|
||||
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_account_balance: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
show_inclusive_tax_in_print: DF.Check
|
||||
show_party_balance: DF.Check
|
||||
show_payment_schedule_in_print: DF.Check
|
||||
show_taxes_as_table_in_print: DF.Check
|
||||
stale_days: DF.Int
|
||||
submit_journal_entries: DF.Check
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_legacy_controller_for_pcv: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -110,7 +104,6 @@ class AccountsSettings(Document):
|
||||
frappe.clear_cache()
|
||||
|
||||
self.validate_and_sync_auto_reconcile_config()
|
||||
self.hide_or_show_party_and_account_balance()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
@@ -118,18 +111,6 @@ class AccountsSettings(Document):
|
||||
_("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1
|
||||
)
|
||||
|
||||
def hide_or_show_party_and_account_balance(self):
|
||||
def set_property(fieldname, value):
|
||||
make_property_setter("Payment Entry", fieldname, "hidden", value, "Check")
|
||||
|
||||
if self.has_value_changed("show_party_balance"):
|
||||
set_property("party_balance", not self.show_party_balance)
|
||||
|
||||
if self.has_value_changed("show_account_balance"):
|
||||
account_fields = ["paid_from_account_balance", "paid_to_account_balance"]
|
||||
for field in account_fields:
|
||||
set_property(field, not self.show_account_balance)
|
||||
|
||||
def enable_payment_schedule_in_print(self):
|
||||
show_in_print = cint(self.show_payment_schedule_in_print)
|
||||
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
|
||||
@@ -172,3 +153,11 @@ class AccountsSettings(Document):
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def drop_ar_sql_procedures(self):
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
|
||||
|
||||
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -83,7 +82,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-13 15:11:58.300836",
|
||||
"modified": "2025-07-29 11:37:42.678556",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
|
||||
@@ -34,15 +34,3 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["against_voucher_type", "against_voucher_no"],
|
||||
)
|
||||
|
||||
frappe.db.add_index(
|
||||
"Advance Payment Ledger Entry",
|
||||
["voucher_type", "voucher_no"],
|
||||
)
|
||||
|
||||
@@ -94,11 +94,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
@@ -187,14 +187,12 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-01 00:38:53.368737",
|
||||
"modified": "2021-11-25 11:10:10.945027",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
"sort_order": "ASC"
|
||||
}
|
||||
@@ -30,7 +30,6 @@ class AdvanceTaxesandCharges(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
tax_amount: DF.Currency
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
frappe.provide("erpnext.integrations");
|
||||
|
||||
frappe.ui.form.on("Bank", {
|
||||
onload: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
},
|
||||
refresh: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
|
||||
@@ -34,11 +37,11 @@ let add_fields_to_mapping_table = function (frm) {
|
||||
});
|
||||
});
|
||||
|
||||
const grid = frm.fields_dict.bank_transaction_mapping?.grid;
|
||||
|
||||
if (grid) {
|
||||
grid.update_docfield_property("bank_transaction_field", "options", options);
|
||||
}
|
||||
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
|
||||
"bank_transaction_field",
|
||||
"options",
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
@@ -113,7 +116,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
"There was an issue connecting to Plaid's authentication server. Check browser console for more information"
|
||||
)
|
||||
);
|
||||
console.error(error);
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
plaid_success(token, response) {
|
||||
|
||||
@@ -42,4 +42,8 @@ frappe.ui.form.on("Bank Account", {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
is_company_account: function (frm) {
|
||||
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company Account",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -99,7 +98,6 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
@@ -134,8 +132,7 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "IBAN",
|
||||
"length": 34,
|
||||
"options": "IBAN"
|
||||
"length": 30
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@@ -211,7 +208,6 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
@@ -254,7 +250,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-20 00:46:16.633364",
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -286,7 +282,6 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "bank,account",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -9,8 +9,7 @@ from frappe.contacts.address_and_contact import (
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_and, get_link_to_form, validate_iban
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from frappe.utils import comma_and, get_link_to_form
|
||||
|
||||
|
||||
class BankAccount(Document):
|
||||
@@ -52,34 +51,55 @@ class BankAccount(Document):
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_is_company_account()
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
def validate_is_company_account(self):
|
||||
if self.is_company_account:
|
||||
if not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
if not self.account:
|
||||
frappe.throw(_("Company Account is mandatory"))
|
||||
|
||||
self.validate_account()
|
||||
|
||||
@deprecated
|
||||
def validate_iban(self):
|
||||
"""Kept for backward compatibility, will be removed in v16."""
|
||||
validate_iban(self.iban, throw=True)
|
||||
|
||||
def validate_account(self):
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
if self.account:
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_company(self):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is manadatory for company account"))
|
||||
|
||||
def validate_iban(self):
|
||||
"""
|
||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||
"""
|
||||
# IBAN field is optional
|
||||
if not self.iban:
|
||||
return
|
||||
|
||||
def encode_char(c):
|
||||
# Position in the alphabet (A=1, B=2, ...) plus nine
|
||||
return str(9 + ord(c) - 64)
|
||||
|
||||
# remove whitespaces, upper case to get the right number from ord()
|
||||
iban = "".join(self.iban.split(" ")).upper()
|
||||
|
||||
# Move country code and checksum from the start to the end
|
||||
flipped = iban[4:] + iban[:4]
|
||||
|
||||
# Encode characters as numbers
|
||||
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||
|
||||
try:
|
||||
to_check = int("".join(encoded))
|
||||
except ValueError:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
@@ -115,7 +135,7 @@ def get_party_bank_account(party_type, party):
|
||||
)
|
||||
|
||||
|
||||
def get_default_company_bank_account(company, party_type, party, ignore_permissions=True):
|
||||
def get_default_company_bank_account(company, party_type, party):
|
||||
default_company_bank_account = frappe.db.get_value(party_type, party, "default_bank_account")
|
||||
if default_company_bank_account:
|
||||
if company != frappe.get_cached_value("Bank Account", default_company_bank_account, "company"):
|
||||
@@ -126,20 +146,11 @@ def get_default_company_bank_account(company, party_type, party, ignore_permissi
|
||||
"Bank Account", {"company": company, "is_company_account": 1, "is_default": 1}
|
||||
)
|
||||
|
||||
if not ignore_permissions:
|
||||
default_company_bank_account = (
|
||||
default_company_bank_account
|
||||
if default_company_bank_account
|
||||
and frappe.get_cached_doc("Bank Account", default_company_bank_account).has_permission("select")
|
||||
else None
|
||||
)
|
||||
|
||||
return default_company_bank_account
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bank_account_details(bank_account):
|
||||
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
|
||||
return frappe.get_cached_value(
|
||||
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
|
||||
)
|
||||
|
||||
@@ -3,8 +3,45 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError
|
||||
|
||||
# test_records = frappe.get_test_records('Bank Account')
|
||||
|
||||
|
||||
class TestBankAccount(unittest.TestCase):
|
||||
pass
|
||||
def test_validate_iban(self):
|
||||
valid_ibans = [
|
||||
"GB82 WEST 1234 5698 7654 32",
|
||||
"DE91 1000 0000 0123 4567 89",
|
||||
"FR76 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
invalid_ibans = [
|
||||
# wrong checksum (3rd place)
|
||||
"GB72 WEST 1234 5698 7654 32",
|
||||
"DE81 1000 0000 0123 4567 89",
|
||||
"FR66 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
bank_account = frappe.get_doc({"doctype": "Bank Account"})
|
||||
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except AttributeError:
|
||||
msg = "BankAccount.validate_iban() failed for empty IBAN"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for iban in valid_ibans:
|
||||
bank_account.iban = iban
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
from pypika import Order
|
||||
|
||||
@@ -90,14 +88,7 @@ class BankClearance(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
payment_docs = []
|
||||
for d in self.get("payment_entries"):
|
||||
if d.payment_document not in payment_docs:
|
||||
payment_docs.append(d.payment_document)
|
||||
|
||||
for doctype in payment_docs:
|
||||
frappe.has_permission(doctype, "write", throw=True)
|
||||
|
||||
clearance_date_updated = False
|
||||
for d in self.get("payment_entries"):
|
||||
if d.clearance_date:
|
||||
if not d.payment_document:
|
||||
@@ -145,162 +136,65 @@ def get_payment_entries_for_bank_clearance(
|
||||
):
|
||||
entries = []
|
||||
|
||||
journal_entry = frappe.qb.DocType("Journal Entry")
|
||||
journal_entry_account = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
journal_entry_query = (
|
||||
frappe.qb.from_(journal_entry_account)
|
||||
.inner_join(journal_entry)
|
||||
.on(journal_entry_account.parent == journal_entry.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("payment_document"),
|
||||
journal_entry.name.as_("payment_entry"),
|
||||
journal_entry.cheque_no.as_("cheque_number"),
|
||||
journal_entry.cheque_date,
|
||||
Sum(journal_entry_account.debit_in_account_currency).as_("debit"),
|
||||
Sum(journal_entry_account.credit_in_account_currency).as_("credit"),
|
||||
journal_entry.posting_date,
|
||||
journal_entry_account.against_account,
|
||||
journal_entry.clearance_date,
|
||||
journal_entry_account.account_currency,
|
||||
)
|
||||
.where(
|
||||
(journal_entry_account.account == account)
|
||||
& (journal_entry.docstatus == 1)
|
||||
& (journal_entry.posting_date >= from_date)
|
||||
& (journal_entry.posting_date <= to_date)
|
||||
& (journal_entry.is_opening == "No")
|
||||
)
|
||||
)
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
if not include_reconciled_entries:
|
||||
journal_entry_query = journal_entry_query.where(
|
||||
(journal_entry.clearance_date.isnull()) | (journal_entry.clearance_date == "0000-00-00")
|
||||
)
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = (
|
||||
journal_entry_query.groupby(journal_entry_account.account, journal_entry.name)
|
||||
.orderby(journal_entry.posting_date)
|
||||
.orderby(journal_entry.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
company = frappe.qb.DocType("Company")
|
||||
payment_entry_query = (
|
||||
frappe.qb.from_(pe)
|
||||
.join(company)
|
||||
.on(pe.company == company.name)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("payment_document"),
|
||||
pe.name.as_("payment_entry"),
|
||||
pe.reference_no.as_("cheque_number"),
|
||||
pe.reference_date.as_("cheque_date"),
|
||||
(
|
||||
Case()
|
||||
.when(
|
||||
pe.paid_from == account,
|
||||
(
|
||||
pe.paid_amount
|
||||
+ (
|
||||
Case()
|
||||
.when(
|
||||
(pe.payment_type == "Pay")
|
||||
& (company.default_currency == pe.paid_from_account_currency),
|
||||
pe.base_total_taxes_and_charges,
|
||||
)
|
||||
.else_(pe.total_taxes_and_charges)
|
||||
)
|
||||
),
|
||||
)
|
||||
.else_(0)
|
||||
).as_("credit"),
|
||||
(
|
||||
Case()
|
||||
.when(pe.paid_from == account, 0)
|
||||
.else_(
|
||||
pe.received_amount
|
||||
+ (
|
||||
Case()
|
||||
.when(
|
||||
company.default_currency == pe.paid_to_account_currency,
|
||||
pe.base_total_taxes_and_charges,
|
||||
)
|
||||
.else_(pe.total_taxes_and_charges)
|
||||
)
|
||||
)
|
||||
).as_("debit"),
|
||||
pe.posting_date,
|
||||
Coalesce(pe.party, Case().when(pe.paid_from == account, pe.paid_to).else_(pe.paid_from)).as_(
|
||||
"against_account"
|
||||
),
|
||||
pe.clearance_date,
|
||||
(
|
||||
Case()
|
||||
.when(pe.paid_to == account, pe.paid_to_account_currency)
|
||||
.else_(pe.paid_from_account_currency)
|
||||
).as_("account_currency"),
|
||||
)
|
||||
.where(
|
||||
((pe.paid_from == account) | (pe.paid_to == account))
|
||||
& (pe.docstatus == 1)
|
||||
& (pe.posting_date >= from_date)
|
||||
& (pe.posting_date <= to_date)
|
||||
)
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Journal Entry" as payment_document, t1.name as payment_entry,
|
||||
t1.cheque_no as cheque_number, t1.cheque_date,
|
||||
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
|
||||
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
|
||||
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
|
||||
and ifnull(t1.is_opening, 'No') = 'No' {condition}
|
||||
group by t2.account, t1.name
|
||||
order by t1.posting_date ASC, t1.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if not include_reconciled_entries:
|
||||
payment_entry_query = payment_entry_query.where(
|
||||
(pe.clearance_date.isnull()) | (pe.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
payment_entries = (payment_entry_query.orderby(pe.posting_date).orderby(pe.name, order=Order.desc)).run(
|
||||
as_dict=True
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry` as pe
|
||||
join `tabCompany` c on c.name = pe.company
|
||||
where
|
||||
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||
{pe_condition}
|
||||
order by
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
"from": from_date,
|
||||
"to": to_date,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
paid_purchase_invoices_query = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.paid_amount.as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.bill_no.as_("cheque_number"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("debit"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == account)
|
||||
& (pi.posting_date >= from_date)
|
||||
& (pi.posting_date <= to_date)
|
||||
)
|
||||
)
|
||||
|
||||
if not include_reconciled_entries:
|
||||
paid_purchase_invoices_query = paid_purchase_invoices_query.where(
|
||||
(pi.clearance_date.isnull()) | (pi.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
paid_purchase_invoices = (
|
||||
paid_purchase_invoices_query.orderby(pi.posting_date).orderby(pi.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
pos_sales_invoices = []
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
if include_pos_transactions:
|
||||
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
pos_sales_invoices_query = (
|
||||
pos_sales_invoices = (
|
||||
frappe.qb.from_(si_payment)
|
||||
.inner_join(si)
|
||||
.on(si_payment.parent == si.name)
|
||||
@@ -323,22 +217,38 @@ def get_payment_entries_for_bank_clearance(
|
||||
& (si.posting_date >= from_date)
|
||||
& (si.posting_date <= to_date)
|
||||
)
|
||||
)
|
||||
.orderby(si.posting_date)
|
||||
.orderby(si.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
if not include_reconciled_entries:
|
||||
pos_sales_invoices_query = pos_sales_invoices_query.where(
|
||||
(si_payment.clearance_date.isnull()) | (si_payment.clearance_date == "0000-00-00")
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
pos_purchase_invoices = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.paid_amount.as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("debit"),
|
||||
)
|
||||
|
||||
pos_sales_invoices = (
|
||||
pos_sales_invoices_query.orderby(si.posting_date).orderby(si.name, order=Order.desc)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.cash_bank_account == account)
|
||||
& (pi.posting_date >= from_date)
|
||||
& (pi.posting_date <= to_date)
|
||||
)
|
||||
.orderby(pi.posting_date)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
entries = (
|
||||
list(payment_entries)
|
||||
+ list(journal_entries)
|
||||
+ list(pos_sales_invoices)
|
||||
+ list(paid_purchase_invoices)
|
||||
list(payment_entries) + list(journal_entries) + list(pos_sales_invoices) + list(pos_purchase_invoices)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
@@ -9,10 +9,10 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
|
||||
|
||||
frappe.ui.form.on("Bank Guarantee", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("reference_doctype", function () {
|
||||
frm.set_query("bank", function () {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", ["Sales Order", "Purchase Order"]],
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
filters.append(["date", "<=", to_date])
|
||||
if from_date:
|
||||
filters.append(["date", ">=", from_date])
|
||||
transactions = frappe.get_list(
|
||||
transactions = frappe.get_all(
|
||||
"Bank Transaction",
|
||||
fields=[
|
||||
"date",
|
||||
@@ -82,7 +82,6 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
@frappe.whitelist()
|
||||
def get_account_balance(bank_account, till_date, company):
|
||||
# returns account balance till the specified date
|
||||
frappe.has_permission("Bank Account", "read", bank_account, throw=True)
|
||||
account = frappe.db.get_value("Bank Account", bank_account, "account")
|
||||
filters = frappe._dict(
|
||||
{
|
||||
@@ -305,7 +304,6 @@ def create_payment_entry_bts(
|
||||
project=None,
|
||||
cost_center=None,
|
||||
allow_edit=None,
|
||||
company_bank_account=None,
|
||||
):
|
||||
# Create a new payment entry based on the bank transaction
|
||||
bank_transaction = frappe.db.get_values(
|
||||
@@ -347,9 +345,6 @@ def create_payment_entry_bts(
|
||||
pe.project = project
|
||||
pe.cost_center = cost_center
|
||||
|
||||
if company_bank_account:
|
||||
pe.bank_account = company_bank_account
|
||||
|
||||
pe.validate()
|
||||
|
||||
if allow_edit:
|
||||
|
||||
@@ -2,15 +2,6 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Bank Statement Import", {
|
||||
onload(frm) {
|
||||
frm.set_query("bank_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
setup(frm) {
|
||||
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
|
||||
frm.import_in_progress = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:Bank Statement Import on {creation}",
|
||||
"beta": 1,
|
||||
"creation": "2019-08-04 14:16:08.318714",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -210,11 +211,10 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-30 20:51:10.353723",
|
||||
"modified": "2024-06-25 17:32:07.658250",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -230,9 +230,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,30 +105,25 @@ class BankStatementImport(DataImport):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
|
||||
bsi = frappe.get_doc("Bank Statement Import", data_import)
|
||||
bsi.check_permission()
|
||||
return bsi.get_preview_from_template(import_file, google_sheets_url)
|
||||
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
|
||||
import_file, google_sheets_url
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_import(data_import):
|
||||
bsi = frappe.get_doc("Bank Statement Import", data_import)
|
||||
bsi.check_permission("write")
|
||||
return bsi.start_import()
|
||||
return frappe.get_doc("Bank Statement Import", data_import).start_import()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_errored_template(data_import_name):
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
data_import.check_permission()
|
||||
data_import.export_errored_rows()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_import_log(data_import_name):
|
||||
bsi = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
bsi.check_permission()
|
||||
return bsi.download_import_log()
|
||||
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
|
||||
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
@@ -257,7 +252,6 @@ def get_import_status(docname):
|
||||
import_status = {}
|
||||
|
||||
data_import = frappe.get_doc("Bank Statement Import", docname)
|
||||
data_import.check_permission()
|
||||
import_status["status"] = data_import.status
|
||||
|
||||
logs = frappe.get_all(
|
||||
|
||||
@@ -38,10 +38,7 @@
|
||||
"column_break_3czf",
|
||||
"bank_party_name",
|
||||
"bank_party_account_number",
|
||||
"bank_party_iban",
|
||||
"extended_bank_statement_section",
|
||||
"included_fee",
|
||||
"excluded_fee"
|
||||
"bank_party_iban"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -119,14 +116,15 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Transaction ID",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -225,8 +223,7 @@
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)",
|
||||
"options": "IBAN"
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
@@ -236,32 +233,12 @@
|
||||
{
|
||||
"fieldname": "column_break_oufv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "extended_bank_statement_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Extended Bank Statement"
|
||||
},
|
||||
{
|
||||
"fieldname": "included_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Included Fee",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
|
||||
"fieldname": "excluded_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Excluded Fee",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-07 20:49:18.600757",
|
||||
"modified": "2025-06-18 17:24:57.044666",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -32,13 +32,11 @@ class BankTransaction(Document):
|
||||
date: DF.Date | None
|
||||
deposit: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
excluded_fee: DF.Currency
|
||||
included_fee: DF.Currency
|
||||
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
payment_entries: DF.Table[BankTransactionPayments]
|
||||
reference_number: DF.SmallText | None
|
||||
reference_number: DF.Data | None
|
||||
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
|
||||
transaction_id: DF.Data | None
|
||||
transaction_type: DF.Data | None
|
||||
@@ -47,11 +45,9 @@ class BankTransaction(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.handle_excluded_fee()
|
||||
self.update_allocated_amount()
|
||||
|
||||
def validate(self):
|
||||
self.validate_included_fee()
|
||||
self.validate_duplicate_references()
|
||||
self.validate_currency()
|
||||
|
||||
@@ -136,8 +132,6 @@ class BankTransaction(Document):
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
self.delink_payment_entry(payment_entry)
|
||||
|
||||
@@ -313,40 +307,6 @@ class BankTransaction(Document):
|
||||
|
||||
self.party_type, self.party = result
|
||||
|
||||
def validate_included_fee(self):
|
||||
"""
|
||||
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
|
||||
therefore outside of the deposit value and can be larger than the deposit itself.
|
||||
"""
|
||||
|
||||
if self.included_fee and self.withdrawal:
|
||||
if self.included_fee > self.withdrawal:
|
||||
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
|
||||
|
||||
def handle_excluded_fee(self):
|
||||
# Include the excluded fee on validate to handle all further processing the same
|
||||
excluded_fee = flt(self.excluded_fee)
|
||||
if excluded_fee <= 0:
|
||||
return
|
||||
|
||||
# Suppress a negative deposit (aka withdrawal), likely not intendend
|
||||
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
|
||||
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
|
||||
|
||||
# Enforce directionality
|
||||
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
|
||||
frappe.throw(
|
||||
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
|
||||
)
|
||||
|
||||
if flt(self.deposit) > 0:
|
||||
self.deposit = flt(self.deposit) - excluded_fee
|
||||
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
|
||||
elif flt(self.withdrawal) >= 0:
|
||||
self.withdrawal = flt(self.withdrawal) + excluded_fee
|
||||
self.included_fee = flt(self.included_fee) + excluded_fee
|
||||
self.excluded_fee = 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_bank_reconciliation():
|
||||
@@ -372,12 +332,11 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
|
||||
("unallocated_amount", "bank_account"),
|
||||
as_dict=True,
|
||||
)
|
||||
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
|
||||
|
||||
if bt_bank_account != gl_bank_account:
|
||||
if bt.bank_account != gl_bank_account:
|
||||
frappe.throw(
|
||||
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
|
||||
bt_bank_account, payment_entry.payment_entry, gl_bank_account
|
||||
bt.bank_account, payment_entry.payment_entry, gl_bank_account
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -115,36 +115,6 @@ class TestBankTransaction(FrappeTestCase):
|
||||
self.assertEqual(bank_transaction.unallocated_amount, 1700)
|
||||
self.assertEqual(bank_transaction.payment_entries, [])
|
||||
|
||||
# Amending a reconciled payment entry must not carry over its clearance date
|
||||
def test_clearance_date_cleared_on_amend(self):
|
||||
bank_transaction = frappe.get_doc(
|
||||
"Bank Transaction",
|
||||
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
|
||||
)
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
|
||||
vouchers = json.dumps(
|
||||
[
|
||||
{
|
||||
"payment_doctype": "Payment Entry",
|
||||
"payment_name": payment.name,
|
||||
"amount": bank_transaction.unallocated_amount,
|
||||
}
|
||||
]
|
||||
)
|
||||
reconcile_vouchers(bank_transaction.name, vouchers)
|
||||
|
||||
self.assertTrue(frappe.db.get_value("Payment Entry", payment.name, "clearance_date"))
|
||||
|
||||
payment.reload()
|
||||
payment.cancel()
|
||||
|
||||
amended = frappe.copy_doc(payment)
|
||||
amended.amended_from = payment.name
|
||||
amended.docstatus = 0
|
||||
amended.insert()
|
||||
|
||||
self.assertFalse(amended.clearance_date)
|
||||
|
||||
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
|
||||
def test_debit_credit_output(self):
|
||||
bank_transaction = frappe.get_doc(
|
||||
@@ -428,7 +398,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_group": "Individual",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Poore Simon's",
|
||||
}
|
||||
@@ -459,7 +429,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_group": "Individual",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_type": "Company",
|
||||
"customer_name": "Fayva",
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBankTransactionFees(FrappeTestCase):
|
||||
def test_included_fee_throws(self):
|
||||
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||
withdrawal itself."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 101
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
|
||||
|
||||
def test_included_fee_allows_equal(self):
|
||||
"""A fee that's part of a withdrawal may be equal to the withdrawal
|
||||
amount (only the fee was deducted from the account)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 100
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_included_fee_allows_for_deposit(self):
|
||||
"""For deposits, a fee may be recorded separately without limiting the
|
||||
received amount."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.included_fee = 999
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_excluded_fee_noop_when_zero(self):
|
||||
"""When there is no excluded fee to apply, the amounts should remain
|
||||
unchanged."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 5
|
||||
bt.excluded_fee = 0
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 100)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_throws_when_exceeds_deposit(self):
|
||||
"""A fee deducted from an incoming payment must not exceed the incoming
|
||||
amount (else it would be a withdrawal, a conversion we don't support)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.excluded_fee = 11
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
|
||||
"""A transaction must be either incoming or outgoing when applying a
|
||||
fee, not both."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.withdrawal = 10
|
||||
bt.excluded_fee = 1
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_deducts_from_deposit(self):
|
||||
"""When a fee is deducted from an incoming payment, the net received
|
||||
amount decreases and the fee is tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 95)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
|
||||
"""A separately-deducted fee may reduce an incoming payment to zero,
|
||||
while still tracking the fee."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 5
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_increases_outgoing_payment(self):
|
||||
"""When a separately-deducted fee is provided for an outgoing payment,
|
||||
the total money leaving increases and the fee is tracked."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 105)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
|
||||
"""If only an excluded fee is provided, it should be treated as an
|
||||
outgoing payment and the fee is then tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 5)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
@@ -121,7 +121,7 @@ class BisectAccountingStatements(Document):
|
||||
|
||||
cur_node.save()
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
@frappe.whitelist()
|
||||
def build_tree(self):
|
||||
frappe.db.delete("Bisect Nodes")
|
||||
|
||||
|
||||
@@ -98,12 +98,12 @@ class Budget(Document):
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(
|
||||
_("Account {0} does not belong to company {1}").format(d.account, self.company)
|
||||
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
|
||||
)
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as its Root Type is not of Income or Expense"
|
||||
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
|
||||
).format(d.account)
|
||||
)
|
||||
|
||||
@@ -425,11 +425,11 @@ def get_ordered_amount(args):
|
||||
|
||||
|
||||
def get_other_condition(args, for_doc):
|
||||
condition = f"expense_account = {frappe.db.escape(args.expense_account)}"
|
||||
condition = "expense_account = '%s'" % (args.expense_account)
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += f" and child.{budget_against_field} = {frappe.db.escape(args.get(budget_against_field))}"
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
@@ -437,7 +437,8 @@ def get_other_condition(args, for_doc):
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
condition += f" and parent.{date_field} between {frappe.db.escape(str(start_date))} and {frappe.db.escape(str(end_date))}"
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
@@ -357,16 +357,6 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
def test_budget_against_balance_sheet_account(self):
|
||||
budget = frappe.new_doc("Budget")
|
||||
budget.budget_against = "Cost Center"
|
||||
budget.cost_center = "_Test Cost Center - _TC"
|
||||
budget.company = "_Test Company"
|
||||
budget.fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
budget.append("accounts", {"account": "_Test Bank - _TC", "budget_amount": 200000})
|
||||
|
||||
self.assertRaisesRegex(frappe.ValidationError, "_Test Bank - _TC", budget.insert)
|
||||
|
||||
|
||||
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
||||
if budget_against_field == "project":
|
||||
|
||||
@@ -110,6 +110,18 @@ frappe.ui.form.on("Chart of Accounts Importer", {
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message === false) {
|
||||
frm.set_value("company", "");
|
||||
frappe.throw(
|
||||
__(
|
||||
"Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."
|
||||
)
|
||||
);
|
||||
} else {
|
||||
frm.trigger("refresh");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -70,37 +70,22 @@ def validate_company(company):
|
||||
frappe.throw(msg, title=_("Wrong Company"))
|
||||
|
||||
if frappe.db.get_all("GL Entry", {"company": company}, "name", limit=1):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions."
|
||||
)
|
||||
)
|
||||
|
||||
validate_user_perms(company)
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_coa(file_name, company):
|
||||
frappe.only_for("Accounts Manager")
|
||||
|
||||
# delete existing data for accounts
|
||||
frappe.has_permission("Company", "write", company, throw=True)
|
||||
unset_existing_data(company)
|
||||
|
||||
# create accounts
|
||||
file_doc, extension = get_file(file_name)
|
||||
validate_accounts(file_doc, extension)
|
||||
|
||||
if extension == "csv":
|
||||
data = generate_data_from_csv(file_doc)
|
||||
else:
|
||||
data = generate_data_from_excel(file_doc, extension)
|
||||
|
||||
validate_columns(data)
|
||||
|
||||
validate_company(company)
|
||||
|
||||
unset_existing_data(company)
|
||||
|
||||
frappe.local.flags.ignore_root_company_validation = True
|
||||
forest = build_forest(data)
|
||||
create_charts(company, custom_chart=forest, from_coa_importer=True)
|
||||
@@ -230,7 +215,6 @@ def build_forest(data):
|
||||
for row in data:
|
||||
account_name, parent_account, account_number, parent_account_number = row[0:4]
|
||||
if account_number:
|
||||
account_number = cstr(account_number).strip()
|
||||
account_name = f"{account_number} - {account_name}"
|
||||
if parent_account_number:
|
||||
parent_account_number = cstr(parent_account_number).strip()
|
||||
@@ -467,7 +451,6 @@ def unset_existing_data(company):
|
||||
fieldnames = get_linked_fields("Account").get("Company", {}).get("fieldname", [])
|
||||
linked = [{"fieldname": name} for name in fieldnames]
|
||||
update_values = {d.get("fieldname"): "" for d in linked}
|
||||
|
||||
frappe.db.set_value("Company", company, update_values, update_values)
|
||||
|
||||
# remove accounts data from various doctypes
|
||||
@@ -483,19 +466,6 @@ def unset_existing_data(company):
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
|
||||
|
||||
def validate_user_perms(company):
|
||||
# User Permission Check for Account Deletion
|
||||
company_accounts = frappe.get_query("Account", filters={"company": company}).run(as_dict=1)
|
||||
|
||||
for d in company_accounts:
|
||||
if not frappe.get_cached_doc("Account", d.name).has_permission():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Accounts cannot be removed, as user doesn't have access to all the accounts of {0}."
|
||||
).format(frappe.bold(company))
|
||||
)
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
from erpnext.setup.doctype.company.company import install_country_fixtures
|
||||
|
||||
|
||||
@@ -6,14 +6,12 @@ frappe.provide("erpnext.cheque_print");
|
||||
frappe.ui.form.on("Cheque Print Template", {
|
||||
refresh: function (frm) {
|
||||
if (!frm.doc.__islocal) {
|
||||
if (frappe.user.has_role("System Manager")) {
|
||||
frm.add_custom_button(
|
||||
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
|
||||
function () {
|
||||
erpnext.cheque_print.view_cheque_print(frm);
|
||||
}
|
||||
).addClass("btn-primary");
|
||||
}
|
||||
frm.add_custom_button(
|
||||
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
|
||||
function () {
|
||||
erpnext.cheque_print.view_cheque_print(frm);
|
||||
}
|
||||
).addClass("btn-primary");
|
||||
|
||||
$(frm.fields_dict.cheque_print_preview.wrapper).empty();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,8 +48,6 @@ class ChequePrintTemplate(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_or_update_cheque_print_format(template_name):
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
if not frappe.db.exists("Print Format", template_name):
|
||||
cheque_print = frappe.new_doc("Print Format")
|
||||
cheque_print.update(
|
||||
|
||||
@@ -11,28 +11,22 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r && r.message) {
|
||||
let result = [],
|
||||
params = {};
|
||||
if (frm.doc.service_provider == "exchangerate.host") {
|
||||
result = ["result"];
|
||||
params = {
|
||||
let result = ["result"];
|
||||
let params = {
|
||||
date: "{transaction_date}",
|
||||
from: "{from_currency}",
|
||||
to: "{to_currency}",
|
||||
};
|
||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||
result = ["rates", "{to_currency}"];
|
||||
params = {
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
symbols: "{to_currency}",
|
||||
};
|
||||
} else if (frm.doc.service_provider == "frankfurter.dev - v2") {
|
||||
result = ["rate"];
|
||||
params = {
|
||||
date: "{transaction_date}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
}
|
||||
add_param(frm, r.message, params, result);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -104,7 +104,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-15 11:25:55.873110",
|
||||
"modified": "2024-03-18 08:32:26.895076",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -121,16 +121,28 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "frankfurter.dev - v2", "Custom"]
|
||||
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
|
||||
elif self.service_provider == "frankfurter.app":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
@@ -70,14 +70,6 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "base", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
|
||||
|
||||
elif self.service_provider == "frankfurter.dev - v2":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
|
||||
self.append("result_key", {"key": "rate"})
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
|
||||
def validate_parameters(self):
|
||||
params = {}
|
||||
for row in self.req_params:
|
||||
@@ -113,20 +105,11 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in [
|
||||
"exchangerate.host",
|
||||
"frankfurter.dev",
|
||||
"frankfurter.app",
|
||||
"frankfurter.dev - v2",
|
||||
]:
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev - v2":
|
||||
api = "api.frankfurter.dev/v2/rate/{from_currency}/{to_currency}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -169,10 +169,23 @@ frappe.ui.form.on("Dunning", {
|
||||
},
|
||||
get_dunning_letter_text: function (frm) {
|
||||
if (frm.doc.dunning_type) {
|
||||
frm.call("get_dunning_letter_text").then((r) => {
|
||||
if (!r.exc) {
|
||||
frm.refresh_fields();
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
|
||||
args: {
|
||||
dunning_type: frm.doc.dunning_type,
|
||||
language: frm.doc.language,
|
||||
doc: frm.doc,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("body_text", r.message.body_text);
|
||||
frm.set_value("closing_text", r.message.closing_text);
|
||||
frm.set_value("language", r.message.language);
|
||||
} else {
|
||||
frm.set_value("body_text", "");
|
||||
frm.set_value("closing_text", "");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -234,10 +247,8 @@ frappe.ui.form.on("Dunning", {
|
||||
dn: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "naming_series:",
|
||||
"beta": 1,
|
||||
"creation": "2019-07-05 16:34:31.013238",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
@@ -399,7 +400,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-30 20:40:30.851842",
|
||||
"modified": "2024-11-26 13:46:07.760867",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
@@ -448,10 +449,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,46 +156,6 @@ class Dunning(AccountsController):
|
||||
"Serial and Batch Bundle",
|
||||
]
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dunning_letter_text(self):
|
||||
DOCTYPE = "Dunning Letter Text"
|
||||
FIELDS = ["body_text", "closing_text", "language"]
|
||||
|
||||
if not self.dunning_type:
|
||||
return
|
||||
|
||||
filters = {"parent": self.dunning_type, "is_default_language": 1}
|
||||
|
||||
if self.language:
|
||||
filters.pop("is_default_language")
|
||||
filters["language"] = self.language
|
||||
|
||||
letter_text = frappe.db.get_value(DOCTYPE, filters, FIELDS, as_dict=True)
|
||||
|
||||
if not letter_text:
|
||||
msg = (
|
||||
_("Dunning Letter for Dunning Type {0} in language '{1}' not found.").format(
|
||||
frappe.bold(self.dunning_type), frappe.bold(self.language)
|
||||
)
|
||||
if self.language
|
||||
else _("Dunning Letter for Dunning Type {0} not found.").format(
|
||||
frappe.bold(self.dunning_type)
|
||||
)
|
||||
)
|
||||
frappe.msgprint(msg, alert=True, indicator="yellow")
|
||||
|
||||
self.body_text = (
|
||||
frappe.render_template(letter_text.body_text, self.as_dict(), restrict_globals=True)
|
||||
if letter_text
|
||||
else None
|
||||
)
|
||||
self.closing_text = (
|
||||
frappe.render_template(letter_text.closing_text, self.as_dict(), restrict_globals=True)
|
||||
if letter_text
|
||||
else None
|
||||
)
|
||||
self.language = letter_text.language if letter_text else self.language
|
||||
|
||||
|
||||
def update_linked_dunnings(doc, previous_outstanding_amount):
|
||||
if (
|
||||
@@ -274,3 +234,35 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
& (overdue_payment.sales_invoice == sales_invoice)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
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")
|
||||
|
||||
letter_text = None
|
||||
if 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,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"beta": 1,
|
||||
"creation": "2019-12-04 04:59:08.003664",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -106,7 +107,7 @@
|
||||
"link_fieldname": "dunning_type"
|
||||
}
|
||||
],
|
||||
"modified": "2026-05-30 20:40:09.952533",
|
||||
"modified": "2021-11-13 00:25:35.659283",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Type",
|
||||
@@ -150,9 +151,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -3,10 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_and
|
||||
from frappe.utils.jinja import validate_template
|
||||
|
||||
|
||||
class DunningType(Document):
|
||||
@@ -33,134 +30,3 @@ class DunningType(Document):
|
||||
def autoname(self):
|
||||
company_abbr = frappe.get_value("Company", self.company, "abbr")
|
||||
self.name = f"{self.dunning_type} - {company_abbr}"
|
||||
|
||||
def validate(self):
|
||||
self.validate_dunning_letter_text()
|
||||
self.validate_income_account()
|
||||
self.validate_cost_center()
|
||||
self.set_default_dunning_type()
|
||||
|
||||
def validate_dunning_letter_text(self):
|
||||
self.validate_languages()
|
||||
self.validate_is_default_language()
|
||||
self.validate_dunning_letter_text_templates()
|
||||
|
||||
def validate_income_account(self):
|
||||
if not self.income_account:
|
||||
return
|
||||
|
||||
account = frappe.get_cached_doc("Account", self.income_account)
|
||||
|
||||
msg = []
|
||||
if account.company != self.company:
|
||||
msg.append(
|
||||
_(
|
||||
"{0} doesn't belong to Company {1}. Please select an Income Account that belongs to Company {1}."
|
||||
).format(frappe.bold(self.income_account), frappe.bold(self.company))
|
||||
)
|
||||
|
||||
if account.disabled:
|
||||
msg.append(
|
||||
_("{0} is disabled. Please select a valid Income Account.").format(
|
||||
frappe.bold(self.income_account)
|
||||
)
|
||||
)
|
||||
|
||||
if account.root_type != "Income":
|
||||
msg.append(
|
||||
_("{0} is not an Income Account. Please select a valid Income Account.").format(
|
||||
frappe.bold(self.income_account)
|
||||
)
|
||||
)
|
||||
|
||||
if account.is_group:
|
||||
msg.append(
|
||||
_("{0} is a group account. Please select a non-group Income Account.").format(
|
||||
frappe.bold(self.income_account)
|
||||
)
|
||||
)
|
||||
|
||||
if msg:
|
||||
frappe.msgprint(
|
||||
msg,
|
||||
title=_("Income Account Validation Error"),
|
||||
as_list=True,
|
||||
raise_exception=frappe.ValidationError,
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
cost_center = frappe.get_cached_doc("Cost Center", self.cost_center)
|
||||
|
||||
msg = []
|
||||
if cost_center.company != self.company:
|
||||
msg.append(
|
||||
_(
|
||||
"{0} doesn't belong to Company {1}. Please select a Cost Center that belongs to Company {1}."
|
||||
).format(frappe.bold(self.cost_center), frappe.bold(self.company))
|
||||
)
|
||||
|
||||
if cost_center.disabled:
|
||||
msg.append(
|
||||
_("{0} is disabled. Please select an enabled Cost Center.").format(
|
||||
frappe.bold(self.cost_center)
|
||||
)
|
||||
)
|
||||
|
||||
if cost_center.is_group:
|
||||
msg.append(
|
||||
_("{0} is a group Cost Center. Please select a non-group Cost Center.").format(
|
||||
frappe.bold(self.cost_center)
|
||||
)
|
||||
)
|
||||
|
||||
if msg:
|
||||
frappe.msgprint(
|
||||
msg,
|
||||
title=_("Cost Center Validation Error"),
|
||||
as_list=True,
|
||||
raise_exception=frappe.ValidationError,
|
||||
)
|
||||
|
||||
def validate_languages(self):
|
||||
languages = [d.language for d in self.dunning_letter_text]
|
||||
|
||||
if len(languages) == len(set(languages)):
|
||||
return
|
||||
|
||||
frappe.throw(_("Duplicate languages found on Dunning Letter Text. Keep only one of them."))
|
||||
|
||||
def validate_is_default_language(self):
|
||||
is_default_language_list = [
|
||||
d.language for d in self.dunning_letter_text if d.is_default_language == 1
|
||||
]
|
||||
|
||||
if len(is_default_language_list) <= 1:
|
||||
return
|
||||
|
||||
frappe.throw(
|
||||
_("{0} languages are marked as default languages. Please select only one of them.").format(
|
||||
comma_and(is_default_language_list, add_quotes=True)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dunning_letter_text_templates(self):
|
||||
for d in self.dunning_letter_text:
|
||||
if d.body_text:
|
||||
validate_template(d.body_text, restrict_globals=True)
|
||||
|
||||
if d.closing_text:
|
||||
validate_template(d.closing_text, restrict_globals=True)
|
||||
|
||||
def set_default_dunning_type(self):
|
||||
if self.is_default != 1:
|
||||
return
|
||||
|
||||
frappe.db.set_value(
|
||||
"Dunning Type",
|
||||
{"company": self.company, "is_default": 1, "name": ["!=", self.name]},
|
||||
"is_default",
|
||||
0,
|
||||
)
|
||||
|
||||
@@ -1,199 +1,9 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
def make_dunning_type(dunning_type, company="_Test Company", **kwargs):
|
||||
doc = frappe.new_doc("Dunning Type")
|
||||
doc.dunning_type = dunning_type
|
||||
doc.company = company
|
||||
doc.dunning_fee = kwargs.get("dunning_fee", 100)
|
||||
doc.rate_of_interest = kwargs.get("rate_of_interest", 5)
|
||||
doc.is_default = kwargs.get("is_default", 0)
|
||||
|
||||
if "income_account" in kwargs:
|
||||
doc.income_account = kwargs["income_account"]
|
||||
elif kwargs.get("income_account") is not False:
|
||||
doc.income_account = "Sales - _TC" if company == "_Test Company" else "Sales - _TC1"
|
||||
|
||||
if "cost_center" in kwargs:
|
||||
doc.cost_center = kwargs["cost_center"]
|
||||
elif kwargs.get("cost_center") is not False:
|
||||
doc.cost_center = "Main - _TC" if company == "_Test Company" else "Main - _TC1"
|
||||
|
||||
for row in kwargs.get("dunning_letter_text", [{"language": "en", "body_text": "Test body"}]):
|
||||
doc.append("dunning_letter_text", row)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
class TestDunningType(FrappeTestCase):
|
||||
def test_income_account_must_belong_to_company(self):
|
||||
doc = make_dunning_type("_Test Dunning Wrong Company Account", income_account="Sales - _TC1")
|
||||
self.assertRaisesRegex(frappe.ValidationError, "doesn't belong to Company", doc.insert)
|
||||
|
||||
def test_income_account_must_not_be_disabled(self):
|
||||
disabled_account = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "_Test Disabled Income Account",
|
||||
"parent_account": "Direct Income - _TC",
|
||||
"company": "_Test Company",
|
||||
"account_type": "Income Account",
|
||||
"disabled": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
doc = make_dunning_type("_Test Dunning Disabled Account", income_account=disabled_account.name)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "is disabled", doc.insert)
|
||||
|
||||
def test_income_account_must_be_income_type(self):
|
||||
doc = make_dunning_type("_Test Dunning Non Income Account", income_account="Debtors - _TC")
|
||||
self.assertRaisesRegex(frappe.ValidationError, "is not an Income Account", doc.insert)
|
||||
|
||||
def test_income_account_must_not_be_group(self):
|
||||
doc = make_dunning_type("_Test Dunning Group Account", income_account="Income - _TC")
|
||||
self.assertRaisesRegex(frappe.ValidationError, "is a group account", doc.insert)
|
||||
|
||||
def test_income_account_is_optional(self):
|
||||
doc = make_dunning_type("_Test Dunning No Income Account", income_account=False)
|
||||
doc.insert()
|
||||
self.assertFalse(doc.income_account)
|
||||
|
||||
def test_valid_income_account_passes(self):
|
||||
doc = make_dunning_type("_Test Dunning Valid Income Account", income_account="Sales - _TC")
|
||||
doc.insert()
|
||||
self.assertEqual(doc.income_account, "Sales - _TC")
|
||||
|
||||
def test_cost_center_must_belong_to_company(self):
|
||||
doc = make_dunning_type("_Test Dunning Wrong Company CC", cost_center="Main - _TC1")
|
||||
self.assertRaisesRegex(frappe.ValidationError, "doesn't belong to Company", doc.insert)
|
||||
|
||||
def test_cost_center_must_not_be_disabled(self):
|
||||
disabled_cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "_Test Disabled Cost Center",
|
||||
"parent_cost_center": "_Test Company - _TC",
|
||||
"company": "_Test Company",
|
||||
"disabled": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
doc = make_dunning_type("_Test Dunning Disabled CC", cost_center=disabled_cc.name)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "is disabled", doc.insert)
|
||||
|
||||
def test_cost_center_must_not_be_group(self):
|
||||
doc = make_dunning_type("_Test Dunning Group CC", cost_center="_Test Company - _TC")
|
||||
self.assertRaisesRegex(frappe.ValidationError, "is a group Cost Center", doc.insert)
|
||||
|
||||
def test_cost_center_is_optional(self):
|
||||
doc = make_dunning_type("_Test Dunning No CC", cost_center=False)
|
||||
doc.insert()
|
||||
self.assertFalse(doc.cost_center)
|
||||
|
||||
def test_valid_cost_center_passes(self):
|
||||
doc = make_dunning_type("_Test Dunning Valid CC", cost_center="Main - _TC")
|
||||
doc.insert()
|
||||
self.assertEqual(doc.cost_center, "Main - _TC")
|
||||
|
||||
def test_duplicate_languages_not_allowed(self):
|
||||
doc = make_dunning_type(
|
||||
"_Test Dunning Duplicate Language",
|
||||
dunning_letter_text=[
|
||||
{"language": "en", "body_text": "Body one"},
|
||||
{"language": "en", "body_text": "Body two"},
|
||||
],
|
||||
)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Duplicate languages found", doc.insert)
|
||||
|
||||
def test_unique_languages_allowed(self):
|
||||
doc = make_dunning_type(
|
||||
"_Test Dunning Unique Languages",
|
||||
dunning_letter_text=[
|
||||
{"language": "en", "body_text": "Body one"},
|
||||
{"language": "de", "body_text": "Body two"},
|
||||
],
|
||||
)
|
||||
doc.insert()
|
||||
self.assertEqual(len(doc.dunning_letter_text), 2)
|
||||
|
||||
def test_only_one_default_language_allowed(self):
|
||||
doc = make_dunning_type(
|
||||
"_Test Dunning Multiple Default Language",
|
||||
dunning_letter_text=[
|
||||
{"language": "en", "body_text": "Body one", "is_default_language": 1},
|
||||
{"language": "de", "body_text": "Body two", "is_default_language": 1},
|
||||
],
|
||||
)
|
||||
self.assertRaisesRegex(
|
||||
frappe.ValidationError, "languages are marked as default languages", doc.insert
|
||||
)
|
||||
|
||||
def test_single_default_language_allowed(self):
|
||||
doc = make_dunning_type(
|
||||
"_Test Dunning Single Default Language",
|
||||
dunning_letter_text=[
|
||||
{"language": "en", "body_text": "Body one", "is_default_language": 1},
|
||||
{"language": "de", "body_text": "Body two", "is_default_language": 0},
|
||||
],
|
||||
)
|
||||
doc.insert()
|
||||
self.assertEqual(doc.dunning_letter_text[0].is_default_language, 1)
|
||||
|
||||
def test_invalid_jinja_template_in_body_text_raises(self):
|
||||
doc = make_dunning_type(
|
||||
"_Test Dunning Invalid Body Template",
|
||||
dunning_letter_text=[{"language": "en", "body_text": "{{ unclosed"}],
|
||||
)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Syntax error in template", doc.insert)
|
||||
|
||||
def test_invalid_jinja_template_in_closing_text_raises(self):
|
||||
doc = make_dunning_type(
|
||||
"_Test Dunning Invalid Closing Template",
|
||||
dunning_letter_text=[
|
||||
{"language": "en", "body_text": "Valid body", "closing_text": "{{ unclosed"}
|
||||
],
|
||||
)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "Syntax error in template", doc.insert)
|
||||
|
||||
def test_valid_jinja_template_passes(self):
|
||||
doc = make_dunning_type(
|
||||
"_Test Dunning Valid Template",
|
||||
dunning_letter_text=[
|
||||
{
|
||||
"language": "en",
|
||||
"body_text": "Outstanding amount is {{ outstanding_amount }}",
|
||||
"closing_text": "Regards, {{ company }}",
|
||||
}
|
||||
],
|
||||
)
|
||||
doc.insert()
|
||||
self.assertTrue(doc.name)
|
||||
|
||||
def test_set_default_dunning_type_unsets_previous_default(self):
|
||||
first = make_dunning_type("_Test Dunning Default One", is_default=1)
|
||||
first.insert()
|
||||
self.assertEqual(frappe.db.get_value("Dunning Type", first.name, "is_default"), 1)
|
||||
|
||||
second = make_dunning_type("_Test Dunning Default Two", is_default=1)
|
||||
second.insert()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Dunning Type", first.name, "is_default"), 0)
|
||||
self.assertEqual(frappe.db.get_value("Dunning Type", second.name, "is_default"), 1)
|
||||
|
||||
def test_set_default_dunning_type_scoped_per_company(self):
|
||||
company_1 = make_dunning_type("_Test Dunning Default Co1", is_default=1)
|
||||
company_1.insert()
|
||||
|
||||
company_2 = make_dunning_type(
|
||||
"_Test Dunning Default Co2",
|
||||
company="_Test Company 1",
|
||||
is_default=1,
|
||||
)
|
||||
company_2.insert()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Dunning Type", company_1.name, "is_default"), 1)
|
||||
self.assertEqual(frappe.db.get_value("Dunning Type", company_2.name, "is_default"), 1)
|
||||
class TestDunningType(unittest.TestCase):
|
||||
pass
|
||||
|
||||
@@ -22,27 +22,17 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus == 1) {
|
||||
frappe.call({
|
||||
method: "check_journal_and_reversal",
|
||||
method: "check_journal_entry_condition",
|
||||
doc: frm.doc,
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
if (!r.message.journals_posted) {
|
||||
frm.add_custom_button(
|
||||
__("Journal Entries"),
|
||||
function () {
|
||||
return frm.events.make_jv(frm);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
} else if (!r.message.reversals_posted) {
|
||||
frm.add_custom_button(
|
||||
__("Reversal Journal Entries"),
|
||||
function () {
|
||||
return frm.events.make_reverse_journal(frm);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
frm.add_custom_button(
|
||||
__("Journal Entries"),
|
||||
function () {
|
||||
return frm.events.make_jv(frm);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -110,14 +100,6 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
|
||||
},
|
||||
});
|
||||
},
|
||||
make_reverse_journal: function (frm) {
|
||||
frappe.call({
|
||||
method: "make_reverse_journal",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __("Reversing Journals..."),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Exchange Rate Revaluation Account", {
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Criterion, Order
|
||||
from frappe.query_builder.functions import NullIf, Sum
|
||||
from frappe.utils import flt, get_link_to_form, nowdate
|
||||
from frappe.utils import flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
|
||||
@@ -90,31 +90,25 @@ class ExchangeRateRevaluation(Document):
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
self.ignore_linked_doctypes = "GL Entry"
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_journal_and_reversal(self):
|
||||
def check_journal_entry_condition(self):
|
||||
exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
|
||||
|
||||
journals_posted = False
|
||||
reversals_posted = False
|
||||
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
journals = (
|
||||
qb.from_(je)
|
||||
.join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(je.name)
|
||||
qb.from_(jea)
|
||||
.select(jea.parent)
|
||||
.distinct()
|
||||
.where(
|
||||
(jea.reference_type == "Exchange Rate Revaluation")
|
||||
& (jea.reference_name == self.name)
|
||||
& (jea.docstatus == 1)
|
||||
& (je.reversal_of.isnull()) # omit journals that have reversals
|
||||
)
|
||||
.run(pluck="name")
|
||||
.run()
|
||||
)
|
||||
|
||||
if journals:
|
||||
gle = qb.DocType("GL Entry")
|
||||
total_amt = (
|
||||
@@ -129,31 +123,12 @@ class ExchangeRateRevaluation(Document):
|
||||
.run()
|
||||
)
|
||||
|
||||
if total_amt and total_amt[0][0] == self.total_gain_loss:
|
||||
journals_posted = True
|
||||
if total_amt and total_amt[0][0] != self.total_gain_loss:
|
||||
return True
|
||||
else:
|
||||
journals_posted = False
|
||||
return False
|
||||
|
||||
# reverse journals
|
||||
reverse_journals = (
|
||||
qb.from_(je)
|
||||
.join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(je.name)
|
||||
.where(
|
||||
(jea.reference_type == "Exchange Rate Revaluation")
|
||||
& (jea.reference_name == self.name)
|
||||
& (jea.docstatus == 1)
|
||||
& (je.reversal_of.notnull())
|
||||
)
|
||||
.run(pluck="name")
|
||||
)
|
||||
if reverse_journals:
|
||||
reversals_posted = True
|
||||
else:
|
||||
reversals_posted = False
|
||||
|
||||
return {"journals_posted": journals_posted, "reversals_posted": reversals_posted}
|
||||
return True
|
||||
|
||||
def fetch_and_calculate_accounts_data(self):
|
||||
accounts = self.get_accounts_data()
|
||||
@@ -277,7 +252,7 @@ class ExchangeRateRevaluation(Document):
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
|
||||
currency=company_currency,
|
||||
company_currency,
|
||||
)
|
||||
|
||||
if account_details:
|
||||
@@ -367,7 +342,6 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_jv_entries(self):
|
||||
frappe.has_permission("Journal Entry", "write", throw=True)
|
||||
zero_balance_jv = self.make_jv_for_zero_balance()
|
||||
if zero_balance_jv:
|
||||
frappe.msgprint(
|
||||
@@ -512,9 +486,6 @@ class ExchangeRateRevaluation(Document):
|
||||
journal_entry.posting_date = self.posting_date
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
# Prevent JE from overriding user-entered exchange rates (e.g., rate of 1)
|
||||
journal_entry.flags.ignore_exchange_rate = True
|
||||
|
||||
journal_entry_accounts = []
|
||||
for d in accounts:
|
||||
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
|
||||
@@ -597,49 +568,6 @@ class ExchangeRateRevaluation(Document):
|
||||
journal_entry.save()
|
||||
return journal_entry
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_reverse_journal(self):
|
||||
frappe.has_permission("Journal Entry", "write", throw=True)
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
journals = (
|
||||
qb.from_(je)
|
||||
.join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(je.name)
|
||||
.distinct()
|
||||
.where(
|
||||
(jea.reference_type == "Exchange Rate Revaluation")
|
||||
& (jea.reference_name == self.name)
|
||||
& (jea.docstatus == 1)
|
||||
& (je.reversal_of.isnull()) # omit journals that have reversals
|
||||
)
|
||||
.run(pluck="name")
|
||||
)
|
||||
if journals:
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
if drafts := frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={"docstatus": 0, "reversal_of": ["in", journals]},
|
||||
pluck="name",
|
||||
):
|
||||
part = "journals are" if len(drafts) > 1 else "journal is"
|
||||
doc_links = ", ".join(["{}".format(get_link_to_form("Journal Entry", x)) for x in drafts])
|
||||
frappe.throw(
|
||||
msg=_("Reverse {0} already available in draft status: {1}").format(part, doc_links),
|
||||
)
|
||||
else:
|
||||
for x in journals:
|
||||
reversal = make_reverse_journal_entry(x)
|
||||
reversal.posting_date = nowdate()
|
||||
reversal.save()
|
||||
frappe.msgprint(
|
||||
_("A draft reverse journal for {0} has been created: {1}").format(
|
||||
frappe.bold(x), get_link_to_form("Journal Entry", reversal.name)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
"""
|
||||
@@ -688,10 +616,6 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None
|
||||
):
|
||||
if not account:
|
||||
return
|
||||
frappe.has_permission("Account", doc=account, throw=True)
|
||||
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
|
||||
|
||||
@@ -130,8 +130,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
err = err.save().submit()
|
||||
|
||||
# Create JV for ERR
|
||||
ret = err.check_journal_and_reversal()
|
||||
self.assertFalse(ret.get("journals_posted"))
|
||||
self.assertTrue(err.check_journal_entry_condition())
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
|
||||
je = je.submit()
|
||||
@@ -214,8 +213,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
err = err.save().submit()
|
||||
|
||||
# Create JV for ERR
|
||||
ret = err.check_journal_and_reversal()
|
||||
self.assertFalse(ret.get("journals_posted"))
|
||||
self.assertTrue(err.check_journal_entry_condition())
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
|
||||
je = je.submit()
|
||||
@@ -289,91 +287,3 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
for key, _val in expected_data.items():
|
||||
self.assertEqual(expected_data.get(key), account_details.get(key))
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||
)
|
||||
def test_05_revaluation_journal_reversal(self):
|
||||
"""
|
||||
Test reversing of revaluation journals
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debtors_usd,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=1,
|
||||
)
|
||||
si.currency = "USD"
|
||||
si.conversion_rate = 80
|
||||
si.save().submit()
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = self.company
|
||||
err.posting_date = today()
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
self.assertEqual(len(err.accounts), 1)
|
||||
err.save().submit()
|
||||
|
||||
gain_loss_account = err.get_for_unrealized_gain_loss_account()
|
||||
usd_account = err.accounts[0].account
|
||||
old_balance = err.accounts[0].balance_in_base_currency
|
||||
new_balance = err.accounts[0].new_balance_in_base_currency
|
||||
total_gain_loss = err.total_gain_loss
|
||||
|
||||
# Create JV for ERR
|
||||
ret = err.check_journal_and_reversal()
|
||||
self.assertFalse(ret.get("journals_posted"))
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
|
||||
je = je.submit()
|
||||
|
||||
je.reload()
|
||||
self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
|
||||
self.assertEqual(len(je.accounts), 3)
|
||||
expected = [
|
||||
(usd_account, new_balance, 0.0, 100.0, 0.0),
|
||||
(usd_account, 0.0, old_balance, 0.0, 100.0),
|
||||
(gain_loss_account, 0.0, total_gain_loss, 0.0, total_gain_loss),
|
||||
]
|
||||
actual = []
|
||||
for acc in je.accounts:
|
||||
actual.append(
|
||||
(
|
||||
acc.account,
|
||||
acc.debit,
|
||||
acc.credit,
|
||||
acc.debit_in_account_currency,
|
||||
acc.credit_in_account_currency,
|
||||
)
|
||||
)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
# Assert reversals are not posted
|
||||
ret = err.check_journal_and_reversal()
|
||||
self.assertTrue(ret.get("journals_posted"))
|
||||
self.assertFalse(ret.get("reversals_posted"))
|
||||
|
||||
err.make_reverse_journal()
|
||||
# submit
|
||||
draft = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={"docstatus": 0, "reversal_of": je.name, "voucher_type": "Exchange Rate Revaluation"},
|
||||
pluck="name",
|
||||
)
|
||||
self.assertIsNotNone(draft)
|
||||
frappe.get_doc("Journal Entry", draft[0]).submit()
|
||||
ret = err.check_journal_and_reversal()
|
||||
self.assertTrue(ret.get("journals_posted"))
|
||||
self.assertTrue(ret.get("reversals_posted"))
|
||||
|
||||
reverse_jv = frappe.db.get_all(
|
||||
"Journal Entry", filters={"reversal_of": err_journals.get("revaluation_jv")}, pluck="name"
|
||||
)
|
||||
self.assertIsNotNone(reverse_jv)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _, cint
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_years, cstr, getdate
|
||||
|
||||
@@ -33,11 +33,23 @@ class FiscalYear(Document):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
|
||||
def on_update(self):
|
||||
frappe.cache().delete_key("fiscal_years")
|
||||
if not self.is_new():
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select year_start_date, year_end_date
|
||||
from `tabFiscal Year` where name=%s""",
|
||||
(self.name),
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
frappe.cache().delete_key("fiscal_years")
|
||||
if year_start_end_dates:
|
||||
if (
|
||||
getdate(self.year_start_date) != year_start_end_dates[0][0]
|
||||
or getdate(self.year_end_date) != year_start_end_dates[0][1]
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||
@@ -54,20 +66,28 @@ class FiscalYear(Document):
|
||||
frappe.exceptions.InvalidDates,
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
check_duplicate_fiscal_year(self)
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def on_trash(self):
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def validate_overlap(self):
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
name = self.name or self.year
|
||||
|
||||
existing_fiscal_years = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where(
|
||||
(fy.year_start_date <= self.year_end_date)
|
||||
& (fy.year_end_date >= self.year_start_date)
|
||||
& (fy.name != name)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
existing_fiscal_years = frappe.db.sql(
|
||||
"""select name from `tabFiscal Year`
|
||||
where (
|
||||
(%(year_start_date)s between year_start_date and year_end_date)
|
||||
or (%(year_end_date)s between year_start_date and year_end_date)
|
||||
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
|
||||
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
|
||||
) and name!=%(name)s""",
|
||||
{
|
||||
"year_start_date": self.year_start_date,
|
||||
"year_end_date": self.year_end_date,
|
||||
"name": self.name or "No Name",
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if existing_fiscal_years:
|
||||
@@ -79,7 +99,7 @@ class FiscalYear(Document):
|
||||
)
|
||||
|
||||
overlap = False
|
||||
if not self.get("companies") and not company_for_existing:
|
||||
if not self.get("companies") or not company_for_existing:
|
||||
overlap = True
|
||||
|
||||
for d in self.get("companies"):
|
||||
@@ -90,30 +110,37 @@ class FiscalYear(Document):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Year start date or end date is overlapping with {0}. To avoid please set company"
|
||||
).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
|
||||
).format(existing.name),
|
||||
frappe.NameError,
|
||||
)
|
||||
|
||||
|
||||
def auto_create_fiscal_year():
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
# Skipped auto-creating Short Year, as it has very rare use case.
|
||||
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
|
||||
follow_up_date = add_days(getdate(), days=3)
|
||||
fiscal_year = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
|
||||
.run()
|
||||
@frappe.whitelist()
|
||||
def check_duplicate_fiscal_year(doc):
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
|
||||
(doc.name),
|
||||
)
|
||||
for fiscal_year, ysd, yed in year_start_end_dates:
|
||||
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
|
||||
not frappe.flags.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
|
||||
).format(fiscal_year)
|
||||
)
|
||||
|
||||
for d in fiscal_year:
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_create_fiscal_year():
|
||||
for d in frappe.db.sql(
|
||||
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
|
||||
):
|
||||
try:
|
||||
current_fy = frappe.get_doc("Fiscal Year", d[0])
|
||||
|
||||
new_fy = frappe.new_doc("Fiscal Year")
|
||||
new_fy.disabled = cint(current_fy.disabled)
|
||||
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False)
|
||||
|
||||
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
|
||||
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
|
||||
@@ -121,10 +148,6 @@ def auto_create_fiscal_year():
|
||||
start_year = cstr(new_fy.year_start_date.year)
|
||||
end_year = cstr(new_fy.year_end_date.year)
|
||||
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
|
||||
|
||||
for row in current_fy.companies:
|
||||
new_fy.append("companies", {"company": row.company})
|
||||
|
||||
new_fy.auto_created = 1
|
||||
|
||||
new_fy.insert(ignore_permissions=True)
|
||||
|
||||
@@ -26,27 +26,6 @@ class TestFiscalYear(unittest.TestCase):
|
||||
|
||||
self.assertRaises(frappe.exceptions.InvalidDates, fy.insert)
|
||||
|
||||
def test_company_fiscal_year_overlap(self):
|
||||
for name in ["_Test Global FY 2001", "_Test Company FY 2001"]:
|
||||
if frappe.db.exists("Fiscal Year", name):
|
||||
frappe.delete_doc("Fiscal Year", name)
|
||||
|
||||
global_fy = frappe.new_doc("Fiscal Year")
|
||||
global_fy.year = "_Test Global FY 2001"
|
||||
global_fy.year_start_date = "2001-04-01"
|
||||
global_fy.year_end_date = "2002-03-31"
|
||||
global_fy.insert()
|
||||
|
||||
company_fy = frappe.new_doc("Fiscal Year")
|
||||
company_fy.year = "_Test Company FY 2001"
|
||||
company_fy.year_start_date = "2001-01-01"
|
||||
company_fy.year_end_date = "2001-12-31"
|
||||
company_fy.append("companies", {"company": "_Test Company"})
|
||||
|
||||
company_fy.insert()
|
||||
self.assertTrue(frappe.db.exists("Fiscal Year", global_fy.name))
|
||||
self.assertTrue(frappe.db.exists("Fiscal Year", company_fy.name))
|
||||
|
||||
|
||||
def test_record_generator():
|
||||
test_records = [
|
||||
|
||||
@@ -15,14 +15,13 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-20 23:02:26.193606",
|
||||
"modified": "2021-09-28 18:01:53.495929",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year Company",
|
||||
@@ -31,4 +30,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link
|
||||
company: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -131,8 +131,8 @@ class GLEntry(Document):
|
||||
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
|
||||
if not frappe.flags.party_not_required: # skipping validation if party is not required
|
||||
# skipping validation for payroll entry creation in case party is not required
|
||||
if not frappe.flags.party_not_required_for_receivable_payable:
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
@@ -187,6 +187,7 @@ class GLEntry(Document):
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -200,6 +201,7 @@ class GLEntry(Document):
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
@@ -251,7 +253,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center or self.is_cancelled:
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
|
||||
@@ -418,7 +420,7 @@ def update_against_account(voucher_type, voucher_no):
|
||||
if not entries:
|
||||
return
|
||||
company_currency = erpnext.get_company_currency(entries[0].company)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in entries:
|
||||
|
||||
@@ -47,12 +47,3 @@ frappe.ui.form.on("Item Tax Template", {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Item Tax Template Detail", {
|
||||
not_applicable: function (frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.not_applicable) {
|
||||
frappe.model.set_value(cdt, cdn, "tax_rate", 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,15 +27,8 @@ class ItemTaxTemplate(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.set_zero_rate_for_not_applicable_tax()
|
||||
self.validate_tax_accounts()
|
||||
|
||||
def set_zero_rate_for_not_applicable_tax(self):
|
||||
"""Ensure tax_rate is 0 for any row marked as not applicable."""
|
||||
for row in self.get("taxes"):
|
||||
if row.not_applicable:
|
||||
row.tax_rate = 0
|
||||
|
||||
def autoname(self):
|
||||
if self.company and self.title:
|
||||
abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
||||
|
||||
@@ -8,6 +8,6 @@ def get_data():
|
||||
{"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
|
||||
{"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
|
||||
{"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
|
||||
{"label": _("Stock"), "items": ["Item Group", "Item"]},
|
||||
{"label": _("Stock"), "items": ["Item Groups", "Item"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,51 +1,108 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2018-11-22 23:47:02.804568",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"tax_type",
|
||||
"tax_rate",
|
||||
"not_applicable"
|
||||
],
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-11-22 23:47:02.804568",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "tax_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "tax_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tax",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate",
|
||||
"read_only_depends_on": "eval:doc.not_applicable"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Check if this tax is not applicable to items (distinct from 0% rate)",
|
||||
"fieldname": "not_applicable",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Not Applicable"
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tax Rate",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-30 23:59:22.020639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-12-21 23:51:39.445198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template Detail",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
@@ -14,7 +14,6 @@ class ItemTaxTemplateDetail(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
not_applicable: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -20,30 +20,9 @@ frappe.ui.form.on("Journal Entry", {
|
||||
"Unreconcile Payment Entries",
|
||||
"Bank Transaction",
|
||||
];
|
||||
frm.trigger("set_queries");
|
||||
},
|
||||
|
||||
set_queries(frm) {
|
||||
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
let filters = {
|
||||
company: doc.company,
|
||||
};
|
||||
if (row.party_type == "Customer") {
|
||||
filters.customer = row.party;
|
||||
}
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) {
|
||||
erpnext.journal_entry.lock_reversal_entry(frm);
|
||||
}
|
||||
|
||||
erpnext.toggle_naming_series();
|
||||
|
||||
if (frm.doc.docstatus > 0) {
|
||||
@@ -65,7 +44,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && !frm.doc.reversal_of) {
|
||||
if (frm.doc.docstatus == 1) {
|
||||
frm.add_custom_button(
|
||||
__("Reverse Journal Entry"),
|
||||
function () {
|
||||
@@ -382,17 +361,15 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
|
||||
accounts_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (!row.exchange_rate) row.exchange_rate = 1;
|
||||
if (!row.account) {
|
||||
$.each(doc.accounts, function (i, d) {
|
||||
if (d.account && d.party && d.party_type) {
|
||||
row.account = d.account;
|
||||
row.party = d.party;
|
||||
row.party_type = d.party_type;
|
||||
row.exchange_rate = d.exchange_rate;
|
||||
}
|
||||
});
|
||||
}
|
||||
row.exchange_rate = 1;
|
||||
$.each(doc.accounts, function (i, d) {
|
||||
if (d.account && d.party && d.party_type) {
|
||||
row.account = d.account;
|
||||
row.party = d.party;
|
||||
row.party_type = d.party_type;
|
||||
row.exchange_rate = d.exchange_rate;
|
||||
}
|
||||
});
|
||||
|
||||
// set difference
|
||||
if (doc.difference) {
|
||||
@@ -421,8 +398,8 @@ cur_frm.cscript.update_totals = function (doc) {
|
||||
tc += flt(accounts[i].credit, precision("credit", accounts[i]));
|
||||
}
|
||||
doc = locals[doc.doctype][doc.name];
|
||||
doc.total_debit = flt(td, precision("total_debit"));
|
||||
doc.total_credit = flt(tc, precision("total_credit"));
|
||||
doc.total_debit = td;
|
||||
doc.total_credit = tc;
|
||||
doc.difference = flt(td - tc, precision("difference"));
|
||||
refresh_many(["total_debit", "total_credit", "difference"]);
|
||||
};
|
||||
@@ -513,14 +490,6 @@ $.extend(erpnext.journal_entry, {
|
||||
});
|
||||
},
|
||||
|
||||
lock_reversal_entry: function (frm) {
|
||||
frm.fields
|
||||
.filter((field) => field.has_input)
|
||||
.filter((field) => !["posting_date", "user_remark"].includes(field.df.fieldname))
|
||||
.forEach((field) => frm.set_df_property(field.df.fieldname, "read_only", 1));
|
||||
frm.set_df_property("accounts", "read_only", 1);
|
||||
},
|
||||
|
||||
set_debit_credit_in_company_currency: function (frm, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
"addtional_info",
|
||||
"mode_of_payment",
|
||||
"payment_order",
|
||||
"party_not_required",
|
||||
"column_break3",
|
||||
"is_opening",
|
||||
"stock_entry",
|
||||
@@ -544,14 +543,6 @@
|
||||
"label": "Is System Generated",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "party_not_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -566,7 +557,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-09-29 13:05:46.982277",
|
||||
"modified": "2024-07-18 15:32:29.413598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -6,9 +6,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, getdate, nowdate
|
||||
from frappe.utils import comma_and, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
||||
@@ -22,7 +20,6 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import validate_opening_entry_against_pcv
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
@@ -36,7 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -76,7 +72,6 @@ class JournalEntry(AccountsController):
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
paid_loan: DF.Data | None
|
||||
party_not_required: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
posting_date: DF.Date
|
||||
@@ -125,9 +120,6 @@ class JournalEntry(AccountsController):
|
||||
if not self.is_opening:
|
||||
self.is_opening = "No"
|
||||
|
||||
if self.is_opening == "Yes":
|
||||
validate_opening_entry_against_pcv(self.company)
|
||||
|
||||
self.clearance_date = None
|
||||
|
||||
self.validate_party()
|
||||
@@ -155,8 +147,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
if not self.title or (self.is_new() and self.amended_from):
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
@@ -178,17 +169,16 @@ class JournalEntry(AccountsController):
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100 and not self.meta.queue_in_background:
|
||||
queue_submission(self, "_submit")
|
||||
if len(self.accounts) > 100:
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("submit", timeout=4600)
|
||||
else:
|
||||
return self._submit()
|
||||
|
||||
def before_cancel(self):
|
||||
self.has_asset_adjustment_entry()
|
||||
|
||||
def cancel(self):
|
||||
if len(self.accounts) > 100:
|
||||
queue_submission(self, "_cancel")
|
||||
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||
self.queue_action("cancel", timeout=4600)
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
@@ -199,8 +189,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.make_gl_entries()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
@@ -220,8 +210,6 @@ class JournalEntry(AccountsController):
|
||||
def on_cancel(self):
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super().on_cancel()
|
||||
|
||||
from_doc_events = getattr(self, "ignore_linked_doctypes", ())
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
@@ -234,10 +222,6 @@ class JournalEntry(AccountsController):
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
|
||||
if from_doc_events and from_doc_events != self.ignore_linked_doctypes:
|
||||
self.ignore_linked_doctypes = self.ignore_linked_doctypes + from_doc_events
|
||||
|
||||
self.make_gl_entries(1)
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
@@ -275,9 +259,6 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
if not erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
return
|
||||
|
||||
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(
|
||||
@@ -291,7 +272,93 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
|
||||
def apply_tax_withholding(self):
|
||||
JournalEntryTaxWithholding(self).apply()
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
|
||||
return
|
||||
|
||||
parties = [d.party for d in self.get("accounts") if d.party]
|
||||
parties = list(set(parties))
|
||||
|
||||
if len(parties) > 1:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
account_type_map = get_account_type_map(self.company)
|
||||
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
|
||||
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
|
||||
debit_or_credit = (
|
||||
"debit_in_account_currency"
|
||||
if self.voucher_type == "Credit Note"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
rev_debit_or_credit = (
|
||||
"credit_in_account_currency"
|
||||
if debit_or_credit == "debit_in_account_currency"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||
|
||||
net_total = sum(
|
||||
d.get(debit_or_credit)
|
||||
for d in self.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
)
|
||||
|
||||
party_amount = sum(
|
||||
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
|
||||
)
|
||||
|
||||
inv = frappe._dict(
|
||||
{
|
||||
party_type: parties[0],
|
||||
"doctype": doctype,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.get("accounts"):
|
||||
if d.get("account") == tax_withholding_details.get("account_head"):
|
||||
d.update(
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(d.get("account"))
|
||||
|
||||
if d.get("account") == party_account:
|
||||
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
"against_account": parties[0],
|
||||
},
|
||||
)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.get("accounts")
|
||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
||||
@@ -465,36 +532,21 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
|
||||
|
||||
def has_asset_adjustment_entry(self):
|
||||
if self.flags.get("via_asset_value_adjustment"):
|
||||
return
|
||||
|
||||
asset_value_adjustment = frappe.db.get_value(
|
||||
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
|
||||
)
|
||||
if asset_value_adjustment:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
|
||||
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
|
||||
)
|
||||
|
||||
def unlink_asset_adjustment_entry(self):
|
||||
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
|
||||
(
|
||||
frappe.qb.update(AssetValueAdjustment)
|
||||
.set(AssetValueAdjustment.journal_entry, None)
|
||||
.where(AssetValueAdjustment.journal_entry == self.name)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
""" update `tabAsset Value Adjustment`
|
||||
set journal_entry = null where journal_entry = %s""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
|
||||
# skipping validation for payroll entry creation
|
||||
skip_validation = frappe.flags.party_not_required_for_receivable_payable
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if (
|
||||
not (d.party_type and d.party) and not self.party_not_required
|
||||
): # skipping validation if party_not_required is passed via payroll entry
|
||||
if not (d.party_type and d.party) and not skip_validation:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
@@ -800,23 +852,6 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
if reference_type == "Purchase Invoice":
|
||||
on_hold, release_date = frappe.db.get_value(
|
||||
reference_type, reference_name, ["on_hold", "release_date"]
|
||||
)
|
||||
|
||||
if not on_hold or (release_date and getdate(release_date) <= getdate(nowdate())):
|
||||
continue
|
||||
|
||||
msg = (
|
||||
_("{0} {1} is blocked and on hold until {2}.").format(
|
||||
reference_type, reference_name, release_date
|
||||
)
|
||||
if release_date
|
||||
else _("{0} {1} is blocked.").format(reference_type, reference_name)
|
||||
)
|
||||
frappe.throw(msg)
|
||||
|
||||
def set_against_account(self):
|
||||
accounts_debited, accounts_credited = [], []
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
|
||||
@@ -861,14 +896,12 @@ class JournalEntry(AccountsController):
|
||||
if d.debit and d.credit:
|
||||
frappe.throw(_("You cannot credit and debit same account at the same time"))
|
||||
|
||||
self.total_debit = flt(
|
||||
self.total_debit + flt(d.debit, d.precision("debit")), self.precision("total_debit")
|
||||
)
|
||||
self.total_credit = flt(
|
||||
self.total_credit + flt(d.credit, d.precision("credit")), self.precision("total_credit")
|
||||
)
|
||||
self.total_debit = flt(self.total_debit) + flt(d.debit, d.precision("debit"))
|
||||
self.total_credit = flt(self.total_credit) + flt(d.credit, d.precision("credit"))
|
||||
|
||||
self.difference = flt(self.total_debit - self.total_credit, self.precision("difference"))
|
||||
self.difference = flt(self.total_debit, self.precision("total_debit")) - flt(
|
||||
self.total_credit, self.precision("total_credit")
|
||||
)
|
||||
|
||||
def validate_multi_currency(self):
|
||||
alternate_currency = []
|
||||
@@ -1106,11 +1139,6 @@ class JournalEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
# set flag to skip party validation
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
if account_type in ["Receivable", "Payable"] and self.party_not_required:
|
||||
frappe.flags.party_not_required = True
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
row,
|
||||
@@ -1138,7 +1166,6 @@ class JournalEntry(AccountsController):
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
frappe.flags.party_not_required = False
|
||||
if cancel:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
|
||||
@@ -1209,11 +1236,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def get_values(self):
|
||||
cond = (
|
||||
f" and outstanding_amount <= {flt(self.write_off_amount)}"
|
||||
if flt(self.write_off_amount) > 0
|
||||
else ""
|
||||
)
|
||||
cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else ""
|
||||
|
||||
if self.write_off_based_on == "Accounts Receivable":
|
||||
return frappe.db.sql(
|
||||
@@ -1251,230 +1274,6 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Accounts table cannot be blank."))
|
||||
|
||||
|
||||
class JournalEntryTaxWithholding:
|
||||
def __init__(self, journal_entry):
|
||||
self.doc: JournalEntry = journal_entry
|
||||
self.party = None
|
||||
self.party_type = None
|
||||
self.party_account = None
|
||||
self.party_row = None
|
||||
self.existing_tds_rows = []
|
||||
self.precision = None
|
||||
self.has_multiple_parties = False
|
||||
|
||||
# Direction fields based on party type
|
||||
self.party_field = None # "credit" for Supplier, "debit" for Customer
|
||||
self.reverse_field = None # opposite of party_field
|
||||
|
||||
def apply(self):
|
||||
if not self._set_party_info():
|
||||
return
|
||||
|
||||
self._setup_direction_fields()
|
||||
self._reset_existing_tds()
|
||||
|
||||
if not self._should_apply_tds():
|
||||
self._cleanup_duplicate_tds_rows(None)
|
||||
return
|
||||
|
||||
if self.has_multiple_parties:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
net_total = self._calculate_net_total()
|
||||
if net_total <= 0:
|
||||
return
|
||||
|
||||
tds_details = self._get_tds_details(net_total)
|
||||
if not tds_details or not tds_details.get("tax_amount"):
|
||||
return
|
||||
|
||||
self._create_or_update_tds_row(tds_details)
|
||||
self._update_party_amount(tds_details.get("tax_amount"), is_reversal=False)
|
||||
|
||||
self._recalculate_totals()
|
||||
|
||||
def _should_apply_tds(self):
|
||||
return self.doc.apply_tds and self.doc.voucher_type in ("Debit Note", "Credit Note")
|
||||
|
||||
def _set_party_info(self):
|
||||
for row in self.doc.get("accounts"):
|
||||
if row.party_type in ("Customer", "Supplier") and row.party:
|
||||
if self.party and row.party != self.party:
|
||||
self.has_multiple_parties = True
|
||||
|
||||
if not self.party:
|
||||
self.party = row.party
|
||||
self.party_type = row.party_type
|
||||
self.party_account = row.account
|
||||
self.party_row = row
|
||||
|
||||
if row.get("is_tax_withholding_account"):
|
||||
self.existing_tds_rows.append(row)
|
||||
|
||||
return bool(self.party)
|
||||
|
||||
def _setup_direction_fields(self):
|
||||
"""
|
||||
For Supplier (TDS): party has credit, TDS reduces credit
|
||||
For Customer (TCS): party has debit, TCS increases debit
|
||||
"""
|
||||
if self.party_type == "Supplier":
|
||||
self.party_field = "credit"
|
||||
self.reverse_field = "debit"
|
||||
else: # Customer
|
||||
self.party_field = "debit"
|
||||
self.reverse_field = "credit"
|
||||
|
||||
self.precision = self.doc.precision(self.party_field, self.party_row)
|
||||
|
||||
def _reset_existing_tds(self):
|
||||
for row in self.existing_tds_rows:
|
||||
# TDS amount is always in credit (liability to government)
|
||||
tds_amount = flt(row.get("credit") - row.get("debit"), self.precision)
|
||||
if not tds_amount:
|
||||
continue
|
||||
|
||||
self._update_party_amount(tds_amount, is_reversal=True)
|
||||
|
||||
# zero_out_tds_row
|
||||
row.update(
|
||||
{
|
||||
"credit": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
def _update_party_amount(self, amount, is_reversal=False):
|
||||
amount = flt(amount, self.precision)
|
||||
amount_in_party_currency = flt(amount / self.party_row.get("exchange_rate", 1), self.precision)
|
||||
|
||||
# Determine which field the party amount is in
|
||||
active_field = self.party_field if self.party_row.get(self.party_field) else self.reverse_field
|
||||
|
||||
# If amount is in reverse field, flip the signs
|
||||
if active_field == self.reverse_field:
|
||||
amount = -amount
|
||||
amount_in_party_currency = -amount_in_party_currency
|
||||
|
||||
# Direction multiplier based on party type:
|
||||
# Customer (TCS): +1 (add to debit)
|
||||
# Supplier (TDS): -1 (subtract from credit)
|
||||
direction = 1 if self.party_type == "Customer" else -1
|
||||
|
||||
# Reversal inverts the direction
|
||||
if is_reversal:
|
||||
direction = -direction
|
||||
|
||||
adjustment = amount * direction
|
||||
adjustment_in_party_currency = amount_in_party_currency * direction
|
||||
|
||||
active_field_account_currency = f"{active_field}_in_account_currency"
|
||||
|
||||
self.party_row.update(
|
||||
{
|
||||
active_field: flt(self.party_row.get(active_field) + adjustment, self.precision),
|
||||
active_field_account_currency: flt(
|
||||
self.party_row.get(active_field_account_currency) + adjustment_in_party_currency,
|
||||
self.precision,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def _calculate_net_total(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
account_type_map = get_account_type_map(self.doc.company)
|
||||
|
||||
return flt(
|
||||
sum(
|
||||
d.get(self.reverse_field) - d.get(self.party_field)
|
||||
for d in self.doc.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
and d.account != self.party_account
|
||||
and not d.get("is_tax_withholding_account")
|
||||
),
|
||||
self.precision,
|
||||
)
|
||||
|
||||
def _get_tds_details(self, net_total):
|
||||
return get_party_tax_withholding_details(
|
||||
frappe._dict(
|
||||
{
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"doctype": self.doc.doctype,
|
||||
"company": self.doc.company,
|
||||
"posting_date": self.doc.posting_date,
|
||||
"tax_withholding_net_total": net_total,
|
||||
"base_tax_withholding_net_total": net_total,
|
||||
"grand_total": net_total,
|
||||
}
|
||||
),
|
||||
self.doc.tax_withholding_category,
|
||||
)
|
||||
|
||||
def _create_or_update_tds_row(self, tds_details):
|
||||
tax_account = tds_details.get("account_head")
|
||||
account_currency = get_account_currency(tax_account)
|
||||
company_currency = frappe.get_cached_value("Company", self.doc.company, "default_currency")
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, self.doc.posting_date)
|
||||
|
||||
tax_amount = flt(tds_details.get("tax_amount"), self.precision)
|
||||
tax_amount_in_account_currency = flt(tax_amount / exchange_rate, self.precision)
|
||||
|
||||
# Find existing TDS row for this account
|
||||
tax_row = None
|
||||
for row in self.doc.get("accounts"):
|
||||
if row.account == tax_account and row.get("is_tax_withholding_account"):
|
||||
tax_row = row
|
||||
break
|
||||
|
||||
if not tax_row:
|
||||
tax_row = self.doc.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_account,
|
||||
"account_currency": account_currency,
|
||||
"exchange_rate": exchange_rate,
|
||||
"cost_center": tds_details.get("cost_center"),
|
||||
"credit": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"is_tax_withholding_account": 1,
|
||||
},
|
||||
)
|
||||
|
||||
# TDS/TCS is always credited (liability to government)
|
||||
tax_row.update(
|
||||
{
|
||||
"credit": tax_amount,
|
||||
"credit_in_account_currency": tax_amount_in_account_currency,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
self._cleanup_duplicate_tds_rows(tax_row)
|
||||
|
||||
def _cleanup_duplicate_tds_rows(self, current_tax_row):
|
||||
rows_to_remove = [
|
||||
row
|
||||
for row in self.doc.get("accounts")
|
||||
if row.get("is_tax_withholding_account") and row != current_tax_row
|
||||
]
|
||||
|
||||
for row in rows_to_remove:
|
||||
self.doc.remove(row)
|
||||
|
||||
def _recalculate_totals(self):
|
||||
self.doc.set_amounts_in_company_currency()
|
||||
self.doc.set_total_debit_credit()
|
||||
self.doc.set_against_account()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
@@ -1528,7 +1327,6 @@ def get_payment_entry_against_order(
|
||||
dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
|
||||
):
|
||||
ref_doc = frappe.get_doc(dt, dn)
|
||||
ref_doc.check_permission()
|
||||
|
||||
if flt(ref_doc.per_billed, 2) > 0:
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
@@ -1574,8 +1372,6 @@ def get_payment_entry_against_invoice(
|
||||
dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
|
||||
):
|
||||
ref_doc = frappe.get_doc(dt, dn)
|
||||
ref_doc.check_permission()
|
||||
|
||||
if dt == "Sales Invoice":
|
||||
party_type = "Customer"
|
||||
party_account = get_party_account_based_on_invoice_discounting(dn) or ref_doc.debit_to
|
||||
@@ -1611,8 +1407,6 @@ def get_payment_entry_against_invoice(
|
||||
|
||||
|
||||
def get_payment_entry(ref_doc, args):
|
||||
frappe.has_permission("Journal Entry", ptype="create", throw=True)
|
||||
|
||||
cost_center = ref_doc.get("cost_center") or frappe.get_cached_value(
|
||||
"Company", ref_doc.company, "cost_center"
|
||||
)
|
||||
@@ -1848,6 +1642,8 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
@@ -1869,8 +1665,8 @@ def get_exchange_rate(
|
||||
|
||||
# The date used to retreive the exchange rate here is the date passed
|
||||
# in as an argument to this function.
|
||||
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
|
||||
@@ -1900,21 +1696,7 @@ def make_inter_company_journal_entry(name, voucher_type, company):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_reverse_journal_entry(source_name: str, target_doc: str | dict | Document | None = None) -> Document:
|
||||
# `get_mapped_doc` checks this as well, but the guard below discloses which entry
|
||||
# reverses which, so read access has to be settled before it runs
|
||||
if not frappe.has_permission("Journal Entry", doc=source_name):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
reversal_of = frappe.db.get_value("Journal Entry", source_name, "reversal_of")
|
||||
if reversal_of:
|
||||
frappe.throw(
|
||||
_("{0} is already a Reverse Journal Entry of {1}. Cancel it instead of reversing it.").format(
|
||||
get_link_to_form("Journal Entry", source_name),
|
||||
get_link_to_form("Journal Entry", reversal_of),
|
||||
)
|
||||
)
|
||||
|
||||
def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
def post_process(source, target):
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
frappe.listview_settings["Journal Entry"] = {
|
||||
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark", "reversal_of"],
|
||||
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.docstatus == 0) {
|
||||
return [__("Draft", "red", "docstatus,=,0")];
|
||||
} else if (doc.docstatus == 2) {
|
||||
return [__("Cancelled", "grey", "docstatus,=,2")];
|
||||
} else if (doc.docstatus === 1) {
|
||||
if (doc.reversal_of && doc.voucher_type == "Exchange Rate Revaluation") {
|
||||
return [__("Reversal Of Exchange Rate Revaluation"), "blue"];
|
||||
}
|
||||
return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`];
|
||||
} else {
|
||||
return [__(doc.voucher_type), "blue", "voucher_type,=," + doc.voucher_type];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -6,12 +6,11 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
|
||||
|
||||
|
||||
class TestJournalEntry(unittest.TestCase):
|
||||
@@ -249,27 +248,6 @@ class TestJournalEntry(unittest.TestCase):
|
||||
|
||||
self.check_gl_entries()
|
||||
|
||||
def test_disallow_reversal_of_a_reversal_journal_entry(self):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
jv = make_journal_entry("_Test Bank - _TC", "Sales - _TC", 100, submit=True)
|
||||
|
||||
rjv = make_reverse_journal_entry(jv.name)
|
||||
rjv.posting_date = nowdate()
|
||||
rjv.submit()
|
||||
|
||||
self.assertRaisesRegex(
|
||||
frappe.ValidationError,
|
||||
"is already a Reverse Journal Entry",
|
||||
make_reverse_journal_entry,
|
||||
rjv.name,
|
||||
)
|
||||
|
||||
# the guard must not disclose the reversal to a user who cannot read the entry
|
||||
frappe.set_user("Guest")
|
||||
self.addCleanup(frappe.set_user, "Administrator")
|
||||
self.assertRaises(frappe.PermissionError, make_reverse_journal_entry, rjv.name)
|
||||
|
||||
def test_disallow_change_in_account_currency_for_a_party(self):
|
||||
# create jv in USD
|
||||
jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)
|
||||
@@ -410,59 +388,6 @@ class TestJournalEntry(unittest.TestCase):
|
||||
|
||||
self.check_gl_entries()
|
||||
|
||||
def make_jv_with_fractional_totals(self):
|
||||
"""0.10 + 0.20 sums to 0.30000000000000004, the residue this guards against."""
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = nowdate()
|
||||
jv.company = "_Test Company"
|
||||
jv.voucher_type = "Journal Entry"
|
||||
jv.remark = "test"
|
||||
for amount in (0.10, 0.20):
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": "_Test Cash - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"debit_in_account_currency": amount,
|
||||
},
|
||||
)
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": "_Test Bank - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"credit_in_account_currency": 0.30,
|
||||
},
|
||||
)
|
||||
jv.insert()
|
||||
return jv
|
||||
|
||||
def test_totals_are_rounded_to_precision(self):
|
||||
jv = self.make_jv_with_fractional_totals()
|
||||
jv.submit()
|
||||
|
||||
stored = frappe.db.get_value(
|
||||
"Journal Entry", jv.name, ["total_debit", "total_credit", "difference"], as_dict=True
|
||||
)
|
||||
self.assertEqual(jv.total_debit, flt(jv.total_debit, jv.precision("total_debit")))
|
||||
self.assertEqual(jv.total_credit, flt(jv.total_credit, jv.precision("total_credit")))
|
||||
self.assertEqual(jv.total_debit, stored.total_debit)
|
||||
self.assertEqual(jv.total_credit, stored.total_credit)
|
||||
self.assertEqual(jv.difference, stored.difference)
|
||||
|
||||
def test_update_after_submit_with_fractional_totals(self):
|
||||
"""An unrounded total is stored rounded, so updating a submitted entry used to throw."""
|
||||
jv = self.make_jv_with_fractional_totals()
|
||||
jv.submit()
|
||||
|
||||
jv.pay_to_recd_from = "_Test Supplier"
|
||||
jv.save()
|
||||
|
||||
self.assertEqual(jv.docstatus, 1)
|
||||
self.assertEqual(
|
||||
jv.pay_to_recd_from, frappe.db.get_value("Journal Entry", jv.name, "pay_to_recd_from")
|
||||
)
|
||||
|
||||
def test_jv_account_and_party_balance_with_cost_centre(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
@@ -667,78 +592,6 @@ class TestJournalEntry(unittest.TestCase):
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
def test_credit_limit_for_customer(self):
|
||||
customer = make_customer("_Test New Customer")
|
||||
set_credit_limit("_Test New Customer", "_Test Company", 50)
|
||||
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = customer
|
||||
jv.save()
|
||||
self.assertRaises(frappe.ValidationError, jv.submit)
|
||||
|
||||
def make_jv_against_purchase_invoice(self, invoice, amount=100):
|
||||
jv = make_journal_entry("Creditors - _TC", "_Test Cash - _TC", amount, save=False)
|
||||
jv.accounts[0].party_type = "Supplier"
|
||||
jv.accounts[0].party = invoice.supplier
|
||||
jv.accounts[0].reference_type = "Purchase Invoice"
|
||||
jv.accounts[0].reference_name = invoice.name
|
||||
return jv
|
||||
|
||||
def test_jv_against_purchase_invoice_respects_hold_state(self):
|
||||
"""Payment can be booked against a Purchase Invoice only while it is not on hold."""
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
|
||||
release_date = add_days(nowdate(), 10)
|
||||
|
||||
def never_held():
|
||||
return make_purchase_invoice()
|
||||
|
||||
def held_until_a_future_date():
|
||||
invoice = make_purchase_invoice()
|
||||
invoice.block_invoice(hold_comment="Waiting for the goods", release_date=release_date)
|
||||
return invoice
|
||||
|
||||
def held_without_a_release_date():
|
||||
invoice = make_purchase_invoice()
|
||||
invoice.block_invoice(hold_comment="Under dispute")
|
||||
return invoice
|
||||
|
||||
def held_until_a_date_that_has_passed():
|
||||
invoice = held_until_a_future_date()
|
||||
frappe.db.set_value("Purchase Invoice", invoice.name, "release_date", add_days(nowdate(), -1))
|
||||
return invoice
|
||||
|
||||
def unblocked_again():
|
||||
invoice = held_until_a_future_date()
|
||||
invoice.unblock_invoice()
|
||||
return invoice
|
||||
|
||||
for build_invoice in (held_until_a_future_date, held_without_a_release_date):
|
||||
with self.subTest(build_invoice.__name__):
|
||||
jv = self.make_jv_against_purchase_invoice(build_invoice())
|
||||
self.assertRaisesRegex(frappe.ValidationError, "is blocked", jv.insert)
|
||||
|
||||
for build_invoice in (never_held, held_until_a_date_that_has_passed, unblocked_again):
|
||||
with self.subTest(build_invoice.__name__):
|
||||
invoice = build_invoice()
|
||||
jv = self.make_jv_against_purchase_invoice(invoice)
|
||||
jv.insert()
|
||||
self.assertEqual(jv.reference_types[invoice.name], "Purchase Invoice")
|
||||
|
||||
def test_jv_against_blocked_sales_invoice_reference_is_not_checked(self):
|
||||
"""A Sales Invoice has no hold state, so the check must skip it rather than fail."""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
invoice = create_sales_invoice(rate=500)
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 100, save=False)
|
||||
jv.accounts[1].party_type = "Customer"
|
||||
jv.accounts[1].party = "_Test Customer"
|
||||
jv.accounts[1].reference_type = "Sales Invoice"
|
||||
jv.accounts[1].reference_name = invoice.name
|
||||
jv.insert()
|
||||
|
||||
self.assertEqual(jv.reference_types[invoice.name], "Sales Invoice")
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"is_tax_withholding_account",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -107,6 +106,7 @@
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"no_copy": 1,
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -185,7 +185,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -198,7 +198,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
|
||||
"fieldname": "reference_due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Reference Due Date",
|
||||
@@ -272,8 +272,7 @@
|
||||
"label": "Advance Voucher Type",
|
||||
"no_copy": 1,
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_no",
|
||||
@@ -281,21 +280,13 @@
|
||||
"label": "Advance Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-19 17:01:22.642454",
|
||||
"modified": "2025-07-25 04:45:28.117715",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -28,7 +28,6 @@ class JournalEntryAccount(Document):
|
||||
debit_in_account_currency: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
is_advance: DF.Literal["No", "Yes"]
|
||||
is_tax_withholding_account: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
@@ -55,7 +54,6 @@ class JournalEntryAccount(Document):
|
||||
"Fees",
|
||||
"Full and Final Statement",
|
||||
"Payment Entry",
|
||||
"Bank Transaction",
|
||||
]
|
||||
user_remark: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -56,9 +56,7 @@ class LedgerMerge(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_merge(docname):
|
||||
lm_doc = frappe.get_doc("Ledger Merge", docname)
|
||||
lm_doc.check_permission("write")
|
||||
return lm_doc.start_merge()
|
||||
return frappe.get_doc("Ledger Merge", docname).start_merge()
|
||||
|
||||
|
||||
def start_merge(docname):
|
||||
|
||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Mode of Payment", {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
filters: [
|
||||
["Account", "account_type", "in", ["Bank", "Cash", "Receivable"]],
|
||||
["Account", "account_type", "in", "Bank, Cash, Receivable"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "company", "=", d.company],
|
||||
],
|
||||
|
||||
@@ -24,22 +24,15 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
setTimeout(
|
||||
() => {
|
||||
frm.doc.import_in_progress = false;
|
||||
frm.clear_table("invoices");
|
||||
frm.refresh_fields();
|
||||
frm.page.clear_indicator();
|
||||
frm.dashboard.hide_progress();
|
||||
|
||||
if (!data.errors) {
|
||||
frm.clear_table("invoices");
|
||||
frm.refresh_fields();
|
||||
const message =
|
||||
frm.doc.invoice_type == "Sales"
|
||||
? __("Opening Sales Invoice(s) have been created.")
|
||||
: __("Opening Purchase Invoice(s) have been created.");
|
||||
frappe.show_alert({
|
||||
message: message,
|
||||
indicator: "green",
|
||||
});
|
||||
if (frm.doc.invoice_type == "Sales") {
|
||||
frappe.msgprint(__("Opening Sales Invoices have been created."));
|
||||
} else {
|
||||
frm.refresh_fields();
|
||||
frappe.msgprint(__("Opening Purchase Invoices have been created."));
|
||||
}
|
||||
},
|
||||
1500,
|
||||
@@ -82,31 +75,29 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
},
|
||||
|
||||
setup_company_filters: function (frm) {
|
||||
frm.events.apply_company_query_filter(frm, "cost_center", "invoices", { is_group: 0 });
|
||||
frm.events.apply_company_query_filter(frm, "project", "invoices");
|
||||
frm.events.apply_company_query_filter(frm, "project");
|
||||
frm.events.apply_company_query_filter(frm, "cost_center", undefined, { is_group: 0 });
|
||||
frm.events.apply_company_query_filter(frm, "temporary_opening_account", "invoices", {
|
||||
account_type: "Temporary",
|
||||
is_group: 0,
|
||||
});
|
||||
},
|
||||
|
||||
apply_company_query_filter: function (frm, field_name, child_doctype = null, filters = {}) {
|
||||
const query = function (doc) {
|
||||
frm.set_query("cost_center", "invoices", function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
...filters,
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
if (child_doctype) {
|
||||
frm.set_query(field_name, child_doctype, query);
|
||||
} else {
|
||||
frm.set_query(field_name, query);
|
||||
}
|
||||
frm.set_query("cost_center", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("temporary_opening_account", "invoices", function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
@@ -130,7 +121,10 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
},
|
||||
|
||||
invoice_type: function (frm) {
|
||||
frm.clear_table("invoices");
|
||||
$.each(frm.doc.invoices, (idx, row) => {
|
||||
row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier";
|
||||
row.party = "";
|
||||
});
|
||||
frm.refresh_fields();
|
||||
},
|
||||
|
||||
@@ -171,19 +165,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Opening Invoice Creation Tool Item", {
|
||||
invoices_add: (frm, cdt, cdn) => {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
const field_copy = [];
|
||||
|
||||
["project", "cost_center"].forEach((fieldname) => {
|
||||
if (frm.doc[fieldname]) {
|
||||
frappe.model.set_value(cdt, cdn, fieldname, frm.doc[fieldname]);
|
||||
} else {
|
||||
field_copy.push(fieldname);
|
||||
}
|
||||
});
|
||||
|
||||
frm.script_manager.copy_from_first_row("invoices", row, field_copy);
|
||||
invoices_add: (frm) => {
|
||||
frm.trigger("update_invoice_table");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_copy": 1,
|
||||
"beta": 1,
|
||||
"creation": "2017-08-29 02:22:54.947711",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -64,10 +64,10 @@
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -82,8 +82,7 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-30 20:43:36.282738",
|
||||
"modified": "2022-01-04 15:25:06.053187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool",
|
||||
@@ -100,9 +99,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import escape_html, flt, nowdate
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -31,7 +31,6 @@ class OpeningInvoiceCreationTool(Document):
|
||||
create_missing_party: DF.Check
|
||||
invoice_type: DF.Literal["Sales", "Purchase"]
|
||||
invoices: DF.Table[OpeningInvoiceCreationToolItem]
|
||||
project: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def onload(self):
|
||||
@@ -85,11 +84,6 @@ class OpeningInvoiceCreationTool(Document):
|
||||
)
|
||||
prepare_invoice_summary(doctype, invoices)
|
||||
|
||||
invoices_summary_companies = list(invoices_summary.keys())
|
||||
|
||||
for company in invoices_summary_companies:
|
||||
invoices_summary[escape_html(company)] = invoices_summary.pop(company)
|
||||
|
||||
return invoices_summary, max_count
|
||||
|
||||
def validate_company(self):
|
||||
@@ -122,17 +116,6 @@ class OpeningInvoiceCreationTool(Document):
|
||||
if not row.get(scrub(d)):
|
||||
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
|
||||
|
||||
self.validate_temporary_opening_account(row)
|
||||
|
||||
def validate_temporary_opening_account(self, row):
|
||||
account_type = frappe.get_cached_value("Account", row.temporary_opening_account, "account_type")
|
||||
if account_type != "Temporary":
|
||||
frappe.throw(
|
||||
_("Row #{0}: {1} account is not of type {2}").format(
|
||||
row.idx, row.temporary_opening_account, "Temporary"
|
||||
)
|
||||
)
|
||||
|
||||
def get_invoices(self):
|
||||
invoices = []
|
||||
for row in self.invoices:
|
||||
@@ -202,7 +185,6 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"description": row.item_name or "Opening Invoice Item",
|
||||
income_expense_account_field: row.temporary_opening_account,
|
||||
"cost_center": cost_center,
|
||||
"project": row.get("project") or self.get("project"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -266,35 +248,22 @@ class OpeningInvoiceCreationTool(Document):
|
||||
def start_import(invoices):
|
||||
errors = 0
|
||||
names = []
|
||||
total = len(invoices)
|
||||
for idx, d in enumerate(invoices):
|
||||
# Scope each invoice to a savepoint so a failure only undoes that invoice.
|
||||
# A plain rollback() would discard the whole transaction — including invoices
|
||||
# imported earlier in this batch and the error logs of earlier failures (the
|
||||
# latter only survive on mariadb because the Error Log table is MyISAM; on
|
||||
# postgres they would be lost). Rolling back to a savepoint keeps both.
|
||||
savepoint = f"opening_invoice_{frappe.generate_hash(length=8)}"
|
||||
frappe.db.savepoint(savepoint)
|
||||
is_last = idx == total - 1
|
||||
try:
|
||||
invoice_number = None
|
||||
if d.invoice_number:
|
||||
invoice_number = d.invoice_number
|
||||
publish(idx, len(invoices), d.doctype)
|
||||
doc = frappe.get_doc(d)
|
||||
doc.flags.ignore_mandatory = True
|
||||
# the outstanding amount is entered inclusive of tax, so taxes must not
|
||||
# be added on top of it
|
||||
doc.flags.dont_auto_add_taxes = True
|
||||
doc.insert(set_name=invoice_number)
|
||||
doc.submit()
|
||||
frappe.db.commit()
|
||||
names.append(doc.name)
|
||||
publish(idx, total, d.doctype, errors=errors if is_last else None)
|
||||
except Exception:
|
||||
errors += 1
|
||||
frappe.db.rollback()
|
||||
doc.log_error("Opening invoice creation failed")
|
||||
publish(idx, total, d.doctype, errors=errors if is_last else None)
|
||||
if errors:
|
||||
frappe.msgprint(
|
||||
_("You had {} errors while creating opening invoices. Check {} for more details").format(
|
||||
@@ -306,7 +275,7 @@ def start_import(invoices):
|
||||
return names
|
||||
|
||||
|
||||
def publish(index, total, doctype, errors=None):
|
||||
def publish(index, total, doctype):
|
||||
frappe.publish_realtime(
|
||||
"opening_invoice_creation_progress",
|
||||
dict(
|
||||
@@ -314,7 +283,6 @@ def publish(index, total, doctype, errors=None):
|
||||
message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
|
||||
count=index + 1,
|
||||
total=total,
|
||||
errors=errors,
|
||||
),
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
create_dimension,
|
||||
disable_dimension,
|
||||
@@ -13,8 +11,6 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_rule.test_tax_rule import make_tax_rule
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
|
||||
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
|
||||
|
||||
@@ -31,26 +27,21 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
self,
|
||||
invoice_type="Sales",
|
||||
company=None,
|
||||
invoices=None,
|
||||
project=None,
|
||||
cost_center=None,
|
||||
party_1=None,
|
||||
party_2=None,
|
||||
invoice_number=None,
|
||||
department=None,
|
||||
return_doc=False,
|
||||
):
|
||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||
args = get_opening_invoice_creation_dict(
|
||||
invoice_type=invoice_type,
|
||||
company=company,
|
||||
invoices=invoices,
|
||||
project=project,
|
||||
cost_center=cost_center,
|
||||
party_1=party_1,
|
||||
party_2=party_2,
|
||||
invoice_number=invoice_number,
|
||||
department=department,
|
||||
)
|
||||
doc.update(args)
|
||||
|
||||
if return_doc:
|
||||
return doc
|
||||
|
||||
return doc.make_invoices()
|
||||
|
||||
def test_opening_sales_invoice_creation(self):
|
||||
@@ -59,8 +50,8 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 200, "Overdue"],
|
||||
1: ["_Test Customer 1", 200, "Overdue"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
|
||||
@@ -77,34 +68,48 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
for field_idx, field in enumerate(expected_value["keys"]):
|
||||
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
|
||||
|
||||
def test_opening_invoice_requires_temporary_account_type(self):
|
||||
doc = self.make_invoices(company="_Test Opening Invoice Company", return_doc=True)
|
||||
doc.invoices[0].temporary_opening_account = "Sales - _TOIC"
|
||||
self.assertRaises(frappe.ValidationError, doc.make_invoices)
|
||||
|
||||
def test_opening_purchase_invoice_creation(self):
|
||||
invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
|
||||
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["supplier", "outstanding_amount", "status"],
|
||||
0: ["_Test Supplier", 200, "Overdue"],
|
||||
1: ["_Test Supplier 1", 200, "Overdue"],
|
||||
0: ["_Test Supplier", 300, "Overdue"],
|
||||
1: ["_Test Supplier 1", 250, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value, "Purchase")
|
||||
|
||||
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
|
||||
old_default_receivable_account = frappe.db.get_value(
|
||||
"Company", "_Test Opening Invoice Company", "default_receivable_account"
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Opening Invoice Company", "default_receivable_account", "")
|
||||
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", "")
|
||||
|
||||
self.make_invoices(
|
||||
company="_Test Opening Invoice Company",
|
||||
invoices=[{"party": party_1}, {"party": party_2}],
|
||||
)
|
||||
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
|
||||
cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "_Test Opening Invoice Company",
|
||||
"is_group": 1,
|
||||
"company": "_Test Opening Invoice Company",
|
||||
}
|
||||
)
|
||||
cc.insert(ignore_mandatory=True)
|
||||
cc2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "Main",
|
||||
"is_group": 0,
|
||||
"company": "_Test Opening Invoice Company",
|
||||
"parent_cost_center": cc.name,
|
||||
}
|
||||
)
|
||||
cc2.insert()
|
||||
|
||||
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
|
||||
|
||||
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
|
||||
|
||||
# Check if missing debit account error raised
|
||||
error_log = frappe.db.exists(
|
||||
@@ -114,162 +119,74 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
self.assertTrue(error_log)
|
||||
|
||||
# teardown
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
"_Test Opening Invoice Company",
|
||||
"default_receivable_account",
|
||||
old_default_receivable_account,
|
||||
)
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
inv_num = f"TEST-NEW-INV-{frappe.generate_hash(length=8)}"
|
||||
invoices = self.make_invoices(
|
||||
company="_Test Opening Invoice Company",
|
||||
invoices=[
|
||||
{"party": party_1, "invoice_number": inv_num},
|
||||
{"party": party_2},
|
||||
],
|
||||
self.make_invoices(
|
||||
company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11"
|
||||
)
|
||||
|
||||
self.assertEqual(invoices[0], inv_num)
|
||||
sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name")
|
||||
sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name")
|
||||
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
|
||||
|
||||
# teardown
|
||||
for inv in [sales_inv1, sales_inv2]:
|
||||
doc = frappe.get_doc("Sales Invoice", inv)
|
||||
doc.cancel()
|
||||
|
||||
def test_opening_invoice_with_accounting_dimension(self):
|
||||
invoices = self.make_invoices(
|
||||
invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC"
|
||||
)
|
||||
|
||||
for invoice in invoices:
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status", "department"],
|
||||
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
|
||||
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_taxes_and_charges_template": 1, "add_taxes_from_item_tax_template": 0},
|
||||
)
|
||||
def test_opening_invoice_creation_without_taxes(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"company": company,
|
||||
"title": "_Test Opening Invoice Tax",
|
||||
"taxes": [
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": create_account(
|
||||
account_name="_Test Opening Tax Account",
|
||||
parent_account="Duties and Taxes - _TOIC",
|
||||
account_type="Tax",
|
||||
company=company,
|
||||
),
|
||||
"description": "Test taxes",
|
||||
"rate": 9,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# makes the template the default for the party, as it would be on a live site
|
||||
make_tax_rule(tax_type="Sales", company=company, sales_tax_template=template.name, save=1)
|
||||
|
||||
tool = self.make_invoices(company=company, return_doc=True)
|
||||
invoices = tool.make_invoices()
|
||||
self.assertEqual(len(invoices), 2)
|
||||
|
||||
# outstanding amount is entered inclusive of tax, so taxes must not be added on top of it
|
||||
for invoice in invoices:
|
||||
si = frappe.get_doc("Sales Invoice", invoice)
|
||||
self.assertFalse(si.taxes)
|
||||
self.assertEqual(si.grand_total, 200)
|
||||
self.assertEqual(si.outstanding_amount, 200)
|
||||
|
||||
# the same invoice created outside the tool keeps the default taxes,
|
||||
# since adding them there is the user's decision
|
||||
si = frappe.get_doc(tool.get_invoices()[0])
|
||||
si.flags.ignore_mandatory = True
|
||||
si.insert()
|
||||
self.assertTrue(si.taxes)
|
||||
self.assertEqual(si.grand_total, 218)
|
||||
|
||||
def test_opening_entry_project_linking(self):
|
||||
doc = self.make_invoices(
|
||||
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True
|
||||
)
|
||||
project_1 = make_project(
|
||||
{"project_name": "Test Opening Invoice projecty 01", "company": "_Test Opening Invoice Company"}
|
||||
)
|
||||
project_2 = make_project(
|
||||
{"project_name": "Test Opening Invoice projecty 02", "company": "_Test Opening Invoice Company"}
|
||||
)
|
||||
doc.invoices[0].project = project_1.name
|
||||
doc.invoices[1].project = project_2.name
|
||||
invoices = doc.make_invoices()
|
||||
sales_invoice_1 = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
sales_invoice_2 = frappe.get_doc("Sales Invoice", invoices[1])
|
||||
|
||||
self.assertEqual(sales_invoice_1.items[0].project, project_1.name)
|
||||
self.assertEqual(sales_invoice_2.items[0].project, project_2.name)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
def tearDown(self):
|
||||
disable_dimension()
|
||||
super().tearDownClass()
|
||||
|
||||
|
||||
def get_opening_invoice_creation_dict(**args):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
company = args.get("company", "_Test Company")
|
||||
default_invoices = []
|
||||
default_invoice_rows = [
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 200,
|
||||
"party": f"_Test {party}",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": add_days(today(), -10),
|
||||
"posting_date": add_days(today(), -15),
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
},
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 200,
|
||||
"party": f"_Test {party} 1",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": add_days(today(), -10),
|
||||
"posting_date": add_days(today(), -15),
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
},
|
||||
]
|
||||
|
||||
for row in args.get("invoices") or default_invoice_rows:
|
||||
default_invoices.append(
|
||||
{
|
||||
"qty": row.get("qty") or 1.0,
|
||||
"outstanding_amount": row.get("outstanding_amount") or 200,
|
||||
"party": row.get("party") or f"_Test {party}",
|
||||
"item_name": row.get("item_name") or "Opening Item",
|
||||
"due_date": row.get("due_date") or add_days(today(), -10),
|
||||
"posting_date": row.get("posting_date") or add_days(today(), -15),
|
||||
"temporary_opening_account": row.get("temporary_opening_account")
|
||||
or get_temporary_opening_account(company),
|
||||
"invoice_number": row.get("invoice_number"),
|
||||
"project": row.get("project"),
|
||||
"cost_center": row.get("cost_center"),
|
||||
}
|
||||
)
|
||||
|
||||
invoice_dict = frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"invoice_type": args.get("invoice_type", "Sales"),
|
||||
"project": args.get("project"),
|
||||
"cost_center": args.get("cost_center"),
|
||||
"invoices": default_invoices,
|
||||
"invoices": [
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 300,
|
||||
"party": args.get("party_1") or f"_Test {party}",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": args.get("invoice_number"),
|
||||
},
|
||||
{
|
||||
"qty": 2.0,
|
||||
"outstanding_amount": 250,
|
||||
"party": args.get("party_2") or f"_Test {party} 1",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": None,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
invoice_dict.update(args)
|
||||
invoice_dict.invoices = default_invoices
|
||||
return invoice_dict
|
||||
|
||||
|
||||
@@ -292,7 +209,7 @@ def make_customer(customer=None):
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": customer_name,
|
||||
"customer_group": "Individual",
|
||||
"customer_group": "All Customer Groups",
|
||||
"customer_type": "Company",
|
||||
"territory": "All Territories",
|
||||
}
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
"qty",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project"
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -80,7 +79,6 @@
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Outstanding Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -113,17 +111,11 @@
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Invoice Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-03 15:17:11.938499",
|
||||
"modified": "2022-03-21 19:31:45.382656",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
@@ -134,4 +126,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ class OpeningInvoiceCreationToolItem(Document):
|
||||
party: DF.DynamicLink
|
||||
party_type: DF.Link | None
|
||||
posting_date: DF.Date | None
|
||||
project: DF.Link | None
|
||||
qty: DF.Data | None
|
||||
temporary_opening_account: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -45,27 +45,23 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
frm.set_query("paid_from", function (doc) {
|
||||
frm.set_query("paid_from", function () {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
let filters = {
|
||||
account_type: ["in", account_types],
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
};
|
||||
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
if (doc.payment_type == "Internal Transfer" && doc.paid_to) {
|
||||
filters.name = ["!=", doc.paid_to];
|
||||
}
|
||||
|
||||
return {
|
||||
filters,
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -109,25 +105,21 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query("paid_to", function (doc) {
|
||||
frm.set_query("paid_to", function () {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
let filters = {
|
||||
account_type: ["in", account_types],
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
};
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
if (doc.payment_type == "Internal Transfer" && doc.paid_from) {
|
||||
filters.name = ["!=", doc.paid_from];
|
||||
}
|
||||
return {
|
||||
filters,
|
||||
filters: {
|
||||
account_type: ["in", account_types],
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -408,16 +400,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
frm.refresh_fields();
|
||||
|
||||
const party_currency =
|
||||
frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
|
||||
|
||||
var reference_grid = frm.fields_dict["references"].grid;
|
||||
["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
|
||||
reference_grid.update_docfield_property(fieldname, "options", party_currency);
|
||||
});
|
||||
|
||||
reference_grid.refresh();
|
||||
},
|
||||
|
||||
show_general_ledger: function (frm) {
|
||||
@@ -453,7 +435,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"paid_to",
|
||||
"references",
|
||||
"total_allocated_amount",
|
||||
"party_name",
|
||||
],
|
||||
function (i, field) {
|
||||
frm.set_value(field, null);
|
||||
@@ -524,16 +505,12 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("contact_email", "");
|
||||
frm.set_value("contact_person", "");
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
|
||||
if (!frm.doc.posting_date) {
|
||||
frappe.msgprint(__("Please select Posting Date before selecting Party"));
|
||||
frm.set_value("party", "");
|
||||
return;
|
||||
}
|
||||
|
||||
erpnext.utils.get_employee_contact_details(frm);
|
||||
|
||||
frm.set_party_account_based_on_party = true;
|
||||
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
@@ -615,8 +592,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
paid_from: function (frm) {
|
||||
if (frm.set_party_account_based_on_party) return;
|
||||
|
||||
frm.events.set_company_bank_account(frm);
|
||||
|
||||
frm.events.set_account_currency_and_balance(
|
||||
frm,
|
||||
frm.doc.paid_from,
|
||||
@@ -626,7 +601,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (frm.doc.payment_type == "Pay") {
|
||||
frm.events.paid_amount(frm);
|
||||
}
|
||||
frm.events.paid_from_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -634,8 +608,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
paid_to: function (frm) {
|
||||
if (frm.set_party_account_based_on_party) return;
|
||||
|
||||
frm.events.set_company_bank_account(frm);
|
||||
|
||||
frm.events.set_account_currency_and_balance(
|
||||
frm,
|
||||
frm.doc.paid_to,
|
||||
@@ -652,7 +624,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.events.received_amount(frm);
|
||||
}
|
||||
}
|
||||
frm.events.paid_to_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -733,12 +704,31 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (!frm.doc.paid_from_account_currency || !frm.doc.company) return;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
frm.events.set_current_exchange_rate(
|
||||
frm,
|
||||
"source_exchange_rate",
|
||||
frm.doc.paid_from_account_currency,
|
||||
company_currency
|
||||
);
|
||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
from_currency: frm.doc.paid_from_account_currency,
|
||||
to_currency: company_currency,
|
||||
transaction_date: frm.doc.posting_date,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
frm.set_value("source_exchange_rate", r.message);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
frm.events.set_current_exchange_rate(
|
||||
frm,
|
||||
"source_exchange_rate",
|
||||
frm.doc.paid_from_account_currency,
|
||||
company_currency
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
paid_to_account_currency: function (frm) {
|
||||
@@ -770,28 +760,49 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
posting_date: function (frm) {
|
||||
frm.events.paid_from_account_currency(frm);
|
||||
frm.events.paid_to_account_currency(frm);
|
||||
},
|
||||
|
||||
source_exchange_rate: function (frm) {
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.paid_amount && frm.doc.source_exchange_rate) {
|
||||
if (frm.doc.paid_amount) {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
|
||||
// target exchange rate should always be same as source if both account currencies is same
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else {
|
||||
const target_rate =
|
||||
flt(frm.doc.target_exchange_rate) ||
|
||||
(company_currency == frm.doc.paid_to_account_currency ? 1 : 0);
|
||||
if (target_rate) {
|
||||
frm.set_value("received_amount", flt(frm.doc.base_received_amount) / target_rate);
|
||||
}
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
// no need trigger separately
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
}
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
|
||||
target_exchange_rate: function (frm) {
|
||||
frm.set_paid_amount_based_on_received_amount = true;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.received_amount) {
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
|
||||
if (
|
||||
!frm.doc.source_exchange_rate &&
|
||||
frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
|
||||
) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
@@ -800,37 +811,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
frm.set_paid_amount_based_on_received_amount = false;
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
frm.set_df_property("source_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
|
||||
target_exchange_rate: function (frm) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.received_amount && frm.doc.target_exchange_rate) {
|
||||
frm.set_value(
|
||||
"base_received_amount",
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
} else {
|
||||
const source_rate =
|
||||
flt(frm.doc.source_exchange_rate) ||
|
||||
(company_currency == frm.doc.paid_from_account_currency ? 1 : 0);
|
||||
if (source_rate) {
|
||||
frm.set_value("paid_amount", flt(frm.doc.base_paid_amount) / source_rate);
|
||||
}
|
||||
}
|
||||
|
||||
// set_unallocated_amount is called by below method,
|
||||
// no need trigger separately
|
||||
frm.events.set_total_allocated_amount(frm);
|
||||
}
|
||||
|
||||
// Make read only if Accounts Settings doesn't allow stale rates
|
||||
frm.set_df_property("target_exchange_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
|
||||
},
|
||||
@@ -839,14 +819,11 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (!frm.doc.received_amount) {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
} else if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value(
|
||||
"received_amount",
|
||||
flt(frm.doc.base_paid_amount) / flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
}
|
||||
}
|
||||
frm.trigger("reset_received_amount");
|
||||
@@ -863,14 +840,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
} else if (frm.doc.source_exchange_rate) {
|
||||
frm.set_value(
|
||||
"paid_amount",
|
||||
flt(frm.doc.base_received_amount) / flt(frm.doc.source_exchange_rate)
|
||||
);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1134,7 +1112,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||
await frm.call("allocate_amount_to_references", {
|
||||
paid_amount: flt(paid_amount),
|
||||
paid_amount: paid_amount,
|
||||
paid_amount_change: paid_amount_change,
|
||||
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||
});
|
||||
@@ -1318,14 +1296,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const account =
|
||||
company_defaults?.[account_fieldname] ||
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = company_defaults?.cost_center;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
@@ -1369,8 +1348,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
if (frm.set_company_bank_account_based_on_coa) return;
|
||||
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
|
||||
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
|
||||
frappe.call({
|
||||
@@ -1409,34 +1386,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
},
|
||||
|
||||
set_company_bank_account: function (frm) {
|
||||
if (!["Pay", "Receive"].includes(frm.doc.payment_type)) return;
|
||||
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
|
||||
|
||||
if (!frm.doc.company || !frm.doc[field]) return;
|
||||
|
||||
frm.set_company_bank_account_based_on_coa = true;
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: "Bank Account",
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
account: frm.doc[field],
|
||||
disabled: 0,
|
||||
},
|
||||
fieldname: ["name"],
|
||||
},
|
||||
callback: async function (r) {
|
||||
if (r.message) await frm.set_value("bank_account", r.message.name);
|
||||
|
||||
frm.set_company_bank_account_based_on_coa = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
sales_taxes_and_charges_template: function (frm) {
|
||||
frm.trigger("fetch_taxes_from_template");
|
||||
},
|
||||
@@ -1470,15 +1419,16 @@ frappe.ui.form.on("Payment Entry", {
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
// set taxes table
|
||||
let taxes = r.message;
|
||||
taxes.forEach((tax) => {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
if (r.message) {
|
||||
for (let tax of r.message) {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.add_child("taxes", tax);
|
||||
}
|
||||
});
|
||||
frm.set_value("taxes", taxes);
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1535,14 +1485,18 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"
|
||||
);
|
||||
d.row_id = "";
|
||||
} else if (d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") {
|
||||
} else if (
|
||||
(d.charge_type == "On Previous Row Amount" || d.charge_type == "On Previous Row Total") &&
|
||||
d.row_id
|
||||
) {
|
||||
if (d.idx == 1) {
|
||||
msg = __(
|
||||
"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
|
||||
);
|
||||
d.charge_type = "";
|
||||
} else if (!d.row_id) {
|
||||
d.row_id = d.idx - 1;
|
||||
msg = __("Please specify a valid Row ID for row {0} in table {1}", [d.idx, __(d.doctype)]);
|
||||
d.row_id = "";
|
||||
} else if (d.row_id && d.row_id >= d.idx) {
|
||||
msg = __(
|
||||
"Cannot refer row number greater than or equal to current row number for this Charge type"
|
||||
@@ -1790,35 +1744,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
before_cancel: function (frm) {
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_linked_bank_transactions",
|
||||
args: { payment_entry: frm.doc.name },
|
||||
callback: function (r) {
|
||||
const linked = r.message || [];
|
||||
if (!linked.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const bt_links = linked
|
||||
.map((name) => frappe.utils.get_form_link("Bank Transaction", name, true))
|
||||
.join(", ");
|
||||
frappe.confirm(
|
||||
__(
|
||||
"This Payment Entry is reconciled with {0}. Cancelling will automatically unreconcile it. Do you want to proceed?",
|
||||
[bt_links]
|
||||
),
|
||||
() => resolve(),
|
||||
() => reject(),
|
||||
__("Yes"),
|
||||
__("No")
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Reference", {
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.received_amount;",
|
||||
"depends_on": "doc.received_amount",
|
||||
"fieldname": "base_received_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Received Amount (Company Currency)",
|
||||
@@ -800,7 +800,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2026-05-15 13:31:01.166010",
|
||||
"modified": "2025-05-15 18:01:04.013025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -90,7 +90,6 @@ class PaymentEntry(AccountsController):
|
||||
self.set_liability_account()
|
||||
self.set_missing_ref_details(force=True)
|
||||
self.validate_payment_type()
|
||||
self.validate_internal_transfer_accounts()
|
||||
self.validate_party_details()
|
||||
self.set_exchange_rate()
|
||||
self.validate_mandatory()
|
||||
@@ -450,7 +449,7 @@ class PaymentEntry(AccountsController):
|
||||
self.contact_person = get_default_contact(self.party_type, self.party)
|
||||
|
||||
complete_contact_details(self)
|
||||
if not self.party_balance and frappe.get_single_value("Accounts Settings", "show_party_balance"):
|
||||
if not self.party_balance:
|
||||
self.party_balance = get_balance_on(
|
||||
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
|
||||
)
|
||||
@@ -532,10 +531,6 @@ class PaymentEntry(AccountsController):
|
||||
if self.payment_type not in ("Receive", "Pay", "Internal Transfer"):
|
||||
frappe.throw(_("Payment Type must be one of Receive, Pay and Internal Transfer"))
|
||||
|
||||
def validate_internal_transfer_accounts(self):
|
||||
if self.payment_type == "Internal Transfer" and self.paid_from and self.paid_from == self.paid_to:
|
||||
frappe.throw(_("Paid From and Paid To accounts must be different for an Internal Transfer."))
|
||||
|
||||
def validate_party_details(self):
|
||||
if self.party and not frappe.db.exists(self.party_type, self.party):
|
||||
frappe.throw(_("{0} {1} does not exist").format(_(self.party_type), self.party))
|
||||
@@ -1097,32 +1092,20 @@ class PaymentEntry(AccountsController):
|
||||
self.base_paid_amount + deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
flt(
|
||||
(
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
),
|
||||
self.precision("unallocated_amount"),
|
||||
)
|
||||
/ self.source_exchange_rate
|
||||
)
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.source_exchange_rate
|
||||
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
|
||||
self.base_received_amount - deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
flt(
|
||||
(
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
),
|
||||
self.precision("unallocated_amount"),
|
||||
)
|
||||
/ self.target_exchange_rate
|
||||
)
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.target_exchange_rate
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
exchange_gain_loss = flt(
|
||||
@@ -1202,9 +1185,9 @@ class PaymentEntry(AccountsController):
|
||||
continue
|
||||
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += flt(tax.base_tax_amount)
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= flt(tax.base_tax_amount)
|
||||
included_taxes -= tax.base_tax_amount
|
||||
|
||||
return included_taxes
|
||||
|
||||
@@ -1306,14 +1289,8 @@ class PaymentEntry(AccountsController):
|
||||
self.add_deductions_gl_entries(gl_entries)
|
||||
self.add_tax_gl_entries(gl_entries)
|
||||
add_regional_gl_entries(gl_entries, self)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
|
||||
for gle in gl_entries:
|
||||
gle.setdefault("transaction_currency", self.transaction_currency)
|
||||
gle.setdefault("transaction_exchange_rate", self.transaction_exchange_rate)
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gl_entries = self.build_gl_map()
|
||||
gl_entries = process_gl_map(gl_entries)
|
||||
@@ -1823,7 +1800,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
|
||||
self.base_total_taxes_and_charges += current_tax_amount
|
||||
self.base_total_taxes_and_charges += tax.base_tax_amount
|
||||
|
||||
if self.get("taxes"):
|
||||
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
|
||||
@@ -2290,9 +2267,6 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
if args.get("party_type") == "Member":
|
||||
return
|
||||
|
||||
if args.get("party_type") and args.get("party"):
|
||||
frappe.has_permission(args["party_type"], "read", args["party"], throw=True)
|
||||
|
||||
if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"):
|
||||
args["get_outstanding_invoices"] = True
|
||||
|
||||
@@ -2320,20 +2294,22 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
# Get positive outstanding sales /purchase invoices
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
|
||||
condition = " and voucher_type={} and voucher_no={}".format(
|
||||
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
|
||||
)
|
||||
common_filter.append(ple.voucher_type == args["voucher_type"])
|
||||
common_filter.append(ple.voucher_no == args["voucher_no"])
|
||||
|
||||
# Add cost center condition
|
||||
if args.get("cost_center"):
|
||||
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
|
||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if args.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
|
||||
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
|
||||
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||
|
||||
date_fields_dict = {
|
||||
@@ -2342,19 +2318,18 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
}
|
||||
|
||||
for fieldname, date_fields in date_fields_dict.items():
|
||||
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
|
||||
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
|
||||
|
||||
if args.get(date_fields[0]) and args.get(date_fields[1]):
|
||||
condition += f" and {fieldname} between {from_date} and {to_date}"
|
||||
condition += " and {} between '{}' and '{}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
elif args.get(date_fields[0]):
|
||||
# if only from date is supplied
|
||||
condition += f" and {fieldname} >= {from_date}"
|
||||
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
|
||||
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
|
||||
elif args.get(date_fields[1]):
|
||||
# if only to date is supplied
|
||||
condition += f" and {fieldname} <= {to_date}"
|
||||
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
|
||||
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||
|
||||
if args.get("company"):
|
||||
@@ -2569,12 +2544,17 @@ def get_orders_to_be_billed(
|
||||
if not voucher_type:
|
||||
return []
|
||||
|
||||
# dynamic dimension filters
|
||||
# Add cost center condition
|
||||
doc = frappe.get_doc({"doctype": voucher_type})
|
||||
condition = ""
|
||||
active_dimensions = get_dimensions(True)[0]
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
|
||||
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
@@ -2702,26 +2682,17 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
if not frappe.db.exists(party_type, party):
|
||||
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
|
||||
|
||||
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
|
||||
frappe.has_permission(party_type, ptype, party, throw=True)
|
||||
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
account_currency = get_account_currency(party_account)
|
||||
account_balance = (
|
||||
get_balance_on(party_account, date, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
else 0
|
||||
)
|
||||
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, company=company, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_party_balance")
|
||||
else 0
|
||||
party_balance = get_balance_on(
|
||||
party_type=party_type, party=party, company=company, cost_center=cost_center
|
||||
)
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
bank_account = get_default_company_bank_account(company, party_type, party, ignore_permissions=False)
|
||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||
|
||||
return {
|
||||
"party_account": party_account,
|
||||
@@ -2746,11 +2717,7 @@ def get_account_details(account, date, cost_center=None):
|
||||
if not account_list:
|
||||
frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account))
|
||||
|
||||
account_balance = (
|
||||
get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
|
||||
if frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
else 0
|
||||
)
|
||||
account_balance = get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
|
||||
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -2805,8 +2772,7 @@ def get_reference_details(
|
||||
):
|
||||
total_amount = outstanding_amount = exchange_rate = account = None
|
||||
|
||||
frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
|
||||
ref_doc = frappe.get_lazy_doc(reference_doctype, reference_name)
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
||||
|
||||
# Only applies for Reverse Payment Entries
|
||||
@@ -2822,7 +2788,9 @@ def get_reference_details(
|
||||
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
outstanding_amount, total_amount = get_outstanding_on_journal_entry(reference_name, party_type, party)
|
||||
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
|
||||
reference_name, party_type, party
|
||||
)
|
||||
|
||||
elif reference_doctype == "Payment Entry":
|
||||
if reverse_payment_details := frappe.db.get_all(
|
||||
@@ -2899,13 +2867,10 @@ def get_payment_entry(
|
||||
party_type=None,
|
||||
payment_type=None,
|
||||
reference_date=None,
|
||||
ignore_permissions=False,
|
||||
created_from_payment_request=False,
|
||||
):
|
||||
frappe.has_permission("Payment Entry", ptype="create", throw=True)
|
||||
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
doc.check_permission()
|
||||
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (100.0 + over_billing_allowance):
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt)))
|
||||
@@ -3053,7 +3018,7 @@ def get_payment_entry(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
|
||||
pe.set_exchange_rate()
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
@@ -3330,11 +3295,13 @@ def set_paid_amount_and_received_amount(
|
||||
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
|
||||
if bank and company_currency != bank.account_currency:
|
||||
# doc currency can be different from bank currency
|
||||
conversion_rate = get_exchange_rate(bank.account_currency, party_account_currency)
|
||||
posting_date = doc.get("posting_date") or doc.get("transaction_date")
|
||||
conversion_rate = get_exchange_rate(
|
||||
bank.account_currency, party_account_currency, posting_date
|
||||
)
|
||||
received_amount = paid_amount / conversion_rate
|
||||
else:
|
||||
conversion_rate = get_exchange_rate(doc.get("currency", company_currency), company_currency)
|
||||
received_amount = paid_amount * conversion_rate
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
|
||||
# if payment type is pay, then paid amount and received amount are swapped
|
||||
if payment_type == "Pay":
|
||||
@@ -3562,18 +3529,11 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
def get_party_and_account_balance(
|
||||
company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None
|
||||
):
|
||||
show_account_balance = frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
return frappe._dict(
|
||||
{
|
||||
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_party_balance")
|
||||
else 0,
|
||||
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center)
|
||||
if show_account_balance
|
||||
else 0,
|
||||
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center)
|
||||
if show_account_balance
|
||||
else 0,
|
||||
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
|
||||
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
|
||||
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3616,16 +3576,3 @@ def make_payment_order(source_name, target_doc=None):
|
||||
@erpnext.allow_regional
|
||||
def add_regional_gl_entries(gl_entries, doc):
|
||||
return
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_bank_transactions(payment_entry: str) -> list:
|
||||
frappe.has_permission("Payment Entry", ptype="read", doc=payment_entry, throw=True)
|
||||
return frappe.get_all(
|
||||
"Bank Transaction Payments",
|
||||
filters={
|
||||
"payment_document": "Payment Entry",
|
||||
"payment_entry": payment_entry,
|
||||
},
|
||||
pluck="parent",
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user