mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-10 00:19:00 +00:00
Compare commits
1 Commits
accounts-f
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bac036ac53 |
@@ -1,12 +0,0 @@
|
||||
reviews:
|
||||
auto_review:
|
||||
ignore_title_keywords:
|
||||
- "sync translations"
|
||||
- "update POT file"
|
||||
- "style: "
|
||||
review_status: false
|
||||
poem: false
|
||||
collapse_walkthrough: true
|
||||
sequence_diagrams: false
|
||||
changed_files_summary: false
|
||||
high_level_summary: false
|
||||
@@ -42,6 +42,3 @@ a308792ee7fda18a681e9181f4fd00b36385bc23
|
||||
# noisy typing refactoring of get_item_details
|
||||
7b7211ac79c248a79ba8a999ff34e734d874c0ae
|
||||
d827ed21adc7b36047e247cbb0dc6388d048a7f9
|
||||
|
||||
# `frappe.flags.in_test` => `frappe.in_test`
|
||||
7a482a69985c952de0e8193c9d4e086aee65ee6d
|
||||
|
||||
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
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.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.
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
|
||||
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
@@ -66,7 +66,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile
|
||||
sed -i 's/socketio:/# socketio:/g' Procfile
|
||||
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
||||
|
||||
bench get-app payments --branch develop
|
||||
bench get-app payments --branch ${githubbranch%"-hotfix"}
|
||||
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
||||
|
||||
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||
|
||||
3
.github/workflows/backport.yml
vendored
3
.github/workflows/backport.yml
vendored
@@ -5,9 +5,6 @@ on:
|
||||
- closed
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/docker-release.yml
vendored
4
.github/workflows/docker-release.yml
vendored
@@ -2,10 +2,6 @@ name: Trigger Docker build on release
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
curl:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
3
.github/workflows/docs-checker.yml
vendored
3
.github/workflows/docs-checker.yml
vendored
@@ -3,9 +3,6 @@ on:
|
||||
pull_request:
|
||||
types: [ opened, synchronize, reopened, edited ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
4
.github/workflows/initiate_release.yml
vendored
4
.github/workflows/initiate_release.yml
vendored
@@ -2,10 +2,6 @@
|
||||
# To add/remove versions just modify the matrix.
|
||||
|
||||
name: Create weekly release pull requests
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 9:30 UTC => 3 PM IST Tuesday
|
||||
|
||||
4
.github/workflows/labeller.yml
vendored
4
.github/workflows/labeller.yml
vendored
@@ -3,10 +3,6 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
3
.github/workflows/linters.yml
vendored
3
.github/workflows/linters.yml
vendored
@@ -3,9 +3,6 @@ name: Linters
|
||||
on:
|
||||
pull_request: { }
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
linters:
|
||||
|
||||
10
.github/workflows/patch.yml
vendored
10
.github/workflows/patch.yml
vendored
@@ -8,14 +8,8 @@ on:
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- '**.csv'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||
cancel-in-progress: true
|
||||
@@ -42,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
python -m compileall -fq "${GITHUB_WORKSPACE}"
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
@@ -85,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
6
.github/workflows/patch_faux.yml
vendored
6
.github/workflows/patch_faux.yml
vendored
@@ -10,12 +10,6 @@ on:
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- "**.csv"
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -3,10 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- version-13
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
|
||||
5
.github/workflows/run-indinvidual-tests.yml
vendored
5
.github/workflows/run-indinvidual-tests.yml
vendored
@@ -7,9 +7,6 @@ concurrency:
|
||||
group: server-individual-tests-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
discover:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -111,7 +108,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
@@ -9,12 +9,6 @@ on:
|
||||
- "**.css"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
10
.github/workflows/server-tests-mariadb.yml
vendored
10
.github/workflows/server-tests-mariadb.yml
vendored
@@ -9,9 +9,6 @@ on:
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
schedule:
|
||||
# Run everday at midnight UTC / 5:30 IST
|
||||
- cron: "0 0 * * *"
|
||||
@@ -28,9 +25,6 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||
cancel-in-progress: true
|
||||
@@ -71,7 +65,7 @@ jobs:
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
python -m compileall -fq "${GITHUB_WORKSPACE}"
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
@@ -109,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
10
.github/workflows/server-tests-postgres.yml
vendored
10
.github/workflows/server-tests-postgres.yml
vendored
@@ -6,18 +6,12 @@ on:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
types: [opened, labelled, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}
|
||||
@@ -56,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Check for valid Python & Merge Conflicts
|
||||
run: |
|
||||
python -m compileall -fq "${GITHUB_WORKSPACE}"
|
||||
python -m compileall -f "${GITHUB_WORKSPACE}"
|
||||
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
|
||||
then echo "Found merge conflicts"
|
||||
exit 1
|
||||
@@ -94,7 +88,7 @@ jobs:
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
exclude: 'node_modules|.git'
|
||||
default_stages: [pre-commit]
|
||||
default_stages: [commit]
|
||||
fail_fast: false
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ repos:
|
||||
cypress/.*|
|
||||
.*node_modules.*|
|
||||
.*boilerplate.*|
|
||||
erpnext/public/js/controllers/.*|
|
||||
erpnext/templates/pages/order.js|
|
||||
erpnext/templates/includes/.*
|
||||
)$
|
||||
|
||||
|
||||
13
CODEOWNERS
13
CODEOWNERS
@@ -8,16 +8,17 @@ erpnext/assets/ @khushi8112
|
||||
erpnext/regional @ruthra-kumar
|
||||
erpnext/selling @ruthra-kumar
|
||||
erpnext/support/ @ruthra-kumar
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/buying/ @rohitwaghchaure
|
||||
erpnext/maintenance/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/manufacturing/ @rohitwaghchaure
|
||||
erpnext/quality_management/ @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/subcontracting @mihir-kandoi
|
||||
erpnext/stock/ @rohitwaghchaure
|
||||
erpnext/subcontracting @rohitwaghchaure
|
||||
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure
|
||||
erpnext/patches/ @ruthra-kumar
|
||||
|
||||
.github/ @ruthra-kumar
|
||||
pyproject.toml @ruthra-kumar
|
||||
pyproject.toml @akhilnarang
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<p>Powerful, Intuitive and Open-Source ERP</p>
|
||||
</p>
|
||||
|
||||
[](https://frappe.school)<br><br>
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
|
||||
[](https://hub.docker.com/r/frappe/erpnext-worker)
|
||||
|
||||
@@ -133,7 +132,7 @@ To setup the repository locally follow the steps mentioned below:
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -142,7 +141,6 @@ To setup the repository locally follow the steps mentioned below:
|
||||
1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
|
||||
1. [Report Security Vulnerabilities](https://erpnext.com/security)
|
||||
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
2. [Translations](https://crowdin.com/project/frappe)
|
||||
|
||||
|
||||
## Logo and Trademark Policy
|
||||
|
||||
@@ -57,7 +57,7 @@ def get_company_currency(company):
|
||||
|
||||
def set_perpetual_inventory(enable=1, company=None):
|
||||
if not company:
|
||||
company = "_Test Company" if frappe.in_test else get_default_company()
|
||||
company = "_Test Company" if frappe.flags.in_test else get_default_company()
|
||||
|
||||
company = frappe.get_doc("Company", company)
|
||||
company.enable_perpetual_inventory = enable
|
||||
@@ -77,7 +77,7 @@ def encode_company_abbr(name, company=None, abbr=None):
|
||||
|
||||
def is_perpetual_inventory_enabled(company):
|
||||
if not company:
|
||||
company = "_Test Company" if frappe.in_test else get_default_company()
|
||||
company = "_Test Company" if frappe.flags.in_test else get_default_company()
|
||||
|
||||
if not hasattr(frappe.local, "enable_perpetual_inventory"):
|
||||
frappe.local.enable_perpetual_inventory = {}
|
||||
|
||||
@@ -10,10 +10,8 @@ from frappe.contacts.doctype.address.address import (
|
||||
class ERPNextAddress(Address):
|
||||
def validate(self):
|
||||
self.validate_reference()
|
||||
self.update_company_address()
|
||||
|
||||
if hasattr(super(), "validate"):
|
||||
super().validate()
|
||||
self.update_compnay_address()
|
||||
super().validate()
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
@@ -22,7 +20,7 @@ class ERPNextAddress(Address):
|
||||
|
||||
return super().link_address()
|
||||
|
||||
def update_company_address(self):
|
||||
def update_compnay_address(self):
|
||||
for link in self.get("links"):
|
||||
if link.link_doctype == "Company":
|
||||
self.is_your_company_address = 1
|
||||
@@ -40,10 +38,6 @@ class ERPNextAddress(Address):
|
||||
"""
|
||||
After Address is updated, update the related 'Primary Address' on Customer.
|
||||
"""
|
||||
|
||||
if hasattr(super(), "on_update"):
|
||||
super().on_update()
|
||||
|
||||
address_display = get_address_display(self.as_dict())
|
||||
filters = {"customer_primary_address": self.name}
|
||||
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
||||
|
||||
@@ -7,7 +7,6 @@ from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
@@ -47,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))
|
||||
|
||||
@@ -319,7 +317,7 @@ def get_already_booked_amount(doc, item):
|
||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date")
|
||||
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item,
|
||||
@@ -528,7 +526,7 @@ def make_gl_entries(
|
||||
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
|
||||
raise e
|
||||
else:
|
||||
|
||||
@@ -92,10 +92,8 @@ class Account(NestedSet):
|
||||
super().on_update()
|
||||
|
||||
def onload(self):
|
||||
role_allowed_for_frozen_entries = frappe.db.get_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries in frappe.get_roles():
|
||||
frozen_accounts_modifier = frappe.db.get_single_value("Accounts Settings", "frozen_accounts_modifier")
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
|
||||
self.set_onload("can_freeze_account", True)
|
||||
|
||||
def autoname(self):
|
||||
@@ -110,7 +108,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()
|
||||
@@ -170,7 +167,7 @@ class Account(NestedSet):
|
||||
if par.root_type:
|
||||
self.root_type = par.root_type
|
||||
|
||||
if cint(self.is_group):
|
||||
if self.is_group:
|
||||
db_value = self.get_doc_before_save()
|
||||
if db_value:
|
||||
if self.report_type != db_value.report_type:
|
||||
@@ -213,7 +210,7 @@ class Account(NestedSet):
|
||||
if doc_before_save and not doc_before_save.parent_account:
|
||||
throw(_("Root cannot be edited."), RootNotEditable)
|
||||
|
||||
if not self.parent_account and not cint(self.is_group):
|
||||
if not self.parent_account and not self.is_group:
|
||||
throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
|
||||
|
||||
def validate_root_company_and_sync_account_to_children(self):
|
||||
@@ -255,14 +252,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):
|
||||
@@ -270,44 +259,21 @@ class Account(NestedSet):
|
||||
|
||||
if self.check_gle_exists():
|
||||
throw(_("Account with existing transaction cannot be converted to ledger"))
|
||||
elif cint(self.is_group):
|
||||
elif 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:
|
||||
return
|
||||
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
|
||||
throw(_("You are not authorized to set Frozen value"))
|
||||
|
||||
def validate_balance_must_be_debit_or_credit(self):
|
||||
@@ -336,9 +302,7 @@ class Account(NestedSet):
|
||||
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
self.currency_explicitly_specified = False
|
||||
|
||||
gl_currency = frappe.db.get_value(
|
||||
"GL Entry", {"account": self.name, "is_cancelled": 0}, "account_currency"
|
||||
)
|
||||
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
|
||||
|
||||
if gl_currency and self.account_currency != gl_currency:
|
||||
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
||||
@@ -638,7 +602,7 @@ def _ensure_idle_system():
|
||||
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
|
||||
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
|
||||
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
last_gl_update = None
|
||||
@@ -659,27 +623,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",
|
||||
}
|
||||
|
||||
@@ -139,11 +139,6 @@ frappe.treeview_settings["Account"] = {
|
||||
description: __(
|
||||
"Further accounts can be made under Groups, but entries can be made against non-Groups"
|
||||
),
|
||||
onchange: function () {
|
||||
if (!this.value) {
|
||||
this.layout.set_value("root_type", "");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Select",
|
||||
@@ -270,14 +265,12 @@ frappe.treeview_settings["Account"] = {
|
||||
label: __("View Ledger"),
|
||||
click: function (node, btn) {
|
||||
frappe.route_options = {
|
||||
account: node.label,
|
||||
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||
company:
|
||||
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
|
||||
};
|
||||
if (node.parent_label) {
|
||||
frappe.route_options["account"] = node.label;
|
||||
}
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
btnClass: "hidden-xs",
|
||||
|
||||
@@ -18,7 +18,6 @@ def create_charts(
|
||||
accounts = []
|
||||
|
||||
def _import_accounts(children, parent, root_type, root_account=False):
|
||||
nonlocal custom_chart
|
||||
for account_name, child in children.items():
|
||||
if root_account:
|
||||
root_type = child.get("root_type")
|
||||
@@ -56,8 +55,7 @@ def create_charts(
|
||||
"account_number": account_number,
|
||||
"account_type": child.get("account_type"),
|
||||
"account_currency": child.get("account_currency")
|
||||
if custom_chart
|
||||
else frappe.get_cached_value("Company", company, "default_currency"),
|
||||
or frappe.get_cached_value("Company", company, "default_currency"),
|
||||
"tax_rate": child.get("tax_rate"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,817 +0,0 @@
|
||||
{
|
||||
"country_code": "au",
|
||||
"name": "Australia - Chart of Accounts with Account Numbers",
|
||||
"tree": {
|
||||
"Assets": {
|
||||
"Current Assets": {
|
||||
"Cash On Hand": {
|
||||
"Cash On Hand": {
|
||||
"account_number": "11010",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"account_number": "110",
|
||||
"is_group": 1
|
||||
},
|
||||
"Cash at Bank": {
|
||||
"Every Day Bank Account": {
|
||||
"account_number": "11510",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Business Savings Account": {
|
||||
"account_number": "11520"
|
||||
},
|
||||
"Business Term Deposit": {
|
||||
"account_number": "11530"
|
||||
},
|
||||
"account_number": "115",
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Receivables": {
|
||||
"Trade Debtors": {
|
||||
"account_number": "12010",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Provision for Doubtful Debts": {
|
||||
"account_number": "12020"
|
||||
},
|
||||
"Sundry Debtors": {
|
||||
"account_number": "12030"
|
||||
},
|
||||
"Debtor Refund": {
|
||||
"account_number": "12040"
|
||||
},
|
||||
"account_number": "120",
|
||||
"is_group": 1
|
||||
},
|
||||
"Inventory": {
|
||||
"Stock On Hand": {
|
||||
"account_number": "13010",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"WIP - Work In Progress - Manufacturing": {
|
||||
"account_number": "13020"
|
||||
},
|
||||
"account_number": "130",
|
||||
"is_group": 1
|
||||
},
|
||||
"Prepayments": {
|
||||
"Prepayments": {
|
||||
"account_number": "14010"
|
||||
},
|
||||
"Provisional Tax Paid": {
|
||||
"account_number": "14020"
|
||||
},
|
||||
"account_number": "140",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "11",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non Current Assets": {
|
||||
"Plant & Equipment": {
|
||||
"Plant & Equipment": {
|
||||
"account_number": "16010",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Plant & Equipment": {
|
||||
"account_number": "16020",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "160",
|
||||
"is_group": 1
|
||||
},
|
||||
"Motor Vehicle": {
|
||||
"Motor Vehicle": {
|
||||
"account_number": "16110",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Motor Vehicle": {
|
||||
"account_number": "16120",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "161",
|
||||
"is_group": 1
|
||||
},
|
||||
"Office Equipment": {
|
||||
"Office Furniture & Equipment": {
|
||||
"account_number": "16210",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Office Furniture & Equipment": {
|
||||
"account_number": "16220",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "162",
|
||||
"is_group": 1
|
||||
},
|
||||
"Computer Equipment": {
|
||||
"Computer Equipment": {
|
||||
"account_number": "16310",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Computer Equipment": {
|
||||
"account_number": "16320",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "163",
|
||||
"is_group": 1
|
||||
},
|
||||
"Building": {
|
||||
"Buildings": {
|
||||
"account_number": "16410",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation Buildings": {
|
||||
"account_number": "16420",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"CWIP - Construction Work In Progress": {
|
||||
"account_number": "16430",
|
||||
"account_type": "Capital Work in Progress"
|
||||
},
|
||||
"Accumulated Depreciation - Others": {
|
||||
"account_number": "16440",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "164",
|
||||
"is_group": 1
|
||||
},
|
||||
"Related Party": {
|
||||
"Loan to Party 1": {
|
||||
"account_number": "17010"
|
||||
},
|
||||
"account_number": "170",
|
||||
"is_group": 1
|
||||
},
|
||||
"Investments & Unlisted Entities": {
|
||||
"Investment - Entity 1": {
|
||||
"account_number": "17510"
|
||||
},
|
||||
"account_number": "175",
|
||||
"is_group": 1
|
||||
},
|
||||
"Intagible Assets": {
|
||||
"Goodwill": {
|
||||
"account_number": "18010"
|
||||
},
|
||||
"Opening Balance Temporary ": {
|
||||
"account_number": "18090",
|
||||
"account_type": "Temporary"
|
||||
},
|
||||
"account_number": "180",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "16",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "1",
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Liabilities": {
|
||||
"Current Liabilities": {
|
||||
"Trade Payables - Current": {
|
||||
"Trade Creditors": {
|
||||
"account_number": "21010",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"Goods Received Not Invoiced": {
|
||||
"account_number": "21050",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Service Received Not Invoiced": {
|
||||
"account_number": "21060"
|
||||
},
|
||||
"Asset Received Not Invoiced": {
|
||||
"account_number": "21070",
|
||||
"account_type": "Asset Received But Not Billed"
|
||||
},
|
||||
"account_number": "210",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Payables - Current": {
|
||||
"Accrued Expenses": {
|
||||
"account_number": "21510"
|
||||
},
|
||||
"Payroll - Wages Clearing": {
|
||||
"account_number": "21550"
|
||||
},
|
||||
"Payroll - Superannuation Deductions": {
|
||||
"account_number": "21555"
|
||||
},
|
||||
"Payroll - Misc Deductions": {
|
||||
"account_number": "21560"
|
||||
},
|
||||
"Payroll - Withholding Tax Payable": {
|
||||
"account_number": "21565"
|
||||
},
|
||||
"account_number": "215",
|
||||
"is_group": 1
|
||||
},
|
||||
"GST": {
|
||||
"GST Payments to ATO": {
|
||||
"account_number": "22030"
|
||||
},
|
||||
"Provision for PAYG Tax": {
|
||||
"account_number": "22040"
|
||||
},
|
||||
"account_number": "220",
|
||||
"account_type": "Tax",
|
||||
"is_group": 1
|
||||
},
|
||||
"Interest & Non Bearing Liabilities - Current": {
|
||||
"Credit Card - VISA": {
|
||||
"account_number": "22510"
|
||||
},
|
||||
"account_number": "225",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bank Overdraft": {
|
||||
"Bank Overdraft Cash at Bank": {
|
||||
"account_number": "23010"
|
||||
},
|
||||
"account_number": "230",
|
||||
"is_group": 1
|
||||
},
|
||||
"Trade Finance": {
|
||||
"Trade Finance": {
|
||||
"account_number": "23510"
|
||||
},
|
||||
"account_number": "235",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities": {
|
||||
"Finance Lease - Current": {
|
||||
"account_number": "24010"
|
||||
},
|
||||
"account_number": "240",
|
||||
"is_group": 1
|
||||
},
|
||||
"Provisions": {
|
||||
"Provision for Long Service Leave": {
|
||||
"account_number": "24510"
|
||||
},
|
||||
"Provision for Holiday Pay": {
|
||||
"account_number": "24520"
|
||||
},
|
||||
"account_number": "245",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "21",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non Current Liabilities": {
|
||||
"Trade & Other Payables - Non Current": {
|
||||
"Loan Account - Party 1": {
|
||||
"account_number": "25010"
|
||||
},
|
||||
"account_number": "250",
|
||||
"is_group": 1
|
||||
},
|
||||
"Interest & Non Bearing Liabilities - Non Current": {
|
||||
"Non Current Liability - Director Loan": {
|
||||
"account_number": "25510"
|
||||
},
|
||||
"account_number": "255",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bank Loans - Non Current": {
|
||||
"Bank Loan 1 - Non Current": {
|
||||
"account_number": "26010"
|
||||
},
|
||||
"account_number": "260",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities - Non Current": {
|
||||
"Finance Lease - Non Current": {
|
||||
"account_number": "27010"
|
||||
},
|
||||
"account_number": "270",
|
||||
"is_group": 1
|
||||
},
|
||||
"Provisions - Non Current": {
|
||||
"Provision for Long Service Leave": {
|
||||
"account_number": "27510"
|
||||
},
|
||||
"Provision for Holiday Pay": {
|
||||
"account_number": "27520"
|
||||
},
|
||||
"account_number": "275",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "25",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "2",
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Equity": {
|
||||
"Equity": {
|
||||
"Owner's/Shareholder's Equity": {
|
||||
"Owner's/Shareholders Capital": {
|
||||
"account_number": "31010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Owner's/Shareholders Drawings": {
|
||||
"account_number": "31020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "310",
|
||||
"is_group": 1
|
||||
},
|
||||
"Earnings": {
|
||||
"Current Year Earnings": {
|
||||
"account_number": "35010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Retained Earnings": {
|
||||
"account_number": "35020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "350",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "31",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "3",
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"Revenue": {
|
||||
"Revenue": {
|
||||
"Sales Revenue": {
|
||||
"Sales Income": {
|
||||
"account_number": "41010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Freight Income": {
|
||||
"account_number": "41020",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Other Income": {
|
||||
"account_number": "41030",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Service Income": {
|
||||
"account_number": "41040",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"account_number": "410",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Revenue": {
|
||||
"Commission Received": {
|
||||
"account_number": "42010"
|
||||
},
|
||||
"Discounts Received": {
|
||||
"account_number": "42020"
|
||||
},
|
||||
"Interest received": {
|
||||
"account_number": "42030"
|
||||
},
|
||||
"Profit/Loss on Sales of Assets": {
|
||||
"account_number": "42040"
|
||||
},
|
||||
"Rent Received": {
|
||||
"account_number": "42050"
|
||||
},
|
||||
"Sundry Income": {
|
||||
"account_number": "42060"
|
||||
},
|
||||
"account_number": "420",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "41",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "4",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Cost of Goods": {
|
||||
"Cost of Goods": {
|
||||
"Cost of Goods Sold": {
|
||||
"Cost of Goods Sold": {
|
||||
"account_number": "51010",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Freight Expenses (sales related)": {
|
||||
"account_number": "51020"
|
||||
},
|
||||
"Discounts Given": {
|
||||
"account_number": "51030"
|
||||
},
|
||||
"Subcontracting Charges": {
|
||||
"account_number": "51040"
|
||||
},
|
||||
"account_number": "510",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other COGS": {
|
||||
"Purchases - Miscellaneous": {
|
||||
"account_number": "52010"
|
||||
},
|
||||
"Duty & Customs Fees": {
|
||||
"account_number": "52020",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Freight Inwards": {
|
||||
"account_number": "52030",
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
"Stock Adjustment": {
|
||||
"account_number": "52040",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Wirte Off": {
|
||||
"account_number": "52050",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Valuation Expenses": {
|
||||
"account_number": "52060",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Asset Valuation Expenses": {
|
||||
"account_number": "52070",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"account_number": "520",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "51",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "5",
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Expenses": {
|
||||
"Fixed Expenses": {
|
||||
"Payroll & Related Expenses": {
|
||||
"Salaries & Wages": {
|
||||
"account_number": "61010"
|
||||
},
|
||||
"Superannuation": {
|
||||
"account_number": "61015"
|
||||
},
|
||||
"Staff Amenities - GST Paid": {
|
||||
"account_number": "61020"
|
||||
},
|
||||
"Staff Amenities - GST Free": {
|
||||
"account_number": "61025"
|
||||
},
|
||||
"Staff Recruitment": {
|
||||
"account_number": "61030"
|
||||
},
|
||||
"Staff Training": {
|
||||
"account_number": "61035"
|
||||
},
|
||||
"Fringe Benefits Tax": {
|
||||
"account_number": "61040"
|
||||
},
|
||||
"Payroll Tax": {
|
||||
"account_number": "61045"
|
||||
},
|
||||
"Workers Compensation": {
|
||||
"account_number": "61050"
|
||||
},
|
||||
"Long Service Leave": {
|
||||
"account_number": "61060"
|
||||
},
|
||||
"Mileage Reimbursement": {
|
||||
"account_number": "61070"
|
||||
},
|
||||
"Overtime": {
|
||||
"account_number": "61080"
|
||||
},
|
||||
"Worksafe Insurance": {
|
||||
"account_number": "61090"
|
||||
},
|
||||
"account_number": "610",
|
||||
"is_group": 1
|
||||
},
|
||||
"Depreciation Expenses": {
|
||||
"Depreciation - Plant & Equipment": {
|
||||
"account_number": "62010",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Motor Vehicle": {
|
||||
"account_number": "62020",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Office Equipment": {
|
||||
"account_number": "62030",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Computer Equipment": {
|
||||
"account_number": "62040",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Building": {
|
||||
"account_number": "62050",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Others": {
|
||||
"account_number": "62510",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"account_number": "620",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "61",
|
||||
"is_group": 1
|
||||
},
|
||||
"Accrued Expenses": {
|
||||
"Accrued Expenses": {
|
||||
"Accrued Expenses - Salaries & Wages": {
|
||||
"account_number": "63010"
|
||||
},
|
||||
"Accrued Expenses - Interest": {
|
||||
"account_number": "63020"
|
||||
},
|
||||
"account_number": "630",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "63",
|
||||
"is_group": 1
|
||||
},
|
||||
"Operating Expenses": {
|
||||
"General and Administrative Expenses": {
|
||||
"Low Value Assets less than $300": {
|
||||
"account_number": "64010"
|
||||
},
|
||||
"Office Supplies": {
|
||||
"account_number": "64020"
|
||||
},
|
||||
"Postage & Courier": {
|
||||
"account_number": "64025"
|
||||
},
|
||||
"Printing & Stationery": {
|
||||
"account_number": "64030"
|
||||
},
|
||||
"Registration Fees / Filing Fees": {
|
||||
"account_number": "64040"
|
||||
},
|
||||
"Travel & Accommodation - Local": {
|
||||
"account_number": "64050"
|
||||
},
|
||||
"Travel & Accommodation - Overseas": {
|
||||
"account_number": "64060"
|
||||
},
|
||||
"Relocation Costs": {
|
||||
"account_number": "64070"
|
||||
},
|
||||
"Hire Charges": {
|
||||
"account_number": "64080"
|
||||
},
|
||||
"Repairs & Maintenance": {
|
||||
"account_number": "64210"
|
||||
},
|
||||
"Cleaning Expenses": {
|
||||
"account_number": "64215"
|
||||
},
|
||||
"Uniforms": {
|
||||
"account_number": "64220"
|
||||
},
|
||||
"Security": {
|
||||
"account_number": "64225"
|
||||
},
|
||||
"Subscriptions & Licences": {
|
||||
"account_number": "64510"
|
||||
},
|
||||
"Software Expenses": {
|
||||
"account_number": "64515"
|
||||
},
|
||||
"Marketing Expenses": {
|
||||
"account_number": "64520"
|
||||
},
|
||||
"Advertising Expenses": {
|
||||
"account_number": "64525"
|
||||
},
|
||||
"Website Hosting & Domain Expenses": {
|
||||
"account_number": "64530"
|
||||
},
|
||||
"Computer Repairs / Supplies": {
|
||||
"account_number": "64540"
|
||||
},
|
||||
"Conferences": {
|
||||
"account_number": "64550"
|
||||
},
|
||||
"Consultancy /Contract Services": {
|
||||
"account_number": "64560"
|
||||
},
|
||||
"Training Services": {
|
||||
"account_number": "64570"
|
||||
},
|
||||
"Workshop Supplies": {
|
||||
"account_number": "64580"
|
||||
},
|
||||
"Consumables": {
|
||||
"account_number": "64585"
|
||||
},
|
||||
"Entertainment Expenses - Deductible": {
|
||||
"account_number": "64810"
|
||||
},
|
||||
"Entertainment Expenses - Non Deductible": {
|
||||
"account_number": "64820"
|
||||
},
|
||||
"Amortisation Of Goodwill": {
|
||||
"account_number": "64910"
|
||||
},
|
||||
"General / Miscellaneous Expenses": {
|
||||
"account_number": "64915",
|
||||
"account_type": "Chargeable"
|
||||
},
|
||||
"Donations": {
|
||||
"account_number": "64920"
|
||||
},
|
||||
"Client Gifts": {
|
||||
"account_number": "64930"
|
||||
},
|
||||
"Employee Gifts": {
|
||||
"account_number": "64935"
|
||||
},
|
||||
"account_number": "640",
|
||||
"is_group": 1
|
||||
},
|
||||
"Occupancy Expenses": {
|
||||
"Rental Expenses": {
|
||||
"account_number": "65010"
|
||||
},
|
||||
"Property Insurance": {
|
||||
"account_number": "65020"
|
||||
},
|
||||
"Electricity Expenses": {
|
||||
"account_number": "65030"
|
||||
},
|
||||
"Water Rates": {
|
||||
"account_number": "65040"
|
||||
},
|
||||
"Gas Expenses": {
|
||||
"account_number": "65050"
|
||||
},
|
||||
"Property Taxes": {
|
||||
"account_number": "65060"
|
||||
},
|
||||
"Rates": {
|
||||
"account_number": "65070"
|
||||
},
|
||||
"account_number": "650",
|
||||
"is_group": 1
|
||||
},
|
||||
"Communication & Vehicle Expenses": {
|
||||
"Internet Expenses": {
|
||||
"account_number": "66010"
|
||||
},
|
||||
"Mobile Telephone": {
|
||||
"account_number": "66020"
|
||||
},
|
||||
"Telephone Expenses": {
|
||||
"account_number": "66030"
|
||||
},
|
||||
"Motor Vehicle - Fuel Expenses": {
|
||||
"account_number": "66040"
|
||||
},
|
||||
"Motor Vehicle - Parking & Tolls": {
|
||||
"account_number": "66050"
|
||||
},
|
||||
"Motor Vehicle - Registration & Insurance": {
|
||||
"account_number": "66060"
|
||||
},
|
||||
"Motor Vehicle - Service & Repairs": {
|
||||
"account_number": "66070"
|
||||
},
|
||||
"Taxi": {
|
||||
"account_number": "66080"
|
||||
},
|
||||
"account_number": "660",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "64",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non-Operating Expenses": {
|
||||
"Finance Costs": {
|
||||
"Interest - Bank Loans": {
|
||||
"account_number": "67010"
|
||||
},
|
||||
"Interest - Finance Leases": {
|
||||
"account_number": "67020"
|
||||
},
|
||||
"Interest - Other Loans": {
|
||||
"account_number": "67025"
|
||||
},
|
||||
"Insurance": {
|
||||
"account_number": "67030"
|
||||
},
|
||||
"Bank Charges": {
|
||||
"account_number": "67050"
|
||||
},
|
||||
"Rounding off": {
|
||||
"account_number": "67055",
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
"Audit Fees": {
|
||||
"account_number": "67060"
|
||||
},
|
||||
"Accounting Fees": {
|
||||
"account_number": "67070"
|
||||
},
|
||||
"Legal Fees": {
|
||||
"account_number": "67080"
|
||||
},
|
||||
"Management Fees": {
|
||||
"account_number": "67090"
|
||||
},
|
||||
"account_number": "670",
|
||||
"is_group": 1
|
||||
},
|
||||
"Other Costs": {
|
||||
"Doubtful Debts": {
|
||||
"account_number": "67510"
|
||||
},
|
||||
"Fines": {
|
||||
"account_number": "67520"
|
||||
},
|
||||
"Debt Collection": {
|
||||
"account_number": "67530"
|
||||
},
|
||||
"Bad Debts": {
|
||||
"account_number": "67540"
|
||||
},
|
||||
"account_number": "675",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "67",
|
||||
"is_group": 1
|
||||
},
|
||||
"Variable Expenses": {
|
||||
"Variable Expenses": {
|
||||
"Bonus & Commissions Paid": {
|
||||
"account_number": "68010"
|
||||
},
|
||||
"Bonus & Commissions To be Paid": {
|
||||
"account_number": "68020"
|
||||
},
|
||||
"Warranty Claims": {
|
||||
"account_number": "68030"
|
||||
},
|
||||
"account_number": "680",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "68",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "6",
|
||||
"root_type": "Expense"
|
||||
},
|
||||
"Other Income": {
|
||||
"Other Income": {
|
||||
"Interest Income": {
|
||||
"Interest Income": {
|
||||
"account_number": "71010"
|
||||
},
|
||||
"account_number": "710",
|
||||
"is_group": 1
|
||||
},
|
||||
"Asset Disposal Income": {
|
||||
"Gain on Asset Disposal": {
|
||||
"account_number": "73010"
|
||||
},
|
||||
"account_number": "730",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "71",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "7",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Other Expenses": {
|
||||
"Other Expenses": {
|
||||
"Income Tax Expenses": {
|
||||
"Income Tax Expenses": {
|
||||
"account_number": "81010"
|
||||
},
|
||||
"account_number": "810",
|
||||
"is_group": 1
|
||||
},
|
||||
"Foreign Exchange Gain/Loss": {
|
||||
"Exchange Loss/Gain - Realized": {
|
||||
"account_number": "82010"
|
||||
},
|
||||
"account_number": "820",
|
||||
"is_group": 1
|
||||
},
|
||||
"Asset Disposal Expenses": {
|
||||
"Loss on Asset Disposal": {
|
||||
"account_number": "83010"
|
||||
},
|
||||
"account_number": "830",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "81",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "8",
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -96,20 +96,8 @@
|
||||
"account_number": "1132.000"
|
||||
},
|
||||
"account_number": "1130.000"
|
||||
},
|
||||
"Pajak Dibayar di Muka": {
|
||||
"PPN Masukan": {
|
||||
"account_number": "1151.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"PPh 23 Dibayar di Muka": {
|
||||
"account_number": "1152.001",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "1150.000"
|
||||
},
|
||||
},
|
||||
"account_number": "1100.000"
|
||||
|
||||
},
|
||||
"Aktiva Tetap": {
|
||||
"Aktiva": {
|
||||
@@ -569,10 +557,6 @@
|
||||
"Hutang Pajak": {
|
||||
"account_number": "2141.000",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"PPN Keluaran": {
|
||||
"account_number": "2142.000",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "2140.000"
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ def get():
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
|
||||
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {"account_type": "Payable"},
|
||||
_("Employee Advances"): {},
|
||||
},
|
||||
_("Securities and Deposits"): {_("Earnest Money"): {}},
|
||||
_("Stock Assets"): {
|
||||
|
||||
@@ -20,7 +20,7 @@ def get():
|
||||
"account_number": "1100",
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {"account_number": "1610", "account_type": "Payable"},
|
||||
_("Employee Advances"): {"account_number": "1610"},
|
||||
"account_number": "1600",
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"reporting_currency_exchange_rate",
|
||||
"debit_in_reporting_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"account_currency",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
@@ -127,30 +124,12 @@
|
||||
"fieldname": "is_period_closing_voucher_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Period Closing Voucher Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-22 19:13:50.400404",
|
||||
"modified": "2024-03-27 13:05:56.710541",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Closing Balance",
|
||||
@@ -179,8 +158,7 @@
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,12 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr, flt
|
||||
from frappe.utils import cint, cstr
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.exceptions import ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class AccountClosingBalance(Document):
|
||||
@@ -29,15 +26,12 @@ class AccountClosingBalance(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
finance_book: DF.Link | None
|
||||
is_period_closing_voucher_entry: DF.Check
|
||||
period_closing_voucher: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -61,7 +55,6 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
"closing_date": closing_date,
|
||||
}
|
||||
)
|
||||
set_amount_in_reporting_currency(cle, company, closing_date)
|
||||
cle.flags.ignore_permissions = True
|
||||
cle.flags.ignore_links = True
|
||||
cle.submit()
|
||||
@@ -151,29 +144,3 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
||||
entries = query.run(as_dict=1)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def set_amount_in_reporting_currency(cle, company, closing_date):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
|
||||
reporting_currency_exchange_rate = get_exchange_rate(default_currency, reporting_currency, closing_date)
|
||||
if not reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, closing_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
debit_in_reporting_currency = flt(cle.get("debit", 0) * reporting_currency_exchange_rate)
|
||||
credit_in_reporting_currency = flt(cle.get("credit", 0) * reporting_currency_exchange_rate)
|
||||
|
||||
cle.update(
|
||||
{
|
||||
"reporting_currency_exchange_rate": reporting_currency_exchange_rate,
|
||||
"debit_in_reporting_currency": debit_in_reporting_currency,
|
||||
"credit_in_reporting_currency": credit_in_reporting_currency,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
class UnitTestAccountClosingBalance(UnitTestCase):
|
||||
"""
|
||||
Unit tests for AccountClosingBalance.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestAccountClosingBalance(IntegrationTestCase):
|
||||
|
||||
@@ -83,7 +83,7 @@ class AccountingDimension(Document):
|
||||
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
@@ -91,7 +91,7 @@ class AccountingDimension(Document):
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
|
||||
@@ -111,15 +111,17 @@ class AccountingDimension(Document):
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if not doclist:
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
|
||||
doc_count = len(get_accounting_dimensions())
|
||||
count = 0
|
||||
repostable_doctypes = get_allowed_types_from_settings(child_doc=True)
|
||||
repostable_doctypes = get_allowed_types_from_settings()
|
||||
|
||||
for doctype in doclist:
|
||||
if (doc_count + 1) % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
insert_after_field = "accounting_dimensions_section"
|
||||
|
||||
df = {
|
||||
"fieldname": doc.fieldname,
|
||||
"label": doc.label,
|
||||
@@ -211,7 +213,7 @@ def delete_accounting_dimension(doc):
|
||||
|
||||
@frappe.whitelist()
|
||||
def disable_dimension(doc):
|
||||
if frappe.in_test:
|
||||
if frappe.flags.in_test:
|
||||
toggle_disabling(doc=doc)
|
||||
else:
|
||||
frappe.enqueue(toggle_disabling, doc=doc)
|
||||
|
||||
@@ -58,10 +58,6 @@ class TestAccountingDimension(IntegrationTestCase):
|
||||
self.assertEqual(gle1.get("department"), "_Test Department - _TC")
|
||||
|
||||
def test_mandatory(self):
|
||||
location = frappe.get_doc("Accounting Dimension", "Location")
|
||||
location.dimension_defaults[0].mandatory_for_bs = True
|
||||
location.save()
|
||||
|
||||
si = create_sales_invoice(do_not_save=1)
|
||||
si.append(
|
||||
"items",
|
||||
@@ -125,6 +121,7 @@ def create_dimension():
|
||||
"company": "_Test Company",
|
||||
"reference_document": "Location",
|
||||
"default_dimension": "Block 1",
|
||||
"mandatory_for_bs": 1,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"accounting_dimension",
|
||||
"fieldname",
|
||||
"disabled",
|
||||
"column_break_2",
|
||||
"company",
|
||||
@@ -91,17 +90,11 @@
|
||||
"fieldname": "apply_restriction_on_values",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply restriction on dimension values"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Fieldname"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-08 14:13:22.203011",
|
||||
"modified": "2024-03-27 13:05:57.199186",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension Filter",
|
||||
@@ -146,9 +139,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,17 @@ class AccountingDimensionFilter(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import (
|
||||
ApplicableOnAccount,
|
||||
)
|
||||
|
||||
accounting_dimension: DF.Literal[None]
|
||||
accounting_dimension: DF.Literal
|
||||
accounts: DF.Table[ApplicableOnAccount]
|
||||
allow_or_restrict: DF.Literal["Allow", "Restrict"]
|
||||
apply_restriction_on_values: DF.Check
|
||||
company: DF.Link
|
||||
dimensions: DF.Table[AllowedDimension]
|
||||
disabled: DF.Check
|
||||
fieldname: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
@@ -36,10 +37,6 @@ class AccountingDimensionFilter(Document):
|
||||
self.set("dimensions", [])
|
||||
|
||||
def validate(self):
|
||||
self.fieldname = frappe.db.get_value(
|
||||
"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
|
||||
) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension
|
||||
|
||||
self.validate_applicable_accounts()
|
||||
|
||||
def validate_applicable_accounts(self):
|
||||
@@ -74,7 +71,7 @@ def get_dimension_filter_map():
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, p.fieldname, a.is_mandatory
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
@@ -89,6 +86,8 @@ def get_dimension_filter_map():
|
||||
dimension_filter_map = {}
|
||||
|
||||
for f in filters:
|
||||
f.fieldname = scrub(f.accounting_dimension)
|
||||
|
||||
build_map(
|
||||
dimension_filter_map,
|
||||
f.fieldname,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"end_date",
|
||||
"column_break_4",
|
||||
"company",
|
||||
"disabled",
|
||||
"section_break_7",
|
||||
"closed_documents"
|
||||
],
|
||||
@@ -50,13 +49,6 @@
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -70,11 +62,10 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-10-06 15:00:15.568067",
|
||||
"modified": "2024-03-27 13:05:57.388109",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Period",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -114,9 +105,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ class AccountingPeriod(Document):
|
||||
|
||||
closed_documents: DF.Table[ClosedDocument]
|
||||
company: DF.Link
|
||||
disabled: DF.Check
|
||||
end_date: DF.Date
|
||||
period_name: DF.Data
|
||||
start_date: DF.Date
|
||||
@@ -117,7 +116,6 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
.where(
|
||||
(ap.name == cd.parent)
|
||||
& (ap.company == doc.company)
|
||||
& (ap.disabled == 0)
|
||||
& (cd.closed == 1)
|
||||
& (cd.document_type == doc.doctype)
|
||||
& (date >= ap.start_date)
|
||||
|
||||
@@ -22,32 +22,4 @@ frappe.ui.form.on("Accounts Settings", {
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
add_taxes_from_taxes_and_charges_template(frm) {
|
||||
toggle_tax_settings(frm, "add_taxes_from_taxes_and_charges_template");
|
||||
},
|
||||
|
||||
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) {
|
||||
if (frm.doc[field_name]) {
|
||||
const other_field =
|
||||
field_name === "add_taxes_from_item_tax_template"
|
||||
? "add_taxes_from_taxes_and_charges_template"
|
||||
: "add_taxes_from_item_tax_template";
|
||||
frm.set_value(other_field, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
"confirm_before_resetting_posting_date",
|
||||
"journals_section",
|
||||
"merge_similar_account_heads",
|
||||
"deferred_accounting_settings_section",
|
||||
@@ -32,7 +31,6 @@
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"add_taxes_from_taxes_and_charges_template",
|
||||
"book_tax_discount_loss",
|
||||
"round_row_wise_tax",
|
||||
"print_settings",
|
||||
@@ -40,15 +38,8 @@
|
||||
"show_taxes_as_table_in_print",
|
||||
"column_break_12",
|
||||
"show_payment_schedule_in_print",
|
||||
"item_price_settings_section",
|
||||
"maintain_same_internal_transaction_rate",
|
||||
"fetch_valuation_rate_for_internal_transaction",
|
||||
"column_break_feyo",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"allow_pegged_currencies_exchange_rates",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"section_break_jpd0",
|
||||
@@ -67,7 +58,6 @@
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"column_break_xrnd",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"calculate_depr_using_total_days",
|
||||
@@ -75,8 +65,10 @@
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa",
|
||||
"banking_tab",
|
||||
@@ -85,20 +77,28 @@
|
||||
"reports_tab",
|
||||
"remarks_section",
|
||||
"general_ledger_remarks_length",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"column_break_lvjk",
|
||||
"receivable_payable_remarks_length",
|
||||
"accounts_receivable_payable_tuning_section",
|
||||
"receivable_payable_fetch_method",
|
||||
"column_break_ntmi",
|
||||
"drop_ar_procedures",
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status",
|
||||
"budget_settings",
|
||||
"use_legacy_budget_controller"
|
||||
"create_pr_in_draft_status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||
"fieldname": "acc_frozen_upto",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Accounts Frozen Till Date"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
||||
"fieldname": "frozen_accounts_modifier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "Billing Address",
|
||||
"description": "Address used to determine Tax Category in transactions",
|
||||
@@ -532,117 +532,14 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Posting Date Inheritance for Exchange Gain / Loss",
|
||||
"options": "Invoice\nPayment\nReconciliation Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xrnd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Buffered Cursor",
|
||||
"fieldname": "receivable_payable_fetch_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Data Fetch Method",
|
||||
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_receivable_payable_tuning_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Receivable / Payable Tuning"
|
||||
},
|
||||
{
|
||||
"fieldname": "legacy_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Legacy Fields"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_internal_transaction_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain Same Rate Throughout Internal Transaction"
|
||||
},
|
||||
{
|
||||
"default": "Stop",
|
||||
"depends_on": "maintain_same_internal_transaction_rate",
|
||||
"fieldname": "maintain_same_rate_action",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Same Rate is Not Maintained Throughout Internal Transaction",
|
||||
"mandatory_depends_on": "maintain_same_internal_transaction_rate",
|
||||
"options": "Stop\nWarn"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'",
|
||||
"fieldname": "role_to_override_stop_action",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "budget_settings",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Budget"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, user will be alerted before resetting posting date to current date in relevant transactions",
|
||||
"fieldname": "confirm_before_resetting_posting_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Confirm before resetting posting date"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_price_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Item Price Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_feyo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System will do an implicit conversion using the pegged currency. <br>\nEx: Instead of AED -> INR, system will do AED -> USD -> INR using the pegged exchange rate of AED against USD.",
|
||||
"documentation_url": "/app/pegged-currencies/Pegged Currencies",
|
||||
"fieldname": "allow_pegged_currencies_exchange_rates",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Implicit Pegged Currency Conversion"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.",
|
||||
"fieldname": "add_taxes_from_taxes_and_charges_template",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Add Taxes from Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"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": "0",
|
||||
"fieldname": "use_legacy_budget_controller",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Budget Controller"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-24 16:08:08.515254",
|
||||
"modified": "2025-01-23 13:15:44.077853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -667,9 +564,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
@@ -22,10 +23,9 @@ class AccountsSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
allow_pegged_currencies_exchange_rates: DF.Check
|
||||
allow_stale: DF.Check
|
||||
auto_reconcile_payments: DF.Check
|
||||
auto_reconciliation_job_trigger: DF.Int
|
||||
@@ -37,7 +37,6 @@ class AccountsSettings(Document):
|
||||
book_tax_discount_loss: DF.Check
|
||||
calculate_depr_using_total_days: DF.Check
|
||||
check_supplier_invoice_uniqueness: DF.Check
|
||||
confirm_before_resetting_posting_date: DF.Check
|
||||
create_pr_in_draft_status: DF.Check
|
||||
credit_controller: DF.Link | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
@@ -47,21 +46,17 @@ class AccountsSettings(Document):
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
maintain_same_internal_transaction_rate: DF.Check
|
||||
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||
make_payment_via_journal_entry: DF.Check
|
||||
merge_similar_account_heads: DF.Check
|
||||
over_billing_allowance: DF.Currency
|
||||
post_change_gl_entries: DF.Check
|
||||
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_balance_in_coa: DF.Check
|
||||
show_inclusive_tax_in_print: DF.Check
|
||||
@@ -71,11 +66,9 @@ class AccountsSettings(Document):
|
||||
submit_journal_entries: DF.Check
|
||||
unlink_advance_payment_on_cancelation_of_order: DF.Check
|
||||
unlink_payment_on_cancellation_of_invoice: DF.Check
|
||||
use_legacy_budget_controller: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_tax_settings()
|
||||
old_doc = self.get_doc_before_save()
|
||||
clear_cache = False
|
||||
|
||||
@@ -96,6 +89,9 @@ class AccountsSettings(Document):
|
||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||
self.enable_payment_schedule_in_print()
|
||||
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
@@ -122,6 +118,10 @@ class AccountsSettings(Document):
|
||||
validate_fields_for_doctype=False,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
def validate_and_sync_auto_reconcile_config(self):
|
||||
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
||||
if (
|
||||
@@ -135,21 +135,3 @@ class AccountsSettings(Document):
|
||||
if self.has_value_changed("reconciliation_queue_size"):
|
||||
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
|
||||
frappe.throw(_("Queue Size should be between 5 and 100"))
|
||||
|
||||
def validate_auto_tax_settings(self):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
|
||||
),
|
||||
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}")
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Company", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
|
||||
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
|
||||
frm.set_df_property(
|
||||
"role_allowed_for_frozen_entries",
|
||||
"frozen_accounts_modifier",
|
||||
"label",
|
||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||
);
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"currency",
|
||||
"event",
|
||||
"delinked"
|
||||
"event"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -69,20 +68,12 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delinked",
|
||||
"fieldtype": "Check",
|
||||
"label": "DeLinked",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-13 15:11:58.300836",
|
||||
"modified": "2024-11-05 10:31:28.736671",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
@@ -116,8 +107,7 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.utils import get_advance_payment_doctypes, update_voucher_outstanding
|
||||
|
||||
|
||||
class AdvancePaymentLedgerEntry(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -21,28 +19,9 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
delinked: DF.Check
|
||||
event: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def on_update(self):
|
||||
if (
|
||||
self.against_voucher_type in get_advance_payment_doctypes()
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
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"],
|
||||
)
|
||||
pass
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.utils import nowdate, today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
@@ -96,13 +95,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
|
||||
@@ -132,8 +132,7 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "IBAN",
|
||||
"length": 34,
|
||||
"options": "IBAN"
|
||||
"length": 30
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@@ -209,7 +208,6 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
@@ -252,7 +250,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -284,10 +282,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "bank,account",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class BankAccount(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
@@ -71,6 +72,35 @@ class BankAccount(Document):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_iban(self):
|
||||
"""
|
||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||
"""
|
||||
# IBAN field is optional
|
||||
if not self.iban:
|
||||
return
|
||||
|
||||
def encode_char(c):
|
||||
# Position in the alphabet (A=1, B=2, ...) plus nine
|
||||
return str(9 + ord(c) - 64)
|
||||
|
||||
# remove whitespaces, upper case to get the right number from ord()
|
||||
iban = "".join(self.iban.split(" ")).upper()
|
||||
|
||||
# Move country code and checksum from the start to the end
|
||||
flipped = iban[4:] + iban[:4]
|
||||
|
||||
# Encode characters as numbers
|
||||
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||
|
||||
try:
|
||||
to_check = int("".join(encoded))
|
||||
except ValueError:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
frappe.db.set_value(
|
||||
@@ -79,7 +109,6 @@ class BankAccount(Document):
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"is_company_account": self.is_company_account,
|
||||
"company": self.company,
|
||||
"is_default": 1,
|
||||
"disabled": 0,
|
||||
},
|
||||
@@ -88,6 +117,15 @@ class BankAccount(Document):
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_bank_account(doctype, docname):
|
||||
doc = frappe.new_doc("Bank Account")
|
||||
doc.party_type = doctype
|
||||
doc.party = docname
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value(
|
||||
"Bank Account",
|
||||
|
||||
@@ -8,4 +8,38 @@ from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankAccount(IntegrationTestCase):
|
||||
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()
|
||||
|
||||
@@ -89,64 +89,46 @@ class BankClearance(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
invalid_document = []
|
||||
invalid_cheque_date = []
|
||||
entries_to_update = []
|
||||
|
||||
def validate_entry(d):
|
||||
is_valid = True
|
||||
if not d.payment_document:
|
||||
invalid_document.append(str(d.idx))
|
||||
is_valid = False
|
||||
|
||||
if d.clearance_date and d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
invalid_cheque_date.append(str(d.idx))
|
||||
is_valid = False
|
||||
|
||||
return is_valid
|
||||
|
||||
clearance_date_updated = False
|
||||
for d in self.get("payment_entries"):
|
||||
if validate_entry(d) and (d.clearance_date or self.include_reconciled_entries):
|
||||
if d.clearance_date:
|
||||
if not d.payment_document:
|
||||
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
if d.clearance_date or self.include_reconciled_entries:
|
||||
if not d.clearance_date:
|
||||
d.clearance_date = None
|
||||
|
||||
entries_to_update.append(d)
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
|
||||
if invalid_document or invalid_cheque_date:
|
||||
msg = _("<p>Please correct the following row(s):</p><ul>")
|
||||
if invalid_document:
|
||||
msg += _("<li>Payment document required for row(s): {0}</li>").format(
|
||||
", ".join(invalid_document)
|
||||
)
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
if invalid_cheque_date:
|
||||
msg += _("<li>Clearance date must be after cheque date for row(s): {0}</li>").format(
|
||||
", ".join(invalid_cheque_date)
|
||||
)
|
||||
clearance_date_updated = True
|
||||
|
||||
msg += "</ul>"
|
||||
frappe.throw(_(msg))
|
||||
return
|
||||
|
||||
if not entries_to_update:
|
||||
if clearance_date_updated:
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
else:
|
||||
msgprint(_("Clearance Date not mentioned"))
|
||||
return
|
||||
|
||||
for d in entries_to_update:
|
||||
if d.payment_document == "Sales Invoice":
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice Payment",
|
||||
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||
"clearance_date",
|
||||
d.clearance_date,
|
||||
)
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_lazy_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
|
||||
self.get_payment_entries()
|
||||
msgprint(_("Clearance Date updated"))
|
||||
|
||||
|
||||
def get_payment_entries_for_bank_clearance(
|
||||
@@ -155,10 +137,8 @@ def get_payment_entries_for_bank_clearance(
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
@@ -183,20 +163,19 @@ def get_payment_entries_for_bank_clearance(
|
||||
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
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
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}
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
||||
{condition}
|
||||
order by
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
posting_date ASC, name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
|
||||
@@ -7,9 +7,6 @@ from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
@@ -146,7 +143,7 @@ def make_payment_entry():
|
||||
|
||||
supplier = create_supplier(supplier_name="_Test Supplier")
|
||||
pi = make_purchase_invoice(
|
||||
supplier=supplier.name,
|
||||
supplier=supplier,
|
||||
supplier_warehouse="_Test Warehouse - _TC",
|
||||
expense_account="Cost of Goods Sold - _TC",
|
||||
uom="Nos",
|
||||
@@ -175,13 +172,11 @@ def make_pos_sales_invoice():
|
||||
|
||||
customer = make_customer(customer="_Test Customer")
|
||||
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
|
||||
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
|
||||
|
||||
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
||||
si.set("payments", [])
|
||||
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
|
||||
si.append(
|
||||
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
|
||||
)
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN",
|
||||
"options": "IBAN",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -215,10 +214,9 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:52:33.550847",
|
||||
"modified": "2024-03-27 13:06:37.731207",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
@@ -252,10 +250,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "customer",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "customer"
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, create_batch, flt
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
@@ -374,20 +374,21 @@ def auto_reconcile_vouchers(
|
||||
from_reference_date=None,
|
||||
to_reference_date=None,
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
|
||||
if len(bank_transactions) > 10:
|
||||
for bank_transaction_batch in create_batch(bank_transactions, 1000):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transaction_batch,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transactions,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
frappe.msgprint(_("Auto Reconciliation has started in the background"))
|
||||
else:
|
||||
start_auto_reconcile(
|
||||
@@ -403,13 +404,11 @@ def auto_reconcile_vouchers(
|
||||
def start_auto_reconcile(
|
||||
bank_transactions, from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date
|
||||
):
|
||||
frappe.flags.auto_reconcile_vouchers = True
|
||||
|
||||
reconciled, partially_reconciled = set(), set()
|
||||
for transaction in bank_transactions:
|
||||
linked_payments = get_linked_payments(
|
||||
transaction.name,
|
||||
["payment_entry", "journal_entry", "sales_invoice"],
|
||||
["payment_entry", "journal_entry"],
|
||||
from_date,
|
||||
to_date,
|
||||
filter_by_reference_date,
|
||||
@@ -666,7 +665,7 @@ def get_matching_queries(
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match, currency, common_filters, transaction)
|
||||
query = get_si_matching_query(exact_match, currency, common_filters)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0.0:
|
||||
@@ -854,14 +853,11 @@ def get_je_matching_query(
|
||||
return query
|
||||
|
||||
|
||||
def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
def get_si_matching_query(exact_match, currency, common_filters):
|
||||
# get matching sales invoice query
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
ref_condition = sip.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = sip.amount == common_filters.amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
@@ -874,11 +870,11 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
.join(si)
|
||||
.on(sip.parent == si.name)
|
||||
.select(
|
||||
(ref_rank + party_rank + amount_rank + 1).as_("rank"),
|
||||
(party_rank + amount_rank + 1).as_("rank"),
|
||||
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||
si.name,
|
||||
sip.amount.as_("paid_amount"),
|
||||
sip.reference_no,
|
||||
ConstantColumn("").as_("reference_no"),
|
||||
ConstantColumn("").as_("reference_date"),
|
||||
si.customer.as_("party"),
|
||||
ConstantColumn("Customer").as_("party_type"),
|
||||
@@ -892,9 +888,6 @@ def get_si_matching_query(exact_match, currency, common_filters, transaction):
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class TestBankReconciliationTool(AccountsTestMixin, IntegrationTestCase):
|
||||
{
|
||||
"doctype": "Bank Account",
|
||||
"account_name": "HDFC _current_",
|
||||
"bank": bank.name,
|
||||
"bank": bank,
|
||||
"is_company_account": True,
|
||||
"account": self.bank, # account from Chart of Accounts
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
frm.get_field("import_file").df.options = {
|
||||
restrictions: {
|
||||
allowed_file_types: [".csv", ".xls", ".xlsx", ".TXT", ".txt"],
|
||||
allowed_file_types: [".csv", ".xls", ".xlsx"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -81,7 +81,6 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
refresh(frm) {
|
||||
frm.page.hide_icon_group();
|
||||
frm.trigger("toggle_mt940_note");
|
||||
frm.trigger("update_indicators");
|
||||
frm.trigger("import_file");
|
||||
frm.trigger("show_import_log");
|
||||
@@ -193,24 +192,6 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
});
|
||||
},
|
||||
|
||||
import_mt940_fromat(frm) {
|
||||
frm.trigger("toggle_mt940_note");
|
||||
frm.save();
|
||||
},
|
||||
|
||||
toggle_mt940_note(frm) {
|
||||
if (!frm.doc.import_mt940_fromat) {
|
||||
frm.set_df_property("custom_delimiters", "hidden", 0);
|
||||
frm.set_df_property("google_sheets_url", "hidden", 0);
|
||||
frm.set_df_property("html_5", "hidden", 0);
|
||||
} else {
|
||||
frm.set_df_property("custom_delimiters", "hidden", 1);
|
||||
frm.set_df_property("google_sheets_url", "hidden", 1);
|
||||
frm.set_df_property("html_5", "hidden", 1);
|
||||
}
|
||||
frm.set_value("import_mt940_fromat", frm.doc.import_mt940_fromat);
|
||||
},
|
||||
|
||||
show_report_error_button(frm) {
|
||||
if (frm.doc.status === "Error") {
|
||||
frappe.db
|
||||
@@ -252,7 +233,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
open_url_post(method, {
|
||||
doctype: "Bank Transaction",
|
||||
export_records: "blank_template",
|
||||
export_records: "5_records",
|
||||
export_fields: {
|
||||
"Bank Transaction": [
|
||||
"date",
|
||||
@@ -309,45 +290,23 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
.html(__("Loading import file..."))
|
||||
.appendTo(frm.get_field("import_preview").$wrapper);
|
||||
|
||||
frappe.run_serially([
|
||||
// Convert MT940 to CSV if .txt file
|
||||
() => {
|
||||
if (frm.doc.import_file && frm.doc.import_file.toLowerCase().endsWith(".txt")) {
|
||||
return frm
|
||||
.call({
|
||||
method: "convert_mt940_to_csv",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
mt940_file_path: frm.doc.import_file,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const file_url = r.message;
|
||||
frm.set_value("import_file", file_url);
|
||||
frm.save();
|
||||
});
|
||||
}
|
||||
frm.call({
|
||||
method: "get_preview_from_template",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
import_file: frm.doc.import_file,
|
||||
google_sheets_url: frm.doc.google_sheets_url,
|
||||
},
|
||||
() => {
|
||||
frm.call({
|
||||
method: "get_preview_from_template",
|
||||
args: {
|
||||
data_import: frm.doc.name,
|
||||
import_file: frm.doc.import_file,
|
||||
google_sheets_url: frm.doc.google_sheets_url,
|
||||
},
|
||||
error_handlers: {
|
||||
TimestampMismatchError() {
|
||||
// ignore this error
|
||||
},
|
||||
},
|
||||
}).then((r) => {
|
||||
let preview_data = r.message;
|
||||
frm.events.show_import_preview(frm, preview_data);
|
||||
frm.events.show_import_warnings(frm, preview_data);
|
||||
});
|
||||
error_handlers: {
|
||||
TimestampMismatchError() {
|
||||
// ignore this error
|
||||
},
|
||||
},
|
||||
]);
|
||||
}).then((r) => {
|
||||
let preview_data = r.message;
|
||||
frm.events.show_import_preview(frm, preview_data);
|
||||
frm.events.show_import_warnings(frm, preview_data);
|
||||
});
|
||||
},
|
||||
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"bank_account",
|
||||
"bank",
|
||||
"column_break_4",
|
||||
"import_mt940_fromat",
|
||||
"custom_delimiters",
|
||||
"delimiter_options",
|
||||
"google_sheets_url",
|
||||
@@ -21,7 +20,6 @@
|
||||
"download_template",
|
||||
"status",
|
||||
"template_options",
|
||||
"use_csv_sniffer",
|
||||
"import_warnings_section",
|
||||
"template_warnings",
|
||||
"import_warnings",
|
||||
@@ -209,28 +207,14 @@
|
||||
"fieldname": "delimiter_options",
|
||||
"fieldtype": "Data",
|
||||
"label": "Delimiter options"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_csv_sniffer",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Use CSV Sniffer"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "import_mt940_fromat",
|
||||
"fieldtype": "Check",
|
||||
"label": "Import MT940 Fromat"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-11 02:23:22.159961",
|
||||
"modified": "2024-06-25 17:32:07.658250",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -246,9 +230,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,15 @@
|
||||
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
|
||||
import frappe
|
||||
import mt940
|
||||
import openpyxl
|
||||
from frappe import _
|
||||
from frappe.core.doctype.data_import.data_import import DataImport
|
||||
from frappe.core.doctype.data_import.importer import Importer, ImportFile
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.file_manager import get_file, save_file
|
||||
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
|
||||
from openpyxl.styles import Font
|
||||
from openpyxl.utils import get_column_letter
|
||||
@@ -39,7 +35,6 @@ class BankStatementImport(DataImport):
|
||||
delimiter_options: DF.Data | None
|
||||
google_sheets_url: DF.Data | None
|
||||
import_file: DF.Attach | None
|
||||
import_mt940_fromat: DF.Check
|
||||
import_type: DF.Literal["", "Insert New Records", "Update Existing Records"]
|
||||
mute_emails: DF.Check
|
||||
reference_doctype: DF.Link
|
||||
@@ -48,7 +43,6 @@ class BankStatementImport(DataImport):
|
||||
submit_after_import: DF.Check
|
||||
template_options: DF.Code | None
|
||||
template_warnings: DF.Code | None
|
||||
use_csv_sniffer: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -71,9 +65,8 @@ class BankStatementImport(DataImport):
|
||||
|
||||
self.template_warnings = ""
|
||||
|
||||
if self.import_file and not self.import_file.lower().endswith(".txt"):
|
||||
self.validate_import_file()
|
||||
self.validate_google_sheets_url()
|
||||
self.validate_import_file()
|
||||
self.validate_google_sheets_url()
|
||||
|
||||
def start_import(self):
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
@@ -86,7 +79,7 @@ class BankStatementImport(DataImport):
|
||||
from frappe.utils.background_jobs import is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
run_now = frappe.in_test or frappe.conf.developer_mode
|
||||
run_now = frappe.flags.in_test or frappe.conf.developer_mode
|
||||
if is_scheduler_inactive() and not run_now:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
@@ -111,102 +104,6 @@ class BankStatementImport(DataImport):
|
||||
return None
|
||||
|
||||
|
||||
def preprocess_mt940_content(content: str) -> str:
|
||||
"""Preprocess MT940 content to fix statement number format issues.
|
||||
|
||||
The MT940 standard expects statement numbers to be maximum 5 digits,
|
||||
but some banks provide longer statement numbers that cause parsing errors.
|
||||
This function truncates statement numbers longer than 5 digits to the last 5 digits.
|
||||
"""
|
||||
# Fast-path: bail if no :28C: tag exists
|
||||
if ":28C:" not in content:
|
||||
return content
|
||||
|
||||
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
|
||||
pattern = re.compile(r"(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$")
|
||||
|
||||
def replace_statement_number(match):
|
||||
prefix = match.group(1) # ':28C:'
|
||||
statement_num = match.group(2) # The statement number
|
||||
sequence_part = match.group(3) or "" # The sequence part like '/1'
|
||||
trailing_space = match.group(4) or "" # Preserve trailing whitespace
|
||||
|
||||
# If statement number is longer than 5 digits, truncate to last 5 digits
|
||||
if len(statement_num) > 5:
|
||||
statement_num = statement_num[-5:]
|
||||
|
||||
return prefix + statement_num + sequence_part + trailing_space
|
||||
|
||||
# Apply the replacement
|
||||
processed_content = pattern.sub(replace_statement_number, content)
|
||||
return processed_content
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import, mt940_file_path):
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
|
||||
_file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
is_mt940 = is_mt940_format(content)
|
||||
if not is_mt940:
|
||||
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
|
||||
|
||||
if is_mt940 and not doc.import_mt940_fromat:
|
||||
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
|
||||
|
||||
try:
|
||||
# Preprocess MT940 content to fix statement number format issues
|
||||
processed_content = preprocess_mt940_content(content)
|
||||
transactions = mt940.parse(processed_content)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
|
||||
|
||||
if not transactions:
|
||||
frappe.throw(_("Parsed file is not in valid MT940 format or contains no transactions."))
|
||||
|
||||
# Use in-memory file buffer instead of writing to temp file
|
||||
csv_buffer = io.StringIO()
|
||||
writer = csv.writer(csv_buffer)
|
||||
|
||||
headers = ["Date", "Deposit", "Withdrawal", "Description", "Reference Number", "Bank Account", "Currency"]
|
||||
writer.writerow(headers)
|
||||
|
||||
for txn in transactions:
|
||||
txn_date = getattr(txn, "date", None)
|
||||
raw_date = txn.data.get("date", "")
|
||||
|
||||
if txn_date:
|
||||
date_str = txn_date.strftime("%Y-%m-%d")
|
||||
elif isinstance(raw_date, date | datetime):
|
||||
date_str = raw_date.strftime("%Y-%m-%d")
|
||||
else:
|
||||
date_str = str(raw_date)
|
||||
|
||||
raw_amount = str(txn.data.get("amount", ""))
|
||||
parts = raw_amount.strip().split()
|
||||
amount_value = float(parts[0]) if parts else 0.0
|
||||
|
||||
deposit = amount_value if amount_value > 0 else ""
|
||||
withdrawal = abs(amount_value) if amount_value < 0 else ""
|
||||
description = txn.data.get("extra_details") or ""
|
||||
reference = txn.data.get("transaction_reference") or ""
|
||||
currency = txn.data.get("currency", "")
|
||||
|
||||
writer.writerow([date_str, deposit, withdrawal, description, reference, doc.bank_account, currency])
|
||||
|
||||
# Prepare in-memory CSV for upload
|
||||
csv_content = csv_buffer.getvalue().encode("utf-8")
|
||||
csv_buffer.close()
|
||||
|
||||
filename = f"{frappe.utils.now_datetime().strftime('%Y%m%d%H%M%S')}_converted_mt940.csv"
|
||||
|
||||
# Save to File Manager
|
||||
saved_file = save_file(filename, csv_content, doc.doctype, doc.name, is_private=True, df="import_file")
|
||||
|
||||
return saved_file.file_url
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
|
||||
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
|
||||
@@ -231,12 +128,6 @@ def download_import_log(data_import_name):
|
||||
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
|
||||
|
||||
|
||||
def is_mt940_format(content: str) -> bool:
|
||||
"""Check if the content has key MT940 tags"""
|
||||
required_tags = [":20:", ":25:", ":28C:", ":61:"]
|
||||
return all(tag in content for tag in required_tags)
|
||||
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
@@ -283,7 +174,6 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
|
||||
|
||||
def update_mapping_db(bank, template_options):
|
||||
"""Update bank transaction mapping database with template options."""
|
||||
bank = frappe.get_doc("Bank", bank)
|
||||
for d in bank.bank_transaction_mapping:
|
||||
d.delete()
|
||||
@@ -295,7 +185,6 @@ def update_mapping_db(bank, template_options):
|
||||
|
||||
|
||||
def add_bank_account(data, bank_account):
|
||||
"""Add bank account information to data rows."""
|
||||
bank_account_loc = None
|
||||
if "Bank Account" not in data[0]:
|
||||
data[0].append("Bank Account")
|
||||
@@ -312,7 +201,6 @@ def add_bank_account(data, bank_account):
|
||||
|
||||
|
||||
def write_files(import_file, data):
|
||||
"""Write processed data to CSV or Excel files."""
|
||||
full_file_path = import_file.file_doc.get_full_path()
|
||||
parts = import_file.file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
@@ -322,12 +210,11 @@ def write_files(import_file, data):
|
||||
with open(full_file_path, "w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(data)
|
||||
elif extension in ("xlsx", "xls"):
|
||||
elif extension == "xlsx" or "xls":
|
||||
write_xlsx(data, "trans", file_path=full_file_path)
|
||||
|
||||
|
||||
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
"""Write data to Excel file with formatting."""
|
||||
# from xlsx utils with changes
|
||||
column_widths = column_widths or []
|
||||
if wb is None:
|
||||
@@ -392,7 +279,7 @@ def get_import_status(docname):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_import_logs(docname: str):
|
||||
frappe.has_permission("Bank Statement Import", throw=True)
|
||||
frappe.has_permission("Bank Statement Import")
|
||||
|
||||
return frappe.get_all(
|
||||
"Data Import Log",
|
||||
|
||||
@@ -1,209 +1,10 @@
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
|
||||
is_mt940_format,
|
||||
preprocess_mt940_content,
|
||||
)
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankStatementImport(unittest.TestCase):
|
||||
"""Unit tests for Bank Statement Import functions"""
|
||||
|
||||
def test_preprocess_mt940_content_with_long_statement_number(self):
|
||||
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
|
||||
# Test case with 6-digit statement number (167619 -> 67619)
|
||||
mt940_content = ":28C:167619/1"
|
||||
expected_content = ":28C:67619/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_with_normal_statement_number(self):
|
||||
"""Test that statement numbers with 5 or fewer digits are unchanged"""
|
||||
# Test case with 5-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
# Test case with 4-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:1234/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_without_sequence_number(self):
|
||||
"""Test statement number truncation without sequence number"""
|
||||
# Test case with long statement number but no sequence (no /1)
|
||||
mt940_content = ":28C:987654321"
|
||||
expected_content = ":28C:54321"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_multiple_occurrences(self):
|
||||
"""Test multiple statement numbers in the same content"""
|
||||
mt940_content = """:28C:167619/1
|
||||
:28C:987654/2"""
|
||||
expected_content = """:28C:67619/1
|
||||
:28C:87654/2"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_edge_cases(self):
|
||||
"""Test edge cases like empty content and content without :28C: tags"""
|
||||
# Test empty content
|
||||
self.assertEqual(preprocess_mt940_content(""), "")
|
||||
|
||||
# Test content without :28C: tags
|
||||
content_without_28c = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
result = preprocess_mt940_content(content_without_28c)
|
||||
self.assertEqual(result, content_without_28c) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_with_full_mt940_document(self):
|
||||
"""Test preprocessing with complete MT940 document"""
|
||||
mt940_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
expected_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:67619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_is_mt940_format_detection(self):
|
||||
"""Test MT940 format detection function"""
|
||||
# Valid MT940 content with all required tags
|
||||
valid_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
|
||||
self.assertTrue(is_mt940_format(valid_mt940))
|
||||
|
||||
# Invalid MT940 content (CSV format)
|
||||
invalid_mt940 = """Date,Description,Amount
|
||||
2023-01-01,Test Transaction,100.00
|
||||
2023-01-02,Another Transaction,-50.00"""
|
||||
self.assertFalse(is_mt940_format(invalid_mt940))
|
||||
|
||||
# Partially valid MT940 (missing some required tags)
|
||||
partial_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
self.assertFalse(is_mt940_format(partial_mt940))
|
||||
|
||||
# Empty content
|
||||
self.assertFalse(is_mt940_format(""))
|
||||
|
||||
def test_preprocess_mt940_content_boundary_conditions(self):
|
||||
"""Test boundary conditions for statement number length"""
|
||||
# Test exactly 6 digits (should be truncated)
|
||||
mt940_content = ":28C:123456/1"
|
||||
expected_content = ":28C:23456/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test exactly 5 digits (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content)
|
||||
|
||||
# Test very long statement number
|
||||
mt940_content = ":28C:123456789012345/1"
|
||||
expected_content = ":28C:12345/1" # Last 5 digits
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_real_world_case(self):
|
||||
"""Test with real-world MT940 content that was failing in production"""
|
||||
# This is based on actual MT940 content that was causing parsing errors (sanitized)
|
||||
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:167619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
# Expected result with statement number 167619 truncated to 67619
|
||||
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:67619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Verify that the problematic statement number was actually changed
|
||||
self.assertIn(":28C:67619/1", result)
|
||||
self.assertNotIn(":28C:167619/1", result)
|
||||
|
||||
# Verify that other content remains unchanged
|
||||
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
|
||||
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
|
||||
|
||||
def test_preprocess_mt940_content_whitespace_variants(self):
|
||||
"""Test handling of whitespace and different line endings"""
|
||||
# Test with trailing spaces
|
||||
mt940_content = ":28C:167619/1 \n"
|
||||
expected_content = ":28C:67619/1 \n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with Windows line endings (CRLF)
|
||||
mt940_content = ":28C:167619/1\r\n"
|
||||
expected_content = ":28C:67619/1\r\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with leading spaces (should not match as it's not line start)
|
||||
mt940_content = " :28C:167619/1\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should remain unchanged
|
||||
class TestBankStatementImport(IntegrationTestCase):
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
from rapidfuzz.utils import default_process
|
||||
|
||||
|
||||
class AutoMatchParty:
|
||||
@@ -26,7 +25,7 @@ class AutoMatchParty:
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
fuzzy_matching_enabled = frappe.get_single_value("Accounts Settings", "enable_fuzzy_matching")
|
||||
fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching")
|
||||
if not result and fuzzy_matching_enabled:
|
||||
result = AutoMatchbyPartyNameDescription(
|
||||
bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit
|
||||
@@ -133,7 +132,6 @@ class AutoMatchbyPartyNameDescription:
|
||||
query=self.get(field),
|
||||
choices={row.get("name"): row.get("party_name") for row in names},
|
||||
scorer=fuzz.token_set_ratio,
|
||||
processor=default_process,
|
||||
)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"default": "ACC-BTN-.YYYY.-",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "ACC-BTN-.YYYY.-",
|
||||
@@ -116,14 +117,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,
|
||||
@@ -222,8 +224,7 @@
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)",
|
||||
"options": "IBAN"
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
@@ -235,10 +236,9 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-23 17:32:58.514807",
|
||||
"modified": "2023-11-18 18:32:47.203694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
@@ -287,10 +287,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "date",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "bank_account",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class BankTransaction(Document):
|
||||
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
|
||||
@@ -121,7 +121,7 @@ class BankTransaction(Document):
|
||||
self.allocate_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
if frappe.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
self.auto_set_party()
|
||||
|
||||
def before_update_after_submit(self):
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
|
||||
IBAN_1 = "DE02000000003716541159"
|
||||
IBAN_2 = "DE02500105170137075030"
|
||||
|
||||
class UnitTestBankTransaction(UnitTestCase):
|
||||
"""
|
||||
Unit tests for BankTransaction.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestAutoMatchParty(IntegrationTestCase):
|
||||
@@ -25,24 +31,24 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
||||
|
||||
def test_match_by_account_number(self):
|
||||
create_supplier_for_match(account_no=IBAN_1[11:])
|
||||
create_supplier_for_match(account_no="000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_iban(self):
|
||||
create_supplier_for_match(iban=IBAN_1)
|
||||
create_supplier_for_match(iban="DE02000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="c5455a224602afaa51592a9d9250600d",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
@@ -54,7 +60,7 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
withdrawal=1200,
|
||||
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
||||
party_name="Ella Jackson",
|
||||
iban=IBAN_2,
|
||||
iban="DE04000000003716545346",
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Jackson Ella W.")
|
||||
|
||||
@@ -6,15 +6,12 @@ import json
|
||||
import frappe
|
||||
from frappe import utils
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
get_linked_payments,
|
||||
reconcile_vouchers,
|
||||
)
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -24,6 +21,15 @@ from erpnext.tests.utils import if_lending_app_installed
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = ["Item", "Cost Center"]
|
||||
|
||||
|
||||
class UnitTestBankTransaction(UnitTestCase):
|
||||
"""
|
||||
Unit tests for BankTransaction.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestBankTransaction(IntegrationTestCase):
|
||||
def setUp(self):
|
||||
make_pos_profile()
|
||||
@@ -424,13 +430,15 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Wire Transfer"})
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||
|
||||
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", gl_account)
|
||||
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||
mode_of_payment.save()
|
||||
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||
si.is_pos = 1
|
||||
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 109080})
|
||||
si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080})
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
class UnitTestBisectAccountingStatements(UnitTestCase):
|
||||
"""
|
||||
Unit tests for BisectAccountingStatements.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestBisectAccountingStatements(IntegrationTestCase):
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
class UnitTestBisectNodes(UnitTestCase):
|
||||
"""
|
||||
Unit tests for BisectNodes.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestBisectNodes(IntegrationTestCase):
|
||||
|
||||
@@ -23,11 +23,6 @@ frappe.ui.form.on("Budget", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frappe.db.get_single_value("Accounts Settings", "use_legacy_budget_controller").then((value) => {
|
||||
if (value) {
|
||||
frm.get_field("control_action_for_cumulative_expense_section").hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"budget_against",
|
||||
"company",
|
||||
"cost_center",
|
||||
"naming_series",
|
||||
"project",
|
||||
"fiscal_year",
|
||||
"column_break_3",
|
||||
@@ -28,10 +28,6 @@
|
||||
"applicable_on_booking_actual_expenses",
|
||||
"action_if_annual_budget_exceeded",
|
||||
"action_if_accumulated_monthly_budget_exceeded",
|
||||
"control_action_for_cumulative_expense_section",
|
||||
"applicable_on_cumulative_expense",
|
||||
"action_if_annual_exceeded_on_cumulative_expense",
|
||||
"action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"section_break_21",
|
||||
"accounts"
|
||||
],
|
||||
@@ -199,46 +195,19 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Series",
|
||||
"no_copy": 1,
|
||||
"options": "BUDGET-.YYYY.-",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"read_only": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "control_action_for_cumulative_expense_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Control Action for Cumulative Expense"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "(Purchase Order + Material Request + Actual Expense)",
|
||||
"fieldname": "applicable_on_cumulative_expense",
|
||||
"fieldtype": "Check",
|
||||
"label": "Applicable on Cumulative Expense"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.applicable_on_cumulative_expense == 1",
|
||||
"fieldname": "action_if_annual_exceeded_on_cumulative_expense",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Anual Budget Exceeded on Cumulative Expense",
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.applicable_on_cumulative_expense == 1",
|
||||
"fieldname": "action_if_accumulated_monthly_exceeded_on_cumulative_expense",
|
||||
"fieldtype": "Select",
|
||||
"label": "Action if Accumulative Monthly Budget Exceeded on Cumulative Expense",
|
||||
"options": "\nStop\nWarn\nIgnore"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-16 15:57:13.114981",
|
||||
"modified": "2024-03-27 13:06:42.675933",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Budget",
|
||||
@@ -262,9 +231,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -36,14 +36,11 @@ class Budget(Document):
|
||||
action_if_accumulated_monthly_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_accumulated_monthly_exceeded_on_cumulative_expense: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_budget_exceeded: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_budget_exceeded_on_mr: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_budget_exceeded_on_po: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
action_if_annual_exceeded_on_cumulative_expense: DF.Literal["", "Stop", "Warn", "Ignore"]
|
||||
amended_from: DF.Link | None
|
||||
applicable_on_booking_actual_expenses: DF.Check
|
||||
applicable_on_cumulative_expense: DF.Check
|
||||
applicable_on_material_request: DF.Check
|
||||
applicable_on_purchase_order: DF.Check
|
||||
budget_against: DF.Literal["", "Cost Center", "Project"]
|
||||
@@ -51,7 +48,7 @@ class Budget(Document):
|
||||
cost_center: DF.Link | None
|
||||
fiscal_year: DF.Link
|
||||
monthly_distribution: DF.Link | None
|
||||
naming_series: DF.Literal["BUDGET-.YYYY.-"]
|
||||
naming_series: DF.Data | None
|
||||
project: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -139,21 +136,22 @@ class Budget(Document):
|
||||
):
|
||||
self.applicable_on_booking_actual_expenses = 1
|
||||
|
||||
def before_naming(self):
|
||||
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
|
||||
|
||||
|
||||
def validate_expense_against_budget(args, expense_amount=0):
|
||||
args = frappe._dict(args)
|
||||
if not frappe.db.count("Budget", cache=True):
|
||||
if not frappe.get_all("Budget", limit=1):
|
||||
return
|
||||
|
||||
if not args.fiscal_year:
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
|
||||
if args.get("company"):
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
|
||||
if not frappe.db.get_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}):
|
||||
if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec
|
||||
return
|
||||
|
||||
if not args.account:
|
||||
@@ -304,7 +302,7 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
|
||||
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
|
||||
msg = "<hr>Total Expenses booked through - <ul>"
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
@@ -318,7 +316,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"General Ledger",
|
||||
label=_("Actual Expenses"),
|
||||
label="Actual Expenses",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
||||
@@ -336,7 +334,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Material Request",
|
||||
label=_("Material Requests"),
|
||||
label="Material Requests",
|
||||
report_type="Report Builder",
|
||||
doctype="Material Request",
|
||||
filters=common_filters.copy().update(
|
||||
@@ -359,7 +357,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Purchase Order",
|
||||
label=_("Unbilled Orders"),
|
||||
label="Unbilled Orders",
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters=common_filters.copy().update(
|
||||
@@ -512,7 +510,7 @@ def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_ye
|
||||
accumulated_percentage = 0.0
|
||||
|
||||
while dt <= getdate(posting_date):
|
||||
if monthly_distribution and distribution:
|
||||
if monthly_distribution:
|
||||
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
|
||||
else:
|
||||
accumulated_percentage += 100.0 / 12
|
||||
|
||||
@@ -3,29 +3,18 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
)
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = ["Monthly Distribution"]
|
||||
|
||||
|
||||
class TestBudget(ERPNextTestSuite):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.make_monthly_distribution()
|
||||
cls.make_projects()
|
||||
|
||||
def setUp(self):
|
||||
frappe.db.set_single_value("Accounts Settings", "use_legacy_budget_controller", False)
|
||||
|
||||
class TestBudget(IntegrationTestCase):
|
||||
def test_monthly_budget_crossed_ignore(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
@@ -54,13 +43,10 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
@@ -77,13 +63,10 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
@@ -113,10 +96,6 @@ class TestBudget(ERPNextTestSuite):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -130,7 +109,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": accumulated_limit + 1,
|
||||
"rate": 100000,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
@@ -144,7 +123,6 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
budget.load_from_db()
|
||||
budget.cancel()
|
||||
mr.cancel()
|
||||
|
||||
def test_monthly_budget_crossed_for_po(self):
|
||||
budget = make_budget(
|
||||
@@ -157,12 +135,7 @@ class TestBudget(ERPNextTestSuite):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
po = create_purchase_order(
|
||||
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
|
||||
)
|
||||
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
|
||||
|
||||
po.set_missing_values()
|
||||
|
||||
@@ -180,13 +153,11 @@ class TestBudget(ERPNextTestSuite):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
"_Test Cost Center - _TC",
|
||||
project=project,
|
||||
posting_date=nowdate(),
|
||||
@@ -301,13 +272,10 @@ class TestBudget(ERPNextTestSuite):
|
||||
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
"_Test Cost Center 2 - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
@@ -334,13 +302,10 @@ class TestBudget(ERPNextTestSuite):
|
||||
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit + 1,
|
||||
40000,
|
||||
cost_center,
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
@@ -384,44 +349,6 @@ class TestBudget(ERPNextTestSuite):
|
||||
|
||||
self.assertRaises(BudgetError, jv.submit)
|
||||
|
||||
def test_action_for_cumulative_limit(self):
|
||||
set_total_expense_zero(nowdate(), "cost_center")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", applicable_on_cumulative_expense=True)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
accumulated_limit - 1,
|
||||
"_Test Cost Center - _TC",
|
||||
posting_date=nowdate(),
|
||||
)
|
||||
jv.submit()
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_exceeded_on_cumulative_expense", "Stop"
|
||||
)
|
||||
po = create_purchase_order(
|
||||
transaction_date=nowdate(), qty=1, rate=accumulated_limit + 1, do_not_submit=True
|
||||
)
|
||||
po.set_missing_values()
|
||||
|
||||
self.assertRaises(BudgetError, po.submit)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_exceeded_on_cumulative_expense", "Ignore"
|
||||
)
|
||||
po.submit()
|
||||
|
||||
budget.load_from_db()
|
||||
budget.cancel()
|
||||
po.cancel()
|
||||
jv.cancel()
|
||||
|
||||
|
||||
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
|
||||
if budget_against_field == "project":
|
||||
@@ -496,7 +423,6 @@ def make_budget(**args):
|
||||
|
||||
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
|
||||
monthly_distribution.fiscal_year = fiscal_year
|
||||
monthly_distribution.save()
|
||||
|
||||
budget.fiscal_year = fiscal_year
|
||||
budget.monthly_distribution = "_Test Distribution"
|
||||
@@ -521,15 +447,6 @@ def make_budget(**args):
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
|
||||
if args.applicable_on_cumulative_expense:
|
||||
budget.applicable_on_cumulative_expense = 1
|
||||
budget.action_if_annual_exceeded_on_cumulative_expense = (
|
||||
args.action_if_annual_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
budget.action_if_accumulated_monthly_exceeded_on_cumulative_expense = (
|
||||
args.action_if_accumulated_monthly_exceeded_on_cumulative_expense or "Warn"
|
||||
)
|
||||
|
||||
budget.insert()
|
||||
budget.submit()
|
||||
|
||||
|
||||
@@ -462,8 +462,9 @@ def unset_existing_data(company):
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
frappe.db.sql(
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
|
||||
@@ -154,7 +154,3 @@ class CostCenterAllocation(Document):
|
||||
).format(d.cost_center),
|
||||
InvalidChildCostCenter,
|
||||
)
|
||||
|
||||
def clear_cache(self):
|
||||
frappe.clear_cache(doctype="Cost Center")
|
||||
return super().clear_cache()
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
@@ -191,31 +190,6 @@ class TestCostCenterAllocation(IntegrationTestCase):
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
@IntegrationTestCase.change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
||||
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
|
||||
.where(gl_entry.voucher_type == "Sales Invoice")
|
||||
.where(gl_entry.voucher_no == si.name)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
|
||||
|
||||
si.cancel()
|
||||
cca.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -36,7 +36,7 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
def validate(self):
|
||||
self.set_parameters_and_result()
|
||||
if frappe.in_test or frappe.flags.in_install or frappe.flags.in_setup_wizard:
|
||||
if frappe.flags.in_test or frappe.flags.in_install or frappe.flags.in_setup_wizard:
|
||||
return
|
||||
response, value = self.validate_parameters()
|
||||
self.validate_result(response, value)
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
-> Resolves dunning automatically
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
@@ -164,66 +163,43 @@ class Dunning(AccountsController):
|
||||
]
|
||||
|
||||
|
||||
def update_linked_dunnings(doc, previous_outstanding_amount):
|
||||
if (
|
||||
doc.doctype != "Sales Invoice"
|
||||
or doc.is_return
|
||||
or previous_outstanding_amount == doc.outstanding_amount
|
||||
):
|
||||
return
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
Check if all payments have been made and resolve dunning, if yes. Called
|
||||
when a Payment Entry is submitted.
|
||||
"""
|
||||
for reference in doc.references:
|
||||
# Consider partial and full payments:
|
||||
# Submitting full payment: outstanding_amount will be 0
|
||||
# Submitting 1st partial payment: outstanding_amount will be the pending installment
|
||||
# Cancelling full payment: outstanding_amount will revert to total amount
|
||||
# Cancelling last partial payment: outstanding_amount will revert to pending amount
|
||||
submit_condition = reference.outstanding_amount < reference.total_amount
|
||||
cancel_condition = reference.outstanding_amount <= reference.total_amount
|
||||
|
||||
to_resolve = doc.outstanding_amount < previous_outstanding_amount
|
||||
state = "Unresolved" if to_resolve else "Resolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(doc.name, state)
|
||||
if not dunnings:
|
||||
return
|
||||
if reference.reference_doctype == "Sales Invoice" and (
|
||||
submit_condition if doc.docstatus == 1 else cancel_condition
|
||||
):
|
||||
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
|
||||
|
||||
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
|
||||
invoices = set()
|
||||
payment_schedule_ids = set()
|
||||
for dunning in dunnings:
|
||||
resolve = True
|
||||
dunning = frappe.get_doc("Dunning", dunning.get("name"))
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
outstanding_inv = frappe.get_value(
|
||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
outstanding_ps = frappe.get_value(
|
||||
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
|
||||
)
|
||||
resolve = resolve and (False if (outstanding_ps > 0 and outstanding_inv > 0) else True)
|
||||
|
||||
for dunning in dunnings:
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
invoices.add(overdue_payment.sales_invoice)
|
||||
if overdue_payment.payment_schedule:
|
||||
payment_schedule_ids.add(overdue_payment.payment_schedule)
|
||||
new_status = "Resolved" if resolve else "Unresolved"
|
||||
|
||||
invoice_outstanding_amounts = dict(
|
||||
frappe.get_all(
|
||||
"Sales Invoice",
|
||||
filters={"name": ["in", list(invoices)]},
|
||||
fields=["name", "outstanding_amount"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
ps_outstanding_amounts = (
|
||||
dict(
|
||||
frappe.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"name": ["in", list(payment_schedule_ids)]},
|
||||
fields=["name", "outstanding"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
if payment_schedule_ids
|
||||
else {}
|
||||
)
|
||||
|
||||
for dunning in dunnings:
|
||||
has_outstanding = False
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
|
||||
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
|
||||
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
|
||||
if has_outstanding:
|
||||
break
|
||||
|
||||
new_status = "Resolved" if not has_outstanding else "Unresolved"
|
||||
|
||||
if dunning.status != new_status:
|
||||
dunning.status = new_status
|
||||
dunning.save()
|
||||
if dunning.status != new_status:
|
||||
dunning.status = new_status
|
||||
dunning.save()
|
||||
|
||||
|
||||
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.model import mapper
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
from frappe.utils import add_days, nowdate, today
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
@@ -22,6 +19,15 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = ["Company", "Cost Center"]
|
||||
|
||||
|
||||
class UnitTestDunning(UnitTestCase):
|
||||
"""
|
||||
Unit tests for Dunning.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestDunning(IntegrationTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -71,36 +77,6 @@ class TestDunning(IntegrationTestCase):
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
def test_fetch_overdue_payments(self):
|
||||
"""
|
||||
Create SI with overdue payment. Check if overdue payment is fetched in Dunning.
|
||||
"""
|
||||
si1 = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -1 * 6),
|
||||
qty=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
si2 = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -1 * 6),
|
||||
qty=1,
|
||||
rate=300,
|
||||
)
|
||||
|
||||
dunning = create_dunning_from_sales_invoice(si1.name)
|
||||
dunning.overdue_payments = []
|
||||
|
||||
method = "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning"
|
||||
updated_dunning = mapper.map_docs(method, json.dumps([si1.name, si2.name]), dunning)
|
||||
|
||||
self.assertEqual(len(updated_dunning.overdue_payments), 2)
|
||||
|
||||
self.assertEqual(updated_dunning.overdue_payments[0].sales_invoice, si1.name)
|
||||
self.assertEqual(updated_dunning.overdue_payments[0].outstanding, si1.outstanding_amount)
|
||||
|
||||
self.assertEqual(updated_dunning.overdue_payments[1].sales_invoice, si2.name)
|
||||
self.assertEqual(updated_dunning.overdue_payments[1].outstanding, si2.outstanding_amount)
|
||||
|
||||
def test_dunning_and_payment_against_partially_due_invoice(self):
|
||||
"""
|
||||
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
|
||||
@@ -139,64 +115,6 @@ class TestDunning(IntegrationTestCase):
|
||||
self.assertEqual(sales_invoice.status, "Overdue")
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_dunning_resolution_from_credit_note(self):
|
||||
"""
|
||||
Test that dunning is resolved when a credit note is issued against the original invoice.
|
||||
"""
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||
)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
dunning.submit()
|
||||
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
credit_note = frappe.copy_doc(sales_invoice)
|
||||
credit_note.is_return = 1
|
||||
credit_note.return_against = sales_invoice.name
|
||||
credit_note.update_outstanding_for_self = 0
|
||||
|
||||
for item in credit_note.items:
|
||||
item.qty = -item.qty
|
||||
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
credit_note.cancel()
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_dunning_not_affected_by_standalone_credit_note(self):
|
||||
"""
|
||||
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
|
||||
"""
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||
)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
dunning.submit()
|
||||
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
credit_note = frappe.copy_doc(sales_invoice)
|
||||
credit_note.is_return = 1
|
||||
credit_note.return_against = sales_invoice.name
|
||||
credit_note.update_outstanding_for_self = 1
|
||||
|
||||
for item in credit_note.items:
|
||||
item.qty = -item.qty
|
||||
|
||||
credit_note.save()
|
||||
|
||||
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
|
||||
credit_note.submit()
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
|
||||
def create_dunning(overdue_days, dunning_type_name=None):
|
||||
posting_date = add_days(today(), -1 * overdue_days)
|
||||
|
||||
@@ -134,8 +134,7 @@ class ExchangeRateRevaluation(Document):
|
||||
accounts = self.get_accounts_data()
|
||||
if accounts:
|
||||
for acc in accounts:
|
||||
if acc.get("gain_loss"):
|
||||
self.append("accounts", acc)
|
||||
self.append("accounts", acc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_accounts_data(self):
|
||||
|
||||
@@ -99,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"):
|
||||
@@ -123,7 +123,7 @@ def check_duplicate_fiscal_year(doc):
|
||||
)
|
||||
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.in_test
|
||||
not frappe.flags.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
|
||||
@@ -25,27 +25,6 @@ class TestFiscalYear(IntegrationTestCase):
|
||||
|
||||
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 = [
|
||||
|
||||
@@ -29,17 +29,14 @@
|
||||
"against_voucher",
|
||||
"voucher_detail_no",
|
||||
"transaction_exchange_rate",
|
||||
"reporting_currency_exchange_rate",
|
||||
"amounts_section",
|
||||
"debit_in_account_currency",
|
||||
"debit",
|
||||
"debit_in_transaction_currency",
|
||||
"debit_in_reporting_currency",
|
||||
"column_break_bm1w",
|
||||
"credit_in_account_currency",
|
||||
"credit",
|
||||
"credit_in_transaction_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_lmnm",
|
||||
@@ -356,31 +353,13 @@
|
||||
{
|
||||
"fieldname": "column_break_8abq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-22 12:57:17.750252",
|
||||
"modified": "2025-03-21 15:29:11.221890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
@@ -411,9 +390,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.naming import set_name_from_naming_options
|
||||
from frappe.utils import create_batch, flt, fmt_money, now
|
||||
from frappe.utils import flt, fmt_money
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -18,9 +18,8 @@ from erpnext.accounts.party import (
|
||||
validate_party_frozen_disabled,
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
@@ -43,11 +42,9 @@ class GLEntry(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
credit_in_transaction_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
debit_in_transaction_currency: DF.Currency
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
@@ -60,7 +57,6 @@ class GLEntry(Document):
|
||||
posting_date: DF.Date | None
|
||||
project: DF.Link | None
|
||||
remarks: DF.Text | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
to_rename: DF.Check
|
||||
transaction_currency: DF.Link | None
|
||||
transaction_date: DF.Date | None
|
||||
@@ -92,15 +88,13 @@ class GLEntry(Document):
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
|
||||
self.set_amount_in_reporting_currency()
|
||||
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.company, self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Journal Entry"
|
||||
@@ -137,20 +131,18 @@ 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
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
|
||||
# Zero value transaction is not allowed
|
||||
if not (
|
||||
@@ -232,23 +224,26 @@ class GLEntry(Document):
|
||||
def validate_account_details(self, adv_adj):
|
||||
"""Account must be ledger, active and not freezed"""
|
||||
|
||||
account = frappe.get_cached_value(
|
||||
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
|
||||
)
|
||||
ret = frappe.db.sql(
|
||||
"""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""",
|
||||
self.account,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
if account.is_group == 1:
|
||||
if ret.is_group == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if account.docstatus == 2:
|
||||
if ret.docstatus == 2:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if account.company != self.company:
|
||||
if ret.company != self.company:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, self.company
|
||||
@@ -256,7 +251,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"])
|
||||
@@ -276,7 +271,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.company, self.party_type, self.party)
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
@@ -300,25 +295,6 @@ class GLEntry(Document):
|
||||
if self.party_type and self.party:
|
||||
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
|
||||
|
||||
def set_amount_in_reporting_currency(self):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", self.company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
transaction_date = self.transaction_date or self.posting_date
|
||||
self.reporting_currency_exchange_rate = get_exchange_rate(
|
||||
default_currency, reporting_currency, transaction_date
|
||||
)
|
||||
if not self.reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, transaction_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
self.debit_in_reporting_currency = flt(self.debit * self.reporting_currency_exchange_rate)
|
||||
self.credit_in_reporting_currency = flt(self.credit * self.reporting_currency_exchange_rate)
|
||||
|
||||
def validate_and_set_fiscal_year(self):
|
||||
if not self.fiscal_year:
|
||||
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
|
||||
@@ -335,7 +311,7 @@ def validate_balance_type(account, adv_adj=False):
|
||||
if balance_must_be:
|
||||
balance = frappe.db.sql(
|
||||
"""select sum(debit) - sum(credit)
|
||||
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
|
||||
from `tabGL Entry` where account = %s""",
|
||||
account,
|
||||
)[0][0]
|
||||
|
||||
@@ -409,7 +385,7 @@ def update_outstanding_amt(
|
||||
)
|
||||
)
|
||||
|
||||
if against_voucher_type in OUTSTANDING_DOCTYPES:
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
|
||||
|
||||
# Didn't use db_set for optimization purpose
|
||||
@@ -419,16 +395,16 @@ def update_outstanding_amt(
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def validate_frozen_account(company, account, adv_adj=None):
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if not role_allowed_for_frozen_entries:
|
||||
if not frozen_accounts_modifier:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
elif role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
elif frozen_accounts_modifier not in frappe.get_roles():
|
||||
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
|
||||
|
||||
|
||||
@@ -475,20 +451,12 @@ def rename_gle_sle_docs():
|
||||
def rename_temporarily_named_docs(doctype):
|
||||
"""Rename temporarily named docs using autoname options"""
|
||||
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
|
||||
autoname = frappe.get_meta(doctype).autoname
|
||||
|
||||
for batch in create_batch(docs_to_rename, 100):
|
||||
for doc in batch:
|
||||
oldname = doc.name
|
||||
set_name_from_naming_options(autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
|
||||
(newname, now(), oldname),
|
||||
)
|
||||
|
||||
for hook_type in ("on_gle_rename", "on_sle_rename"):
|
||||
for hook in frappe.get_hooks(hook_type):
|
||||
frappe.call(hook, newname=newname, oldname=oldname)
|
||||
|
||||
frappe.db.commit()
|
||||
for doc in docs_to_rename:
|
||||
oldname = doc.name
|
||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0 where name = %s",
|
||||
(newname, oldname),
|
||||
auto_commit=True,
|
||||
)
|
||||
|
||||
@@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", {
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "Categorize by Voucher (Consolidated)",
|
||||
group_by: "Group by Voucher (Consolidated)",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
|
||||
@@ -39,16 +39,7 @@ class ItemTaxTemplate(Document):
|
||||
check_list = []
|
||||
for d in self.get("taxes"):
|
||||
if d.tax_type:
|
||||
account_type, account_company = frappe.get_cached_value(
|
||||
"Account", d.tax_type, ["account_type", "company"]
|
||||
)
|
||||
|
||||
if account_company != self.company:
|
||||
frappe.throw(
|
||||
_("Item Tax Row {0}: Account must belong to Company - {1}").format(
|
||||
d.idx, frappe.bold(self.company)
|
||||
)
|
||||
)
|
||||
account_type = frappe.get_cached_value("Account", d.tax_type, "account_type")
|
||||
|
||||
if account_type not in [
|
||||
"Tax",
|
||||
|
||||
@@ -20,39 +20,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
"Unreconcile Payment Entries",
|
||||
"Bank Transaction",
|
||||
];
|
||||
|
||||
frm.trigger("set_queries");
|
||||
},
|
||||
|
||||
set_queries(frm) {
|
||||
frm.set_query("periodic_entry_difference_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("stock_asset_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
account_type: "Stock",
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
get_balance_for_periodic_accounting(frm) {
|
||||
frm.call({
|
||||
method: "get_balance_for_periodic_accounting",
|
||||
doc: frm.doc,
|
||||
callback: function (r) {
|
||||
refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
@@ -68,7 +35,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
finance_book: frm.doc.finance_book,
|
||||
categorize_by: "",
|
||||
group_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
@@ -196,7 +163,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
voucher_type: function (frm) {
|
||||
|
||||
@@ -13,21 +13,15 @@
|
||||
"title",
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
"finance_book",
|
||||
"process_deferred_accounting",
|
||||
"reversal_of",
|
||||
"tax_withholding_category",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
"company",
|
||||
"posting_date",
|
||||
"finance_book",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"section_break_tcvw",
|
||||
"for_all_stock_asset_accounts",
|
||||
"column_break_wpau",
|
||||
"stock_asset_account",
|
||||
"periodic_entry_difference_account",
|
||||
"get_balance_for_periodic_accounting",
|
||||
"2_add_edit_gl_entries",
|
||||
"accounts",
|
||||
"section_break99",
|
||||
@@ -46,6 +40,7 @@
|
||||
"reference",
|
||||
"clearance_date",
|
||||
"remark",
|
||||
"paid_loan",
|
||||
"inter_company_journal_entry_reference",
|
||||
"column_break98",
|
||||
"bill_no",
|
||||
@@ -64,7 +59,6 @@
|
||||
"addtional_info",
|
||||
"mode_of_payment",
|
||||
"payment_order",
|
||||
"party_not_required",
|
||||
"column_break3",
|
||||
"is_opening",
|
||||
"stock_entry",
|
||||
@@ -95,7 +89,7 @@
|
||||
"label": "Entry Type",
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nAsset Disposal\nPeriodic Accounting Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
|
||||
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -310,6 +304,13 @@
|
||||
"oldfieldtype": "Small Text",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_loan",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Paid Loan",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.voucher_type== \"Inter Company Journal Entry\"",
|
||||
"fieldname": "inter_company_journal_entry_reference",
|
||||
@@ -542,50 +543,6 @@
|
||||
"label": "Is System Generated",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"",
|
||||
"fieldname": "periodic_entry_difference_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Periodic Entry Difference Account",
|
||||
"mandatory_depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.voucher_type === \"Periodic Accounting Entry\"",
|
||||
"fieldname": "section_break_tcvw",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Periodic Accounting"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "for_all_stock_asset_accounts",
|
||||
"fieldtype": "Check",
|
||||
"label": "For All Stock Asset Accounts"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.for_all_stock_asset_accounts === 0",
|
||||
"fieldname": "stock_asset_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock Asset Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_wpau",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "get_balance_for_periodic_accounting",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Balance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "party_not_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Party Not Required",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -600,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",
|
||||
@@ -645,11 +602,10 @@
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "voucher_type,posting_date, due_date, cheque_no",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
get_stock_and_account_balance,
|
||||
@@ -63,7 +62,6 @@ class JournalEntry(AccountsController):
|
||||
difference: DF.Currency
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
for_all_stock_asset_accounts: DF.Check
|
||||
from_template: DF.Link | None
|
||||
inter_company_journal_entry_reference: DF.Link | None
|
||||
is_opening: DF.Literal["No", "Yes"]
|
||||
@@ -72,16 +70,14 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
paid_loan: DF.Data | None
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
posting_date: DF.Date
|
||||
process_deferred_accounting: DF.Link | None
|
||||
remark: DF.SmallText | None
|
||||
reversal_of: DF.Link | None
|
||||
select_print_heading: DF.Link | None
|
||||
stock_asset_account: DF.Link | None
|
||||
stock_entry: DF.Link | None
|
||||
tax_withholding_category: DF.Link | None
|
||||
title: DF.Data | None
|
||||
@@ -104,8 +100,6 @@ class JournalEntry(AccountsController):
|
||||
"Write Off Entry",
|
||||
"Opening Entry",
|
||||
"Depreciation Entry",
|
||||
"Asset Disposal",
|
||||
"Periodic Accounting Entry",
|
||||
"Exchange Rate Revaluation",
|
||||
"Exchange Gain Or Loss",
|
||||
"Deferred Revenue",
|
||||
@@ -146,13 +140,14 @@ class JournalEntry(AccountsController):
|
||||
self.validate_credit_debit_note()
|
||||
self.validate_empty_accounts_table()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_depr_account_and_depr_entry_voucher_type()
|
||||
self.validate_depr_entry_voucher_type()
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_advance_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
if self.is_new() or not self.title:
|
||||
|
||||
if not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
@@ -194,81 +189,14 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.make_gl_entries()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance_for_periodic_accounting(self):
|
||||
self.validate_company_for_periodic_accounting()
|
||||
|
||||
stock_accounts = self.get_stock_accounts_for_periodic_accounting()
|
||||
self.set("accounts", [])
|
||||
for account in stock_accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
account, self.posting_date, self.company
|
||||
)
|
||||
|
||||
difference_value = flt(stock_bal - account_bal, self.precision("difference"))
|
||||
|
||||
if difference_value == 0:
|
||||
frappe.msgprint(
|
||||
_("No difference found for stock account {0}").format(frappe.bold(account)),
|
||||
alert=True,
|
||||
)
|
||||
continue
|
||||
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": account,
|
||||
"debit_in_account_currency": difference_value if difference_value > 0 else 0,
|
||||
"credit_in_account_currency": abs(difference_value) if difference_value < 0 else 0,
|
||||
},
|
||||
)
|
||||
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.periodic_entry_difference_account,
|
||||
"credit_in_account_currency": difference_value if difference_value > 0 else 0,
|
||||
"debit_in_account_currency": abs(difference_value) if difference_value < 0 else 0,
|
||||
},
|
||||
)
|
||||
|
||||
def validate_company_for_periodic_accounting(self):
|
||||
if erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Periodic Accounting Entry is not allowed for company {0} with perpetual inventory enabled"
|
||||
).format(self.company)
|
||||
)
|
||||
|
||||
if not self.periodic_entry_difference_account:
|
||||
frappe.throw(_("Please select Periodic Accounting Entry Difference Account"))
|
||||
|
||||
def get_stock_accounts_for_periodic_accounting(self):
|
||||
if self.voucher_type != "Periodic Accounting Entry":
|
||||
return []
|
||||
|
||||
if self.for_all_stock_asset_accounts:
|
||||
return frappe.get_all(
|
||||
"Account",
|
||||
filters={
|
||||
"company": self.company,
|
||||
"account_type": "Stock",
|
||||
"root_type": "Asset",
|
||||
"is_group": 0,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if not self.stock_asset_account:
|
||||
frappe.throw(_("Please select Stock Asset Account"))
|
||||
|
||||
return [self.stock_asset_account]
|
||||
self.update_booked_depreciation()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
# Flag will be set on Reconciliation
|
||||
@@ -297,49 +225,49 @@ class JournalEntry(AccountsController):
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
self.unlink_inter_company_jv()
|
||||
self.unlink_asset_adjustment_entry()
|
||||
self.update_invoice_discounting()
|
||||
self.update_booked_depreciation(1)
|
||||
|
||||
def get_title(self):
|
||||
return self.pay_to_recd_from or self.accounts[0].account
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
|
||||
"advance_payment_payable_doctypes"
|
||||
)
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
|
||||
|
||||
for voucher_type, order_list in advance_paid.items():
|
||||
for voucher_no in list(set(order_list)):
|
||||
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
|
||||
|
||||
def validate_inter_company_accounts(self):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
doc = frappe.db.get_value(
|
||||
"Journal Entry",
|
||||
self.inter_company_journal_entry_reference,
|
||||
["company", "total_debit", "total_credit"],
|
||||
as_dict=True,
|
||||
)
|
||||
doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference)
|
||||
account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency")
|
||||
if account_currency == previous_account_currency:
|
||||
credit_precision = self.precision("total_credit")
|
||||
debit_precision = self.precision("total_debit")
|
||||
if (flt(self.total_credit, credit_precision) != flt(doc.total_debit, debit_precision)) or (
|
||||
flt(self.total_debit, debit_precision) != flt(doc.total_credit, credit_precision)
|
||||
):
|
||||
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
|
||||
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
|
||||
|
||||
def validate_depr_account_and_depr_entry_voucher_type(self):
|
||||
for d in self.get("accounts"):
|
||||
if d.account_type == "Depreciation":
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
frappe.throw(
|
||||
_("Journal Entry type should be set as Depreciation Entry for asset depreciation")
|
||||
)
|
||||
|
||||
if frappe.get_cached_value("Account", d.account, "root_type") != "Expense":
|
||||
frappe.throw(_("Account {0} should be of type Expense").format(d.account))
|
||||
def validate_depr_entry_voucher_type(self):
|
||||
if (
|
||||
any(d.account_type == "Depreciation" for d in self.get("accounts"))
|
||||
and self.voucher_type != "Depreciation Entry"
|
||||
):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
if self.voucher_type == "Periodic Accounting Entry":
|
||||
# Skip validation for periodic accounting entry
|
||||
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(
|
||||
@@ -442,11 +370,7 @@ class JournalEntry(AccountsController):
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
self.update_asset_on_depreciation()
|
||||
self.update_asset_on_disposal()
|
||||
|
||||
def update_asset_on_depreciation(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
for d in self.get("accounts"):
|
||||
@@ -456,59 +380,22 @@ class JournalEntry(AccountsController):
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_cached_doc("Asset", d.reference_name)
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.update_journal_entry_link_on_depr_schedule(asset, d)
|
||||
self.update_value_after_depreciation(asset, d.debit)
|
||||
fb_idx = 1
|
||||
if self.finance_book:
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
fb_idx = fb_row.idx
|
||||
break
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation -= d.debit
|
||||
fb_row.db_update()
|
||||
else:
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
|
||||
asset.set_status()
|
||||
asset.set_total_booked_depreciations()
|
||||
|
||||
def update_value_after_depreciation(self, asset, depr_amount):
|
||||
fb_idx = 1
|
||||
if self.finance_book:
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
fb_idx = fb_row.idx
|
||||
break
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation -= depr_amount
|
||||
frappe.db.set_value(
|
||||
"Asset Finance Book", fb_row.name, "value_after_depreciation", fb_row.value_after_depreciation
|
||||
)
|
||||
|
||||
def update_journal_entry_link_on_depr_schedule(self, asset, je_row):
|
||||
depr_schedule = get_depr_schedule(asset.name, "Active", self.finance_book)
|
||||
for d in depr_schedule or []:
|
||||
if (
|
||||
d.schedule_date == self.posting_date
|
||||
and not d.journal_entry
|
||||
and d.depreciation_amount == flt(je_row.debit)
|
||||
):
|
||||
frappe.db.set_value("Depreciation Schedule", d.name, "journal_entry", self.name)
|
||||
|
||||
def update_asset_on_disposal(self):
|
||||
if self.voucher_type == "Asset Disposal":
|
||||
disposed_assets = []
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.reference_name not in disposed_assets
|
||||
):
|
||||
frappe.db.set_value(
|
||||
"Asset",
|
||||
d.reference_name,
|
||||
{
|
||||
"disposal_date": self.posting_date,
|
||||
"journal_entry_for_scrap": self.name,
|
||||
},
|
||||
)
|
||||
asset_doc = frappe.get_doc("Asset", d.reference_name)
|
||||
asset_doc.set_status()
|
||||
disposed_assets.append(d.reference_name)
|
||||
|
||||
def update_inter_company_jv(self):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
@@ -563,6 +450,25 @@ class JournalEntry(AccountsController):
|
||||
if status:
|
||||
inv_disc_doc.set_status(status=status)
|
||||
|
||||
def update_booked_depreciation(self, cancel=0):
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
self.voucher_type == "Depreciation Entry"
|
||||
and d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
|
||||
and d.debit
|
||||
):
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
if cancel:
|
||||
fb_row.total_number_of_booked_depreciations -= 1
|
||||
else:
|
||||
fb_row.total_number_of_booked_depreciations += 1
|
||||
fb_row.db_update()
|
||||
break
|
||||
|
||||
def unlink_advance_entry_reference(self):
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
@@ -612,9 +518,9 @@ class JournalEntry(AccountsController):
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation += d.debit
|
||||
fb_row.db_update()
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
|
||||
else:
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
|
||||
asset.set_status()
|
||||
asset.set_total_booked_depreciations()
|
||||
elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name:
|
||||
journal_entry_for_scrap = frappe.db.get_value(
|
||||
"Asset", d.reference_name, "journal_entry_for_scrap"
|
||||
@@ -645,11 +551,8 @@ class JournalEntry(AccountsController):
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
|
||||
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):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
@@ -658,8 +561,6 @@ class JournalEntry(AccountsController):
|
||||
elif (
|
||||
d.party_type
|
||||
and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type
|
||||
and d.party_type
|
||||
!= "Employee" # making an excpetion for employee since they can be both payable and receivable
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Account {1} and Party Type {2} have different account types").format(
|
||||
@@ -1185,70 +1086,49 @@ class JournalEntry(AccountsController):
|
||||
self.transaction_exchange_rate = row.exchange_rate
|
||||
break
|
||||
|
||||
advance_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
r = [x for x in r if x]
|
||||
remarks = "\n".join(r)
|
||||
|
||||
row = {
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
}
|
||||
|
||||
if d.reference_type in advance_doctypes:
|
||||
row.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_type,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
# 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,
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
},
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
@@ -1257,7 +1137,7 @@ class JournalEntry(AccountsController):
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
merge_entries = frappe.get_single_value("Accounts Settings", "merge_similar_account_heads")
|
||||
merge_entries = frappe.db.get_single_value("Accounts Settings", "merge_similar_account_heads")
|
||||
|
||||
gl_map = self.build_gl_map()
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
|
||||
@@ -1273,7 +1153,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))
|
||||
|
||||
@@ -1384,7 +1263,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_bank_cash_account(
|
||||
company, account_type=None, mode_of_payment=None, account=None, *, fetch_balance=True
|
||||
company, account_type=None, mode_of_payment=None, account=None, ignore_permissions=False
|
||||
):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
|
||||
@@ -1419,14 +1298,15 @@ def get_default_bank_cash_account(
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_currency", "account_type"], as_dict=1
|
||||
)
|
||||
result = {
|
||||
"account": account,
|
||||
"account_currency": account_details.account_currency,
|
||||
"account_type": account_details.account_type,
|
||||
}
|
||||
if fetch_balance:
|
||||
result["balance"] = get_balance_on(account)
|
||||
return frappe._dict(result)
|
||||
|
||||
return frappe._dict(
|
||||
{
|
||||
"account": account,
|
||||
"balance": get_balance_on(account, ignore_account_permission=ignore_permissions),
|
||||
"account_currency": account_details.account_currency,
|
||||
"account_type": account_details.account_type,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return frappe._dict()
|
||||
|
||||
@@ -1806,14 +1686,6 @@ def make_inter_company_journal_entry(name, voucher_type, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
|
||||
if existing_reverse:
|
||||
frappe.throw(
|
||||
_("A Reverse Journal Entry {0} already exists for this Journal Entry.").format(
|
||||
get_link_to_form("Journal Entry", existing_reverse)
|
||||
)
|
||||
)
|
||||
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
def post_process(source, target):
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
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 UnitTestJournalEntry(UnitTestCase):
|
||||
"""
|
||||
Unit tests for JournalEntry.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestJournalEntry(IntegrationTestCase):
|
||||
@@ -580,27 +588,6 @@ class TestJournalEntry(IntegrationTestCase):
|
||||
]
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_pay_to_recd_from(self):
|
||||
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
|
||||
jv.pay_to_recd_from = "_Test Receiver"
|
||||
jv.save()
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver")
|
||||
|
||||
jv.pay_to_recd_from = "_Test Receiver 2"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
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_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
"reference_name",
|
||||
"reference_due_date",
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -106,6 +104,7 @@
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"no_copy": 1,
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@@ -263,39 +262,20 @@
|
||||
"hidden": 1,
|
||||
"label": "Reference Detail No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Voucher Type",
|
||||
"no_copy": 1,
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Advance Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-27 13:48:32.805100",
|
||||
"modified": "2024-03-27 13:09:58.647732",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,8 @@ class JournalEntryAccount(Document):
|
||||
account: DF.Link
|
||||
account_currency: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
against_account: DF.Text | None
|
||||
balance: DF.Currency
|
||||
bank_account: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
@@ -32,6 +31,7 @@ class JournalEntryAccount(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
party: DF.DynamicLink | None
|
||||
party_balance: DF.Currency
|
||||
party_type: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reference_detail_no: DF.Data | None
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
class UnitTestLedgerHealthMonitor(UnitTestCase):
|
||||
"""
|
||||
Unit tests for LedgerHealthMonitor.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TestLedgerHealthMonitor(IntegrationTestCase):
|
||||
|
||||
@@ -35,7 +35,7 @@ class LedgerMerge(Document):
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.in_test:
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
|
||||
|
||||
job_id = f"ledger_merge::{self.name}"
|
||||
@@ -47,7 +47,7 @@ class LedgerMerge(Document):
|
||||
event="ledger_merge",
|
||||
job_id=job_id,
|
||||
docname=self.name,
|
||||
now=frappe.conf.developer_mode or frappe.in_test,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"help_section",
|
||||
"loyalty_program_help"
|
||||
],
|
||||
@@ -145,12 +144,6 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, today
|
||||
|
||||
|
||||
@@ -56,29 +55,21 @@ def get_loyalty_details(
|
||||
if not expiry_date:
|
||||
expiry_date = today()
|
||||
|
||||
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(LoyaltyPointEntry)
|
||||
.select(
|
||||
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
|
||||
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
|
||||
)
|
||||
.where(
|
||||
(LoyaltyPointEntry.customer == customer)
|
||||
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
|
||||
& (LoyaltyPointEntry.posting_date <= expiry_date)
|
||||
)
|
||||
.groupby(LoyaltyPointEntry.customer)
|
||||
)
|
||||
|
||||
condition = ""
|
||||
if company:
|
||||
query = query.where(LoyaltyPointEntry.company == company)
|
||||
|
||||
condition = " and company=%s " % frappe.db.escape(company)
|
||||
if not include_expired_entry:
|
||||
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
|
||||
condition += " and expiry_date>='%s' " % expiry_date
|
||||
|
||||
loyalty_point_details = query.run(as_dict=True)
|
||||
loyalty_point_details = frappe.db.sql(
|
||||
f"""select sum(loyalty_points) as loyalty_points,
|
||||
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
|
||||
where customer=%s and loyalty_program=%s and posting_date <= %s
|
||||
{condition}
|
||||
group by customer""",
|
||||
(customer, loyalty_program, expiry_date),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if loyalty_point_details:
|
||||
return loyalty_point_details[0]
|
||||
@@ -187,9 +178,8 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
|
||||
|
||||
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
|
||||
|
||||
total_amount = ref_doc.grand_total if ref_doc.is_rounded_total_disabled() else ref_doc.rounded_total
|
||||
if loyalty_amount > total_amount:
|
||||
frappe.throw(_("You can't redeem Loyalty Points having more value than the Total Amount."))
|
||||
if loyalty_amount > ref_doc.rounded_total:
|
||||
frappe.throw(_("You can't redeem Loyalty Points having more value than the Rounded Total."))
|
||||
|
||||
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
|
||||
ref_doc.loyalty_amount = loyalty_amount
|
||||
|
||||
@@ -2,26 +2,8 @@
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestModeofPayment(IntegrationTestCase):
|
||||
pass
|
||||
|
||||
|
||||
def set_default_account_for_mode_of_payment(mode_of_payment, company, account):
|
||||
mode_of_payment.reload()
|
||||
if frappe.db.exists(
|
||||
"Mode of Payment Account", {"parent": mode_of_payment.mode_of_payment, "company": company}
|
||||
):
|
||||
frappe.db.set_value(
|
||||
"Mode of Payment Account",
|
||||
{"parent": mode_of_payment.mode_of_payment, "company": company},
|
||||
"default_account",
|
||||
account,
|
||||
)
|
||||
return
|
||||
|
||||
mode_of_payment.append("accounts", {"company": company, "default_account": account})
|
||||
mode_of_payment.save()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
[{
|
||||
"doctype": "Monthly Distribution",
|
||||
"distribution_id": "_Test Distribution",
|
||||
"fiscal_year": "_Test Fiscal Year 2013",
|
||||
"percentages": [
|
||||
{
|
||||
"month": "January",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "February",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "March",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "April",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "May",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "June",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "July",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "August",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "September",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "October",
|
||||
"percentage_allocation": "8"
|
||||
}, {
|
||||
"month": "November",
|
||||
"percentage_allocation": "10"
|
||||
}, {
|
||||
"month": "December",
|
||||
"percentage_allocation": "10"
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -14,7 +14,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_4",
|
||||
"invoices"
|
||||
],
|
||||
@@ -64,12 +63,6 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user