Compare commits

..

1 Commits

Author SHA1 Message Date
Deepesh Garg
01974abbe1 Revert "Update tr.csv (#34285)"
This reverts commit 5266a7e8a7.
2023-03-13 19:42:46 +05:30
534 changed files with 9298 additions and 20142 deletions

View File

@@ -8,9 +8,8 @@ sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappebranch=${FRAPPE_BRANCH:-$githubbranch}
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
@@ -57,7 +56,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 ${githubbranch%"-hotfix"}
bench get-app payments
bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi

2
.github/stale.yml vendored
View File

@@ -13,7 +13,7 @@ exemptProjects: true
exemptMilestones: true
pulls:
daysUntilStale: 14
daysUntilStale: 15
daysUntilClose: 3
exemptLabels:
- hotfix

View File

@@ -43,11 +43,9 @@ jobs:
fi
- name: Setup Python
uses: "actions/setup-python@v4"
uses: "gabrielfalcao/pyenv-action@v9"
with:
python-version: |
3.7
3.10
versions: 3.10:latest, 3.7:latest
- name: Setup Node
uses: actions/setup-node@v2
@@ -94,6 +92,7 @@ jobs:
- name: Install
run: |
pip install frappe-bench
pyenv global $(pyenv versions | grep '3.10')
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
@@ -108,6 +107,7 @@ jobs:
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
pyenv global $(pyenv versions | grep '3.7')
for version in $(seq 12 13)
do
echo "Updating to v$version"
@@ -120,7 +120,7 @@ jobs:
git -C "apps/erpnext" checkout -q -f $branch_name
rm -rf ~/frappe-bench/env
bench setup env --python python3.7
bench setup env
bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext
@@ -132,8 +132,9 @@ jobs:
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
pyenv global $(pyenv versions | grep '3.10')
rm -rf ~/frappe-bench/env
bench -v setup env --python python3.10
bench -v setup env
bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext

View File

@@ -1,38 +0,0 @@
# This action:
#
# 1. Generates release notes using github API.
# 2. Strips unnecessary info like chore/style etc from notes.
# 3. Updates release info.
# This action needs to be maintained on all branches that do releases.
name: 'Release Notes'
on:
workflow_dispatch:
inputs:
tag_name:
description: 'Tag of release like v13.0.0'
required: true
type: string
release:
types: [released]
permissions:
contents: read
jobs:
regen-notes:
name: 'Regenerate release notes'
runs-on: ubuntu-latest
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}

View File

@@ -3,13 +3,13 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @deepeshgarg007
erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007
pos*
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
@@ -18,8 +18,12 @@ erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/patches/ @deepeshgarg007
erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev
erpnext/projects/ @ruchamahabal
.github/ @deepeshgarg007
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
.github/ @ankush
pyproject.toml @ankush

View File

@@ -1,9 +1,8 @@
import functools
import inspect
import frappe
__version__ = "14.34.1"
__version__ = "14.2.3"
def get_default_company(user=None):
@@ -121,14 +120,12 @@ def get_region(company=None):
You can also set global company flag in `frappe.flags.company`
"""
if not company:
company = frappe.local.flags.company
if company:
return frappe.get_cached_value("Company", company, "country")
return frappe.flags.country or frappe.get_system_settings("country")
if company or frappe.flags.company:
return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
elif frappe.flags.country:
return frappe.flags.country
else:
return frappe.get_system_settings("country")
def allow_regional(fn):
@@ -139,7 +136,6 @@ def allow_regional(fn):
def myfunction():
pass"""
@functools.wraps(fn)
def caller(*args, **kwargs):
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"

View File

@@ -4,19 +4,18 @@
"creation": "2020-07-17 11:25:34.593061",
"docstatus": 0,
"doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
"filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2023-07-19 13:13:13.307073",
"modified": "2020-07-22 12:24:49.144210",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget Variance",
"number_of_groups": 0,
"owner": "Administrator",
"report_name": "Budget Variance Report",
"roles": [],
"timeseries": 0,
"type": "Bar",
"use_report_chart": 1,

View File

@@ -4,19 +4,18 @@
"creation": "2020-07-17 11:25:34.448572",
"docstatus": 0,
"doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
"filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"modified": "2023-07-19 13:08:56.470390",
"modified": "2020-07-22 12:33:48.888943",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Profit and Loss",
"number_of_groups": 0,
"owner": "Administrator",
"report_name": "Profit and Loss Statement",
"roles": [],
"timeseries": 0,
"type": "Bar",
"use_report_chart": 1,

View File

@@ -136,7 +136,7 @@ def convert_deferred_revenue_to_income(
send_mail(deferred_process)
def get_booking_dates(doc, item, posting_date=None, prev_posting_date=None):
def get_booking_dates(doc, item, posting_date=None):
if not posting_date:
posting_date = add_days(today(), -1)
@@ -146,42 +146,39 @@ def get_booking_dates(doc, item, posting_date=None, prev_posting_date=None):
"deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account"
)
if not prev_posting_date:
prev_gl_entry = frappe.db.sql(
"""
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
order by posting_date desc limit 1
""",
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
prev_gl_entry = frappe.db.sql(
"""
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
order by posting_date desc limit 1
""",
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
prev_gl_via_je = frappe.db.sql(
"""
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
WHERE p.name = c.parent and p.company=%s and c.account=%s
and c.reference_type=%s and c.reference_name=%s
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
""",
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
prev_gl_via_je = frappe.db.sql(
"""
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
WHERE p.name = c.parent and p.company=%s and c.account=%s
and c.reference_type=%s and c.reference_name=%s
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
""",
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
as_dict=True,
)
if prev_gl_via_je:
if (not prev_gl_entry) or (
prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
):
prev_gl_entry = prev_gl_via_je
if prev_gl_entry:
start_date = getdate(add_days(prev_gl_entry[0].posting_date, 1))
else:
start_date = item.service_start_date
if prev_gl_via_je:
if (not prev_gl_entry) or (
prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
):
prev_gl_entry = prev_gl_via_je
if prev_gl_entry:
start_date = getdate(add_days(prev_gl_entry[0].posting_date, 1))
else:
start_date = getdate(add_days(prev_posting_date, 1))
start_date = item.service_start_date
end_date = get_last_day(start_date)
if end_date >= item.service_end_date:
end_date = item.service_end_date
@@ -341,18 +338,12 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
)
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
def _book_deferred_revenue_or_expense(
item,
via_journal_entry,
submit_journal_entry,
book_deferred_entries_based_on,
prev_posting_date=None,
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
):
start_date, end_date, last_gl_entry = get_booking_dates(
doc, item, posting_date=posting_date, prev_posting_date=prev_posting_date
)
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date):
return
@@ -386,12 +377,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if not amount:
return
gl_posting_date = end_date
prev_posting_date = None
# check if books nor frozen till endate:
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1))
prev_posting_date = end_date
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:
book_revenue_via_journal_entry(
@@ -400,7 +388,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
debit_account,
amount,
base_amount,
gl_posting_date,
end_date,
project,
account_currency,
item.cost_center,
@@ -416,7 +404,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
against,
amount,
base_amount,
gl_posting_date,
end_date,
project,
account_currency,
item.cost_center,
@@ -430,11 +418,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
_book_deferred_revenue_or_expense(
item,
via_journal_entry,
submit_journal_entry,
book_deferred_entries_based_on,
prev_posting_date,
item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
)
via_journal_entry = cint(

View File

@@ -18,6 +18,7 @@
"root_type",
"report_type",
"account_currency",
"inter_company_account",
"column_break1",
"parent_account",
"account_type",
@@ -33,11 +34,15 @@
{
"fieldname": "properties",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break"
"oldfieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break0",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1,
"width": "50%"
},
{
@@ -48,7 +53,9 @@
"no_copy": 1,
"oldfieldname": "account_name",
"oldfieldtype": "Data",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "account_number",
@@ -56,13 +63,17 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account Number",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "is_group",
"fieldtype": "Check",
"label": "Is Group"
"label": "Is Group",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "company",
@@ -74,7 +85,9 @@
"options": "Company",
"read_only": 1,
"remember_last_selected_value": 1,
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "root_type",
@@ -82,7 +95,9 @@
"in_standard_filter": 1,
"label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "report_type",
@@ -90,18 +105,32 @@
"in_standard_filter": 1,
"label": "Report Type",
"options": "\nBalance Sheet\nProfit and Loss",
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"depends_on": "eval:doc.is_group==0",
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
"options": "Currency",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "inter_company_account",
"fieldtype": "Check",
"label": "Inter Company Account",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1,
"width": "50%"
},
{
@@ -113,7 +142,9 @@
"oldfieldtype": "Link",
"options": "Account",
"reqd": 1,
"search_index": 1
"search_index": 1,
"show_days": 1,
"show_seconds": 1
},
{
"description": "Setting Account Type helps in selecting this Account in transactions.",
@@ -123,7 +154,9 @@
"label": "Account Type",
"oldfieldname": "account_type",
"oldfieldtype": "Select",
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
"show_days": 1,
"show_seconds": 1
},
{
"description": "Rate at which this tax is applied",
@@ -131,7 +164,9 @@
"fieldtype": "Float",
"label": "Rate",
"oldfieldname": "tax_rate",
"oldfieldtype": "Currency"
"oldfieldtype": "Currency",
"show_days": 1,
"show_seconds": 1
},
{
"description": "If the account is frozen, entries are allowed to restricted users.",
@@ -140,13 +175,17 @@
"label": "Frozen",
"oldfieldname": "freeze_account",
"oldfieldtype": "Select",
"options": "No\nYes"
"options": "No\nYes",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "balance_must_be",
"fieldtype": "Select",
"label": "Balance must be",
"options": "\nDebit\nCredit"
"options": "\nDebit\nCredit",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "lft",
@@ -155,7 +194,9 @@
"label": "Lft",
"print_hide": 1,
"read_only": 1,
"search_index": 1
"search_index": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "rgt",
@@ -164,7 +205,9 @@
"label": "Rgt",
"print_hide": 1,
"read_only": 1,
"search_index": 1
"search_index": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "old_parent",
@@ -172,27 +215,33 @@
"hidden": 1,
"label": "Old Parent",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)",
"fieldname": "include_in_gross",
"fieldtype": "Check",
"label": "Include in gross"
"label": "Include in gross",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disable"
"label": "Disable",
"show_days": 1,
"show_seconds": 1
}
],
"icon": "fa fa-money",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2023-04-11 16:08:46.983677",
"modified": "2020-06-11 15:15:54.338622",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -252,6 +301,5 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@@ -201,11 +201,8 @@ class Account(NestedSet):
)
def validate_account_currency(self):
self.currency_explicitly_specified = True
if not self.account_currency:
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}, "account_currency")
@@ -251,10 +248,8 @@ class Account(NestedSet):
{
"company": company,
# parent account's currency should be passed down to child account's curreny
# if currency explicitly specified by user, child will inherit. else, default currency will be used.
"account_currency": self.account_currency
if self.currency_explicitly_specified
else erpnext.get_company_currency(company),
# if it is None, it picks it up from default company currency, which might be unintended
"account_currency": erpnext.get_company_currency(company),
"parent_account": parent_acc_name_map[company],
}
)
@@ -398,13 +393,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if ancestors and not allow_independent_account_creation:
for ancestor in ancestors:
old_name = frappe.db.get_value(
"Account",
{"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor},
"name",
)
if old_name:
if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")

View File

@@ -2,13 +2,75 @@
"country_code": "nl",
"name": "Netherlands - Grootboekschema",
"tree": {
"FABRIKAGEREKENINGEN": {
"is_group": 1,
"root_type": "Expense"
},
"FINANCIELE REKENINGEN, KORTLOPENDE VORDERINGEN EN SCHULDEN": {
"Bank": {
"RABO Bank": {
"account_type": "Bank"
},
"account_type": "Bank"
},
},
"KORTLOPENDE SCHULDEN": {
"Af te dragen Btw-verlegd": {
"account_type": "Tax"
},
"Afdracht loonheffing": {},
"Btw af te dragen hoog": {
"account_type": "Tax"
},
"Btw af te dragen laag": {
"account_type": "Tax"
},
"Btw af te dragen overig": {
"account_type": "Tax"
},
"Btw oude jaren": {
"account_type": "Tax"
},
"Btw te vorderen hoog": {
"account_type": "Tax"
},
"Btw te vorderen laag": {
"account_type": "Tax"
},
"Btw te vorderen overig": {
"account_type": "Tax"
},
"Btw-afdracht": {
"account_type": "Tax"
},
"Crediteuren": {
"account_type": "Payable"
},
"Dividend": {},
"Dividendbelasting": {},
"Energiekosten 1": {},
"Investeringsaftrek": {},
"Loonheffing": {},
"Overige te betalen posten": {},
"Pensioenpremies 1": {},
"Premie WIR": {},
"Rekening-courant inkoopvereniging": {},
"Rente": {},
"Sociale lasten 1": {},
"Stock Recieved niet gefactureerd": {
"account_type": "Stock Received But Not Billed"
},
"Tanti\u00e8mes 1": {},
"Te vorderen Btw-verlegd": {
"account_type": "Tax"
},
"Telefoon/telefax 1": {},
"Termijnen onderh. werk": {},
"Vakantiedagen": {},
"Vakantiegeld 1": {},
"Vakantiezegels": {},
"Vennootschapsbelasting": {},
"Vooruit ontvangen bedr.": {}
},
"LIQUIDE MIDDELEN": {
"ABN-AMRO bank": {},
"Bankbetaalkaarten": {},
@@ -29,110 +91,6 @@
},
"account_type": "Cash"
},
"TUSSENREKENINGEN": {
"Betaalwijze cadeaubonnen": {
"account_type": "Cash"
},
"Betaalwijze chipknip": {
"account_type": "Cash"
},
"Betaalwijze contant": {
"account_type": "Cash"
},
"Betaalwijze pin": {
"account_type": "Cash"
},
"Inkopen Nederland hoog": {
"account_type": "Cash"
},
"Inkopen Nederland laag": {
"account_type": "Cash"
},
"Inkopen Nederland onbelast": {
"account_type": "Cash"
},
"Inkopen Nederland overig": {
"account_type": "Cash"
},
"Inkopen Nederland verlegd": {
"account_type": "Cash"
},
"Inkopen binnen EU hoog": {
"account_type": "Cash"
},
"Inkopen binnen EU laag": {
"account_type": "Cash"
},
"Inkopen binnen EU overig": {
"account_type": "Cash"
},
"Inkopen buiten EU hoog": {
"account_type": "Cash"
},
"Inkopen buiten EU laag": {
"account_type": "Cash"
},
"Inkopen buiten EU overig": {
"account_type": "Cash"
},
"Kassa 1": {
"account_type": "Cash"
},
"Kassa 2": {
"account_type": "Cash"
},
"Netto lonen": {
"account_type": "Cash"
},
"Tegenrekening Inkopen": {
"account_type": "Cash"
},
"Tussenrek. autom. betalingen": {
"account_type": "Cash"
},
"Tussenrek. autom. loonbetalingen": {
"account_type": "Cash"
},
"Tussenrek. cadeaubonbetalingen": {
"account_type": "Cash"
},
"Tussenrekening balans": {
"account_type": "Cash"
},
"Tussenrekening chipknip": {
"account_type": "Cash"
},
"Tussenrekening correcties": {
"account_type": "Cash"
},
"Tussenrekening pin": {
"account_type": "Cash"
},
"Vraagposten": {
"account_type": "Cash"
},
"VOORRAAD GRONDSTOFFEN, HULPMATERIALEN EN HANDELSGOEDEREN": {
"Emballage": {},
"Gereed product 1": {},
"Gereed product 2": {},
"Goederen 1": {},
"Goederen 2": {},
"Goederen in consignatie": {},
"Goederen onderweg": {},
"Grondstoffen 1": {},
"Grondstoffen 2": {},
"Halffabrikaten 1": {},
"Halffabrikaten 2": {},
"Hulpstoffen 1": {},
"Hulpstoffen 2": {},
"Kantoorbenodigdheden": {},
"Onderhanden werk": {},
"Verpakkingsmateriaal": {},
"Zegels": {},
"root_type": "Asset"
},
"root_type": "Asset"
},
"VORDERINGEN": {
"Debiteuren": {
"account_type": "Receivable"
@@ -146,299 +104,278 @@
"Voorziening dubieuze debiteuren": {}
},
"root_type": "Asset"
},
"KORTLOPENDE SCHULDEN": {
"Af te dragen Btw-verlegd": {
"account_type": "Tax"
},
"Afdracht loonheffing": {},
"Btw af te dragen hoog": {
"account_type": "Tax"
},
"Btw af te dragen laag": {
"account_type": "Tax"
},
"Btw af te dragen overig": {
"account_type": "Tax"
},
"Btw oude jaren": {
"account_type": "Tax"
},
"Btw te vorderen hoog": {
"account_type": "Tax"
},
"Btw te vorderen laag": {
"account_type": "Tax"
},
"Btw te vorderen overig": {
"account_type": "Tax"
},
"Btw-afdracht": {
"account_type": "Tax"
},
"Crediteuren": {
"account_type": "Payable"
},
"Dividend": {},
"Dividendbelasting": {},
"Energiekosten 1": {},
"Investeringsaftrek": {},
"Loonheffing": {},
"Overige te betalen posten": {},
"Pensioenpremies 1": {},
"Premie WIR": {},
"Rekening-courant inkoopvereniging": {},
"Rente": {},
"Sociale lasten 1": {},
"Stock Recieved niet gefactureerd": {
"account_type": "Stock Received But Not Billed"
},
"Tanti\u00e8mes 1": {},
"Te vorderen Btw-verlegd": {
"account_type": "Tax"
},
"Telefoon/telefax 1": {},
"Termijnen onderh. werk": {},
"Vakantiedagen": {},
"Vakantiegeld 1": {},
"Vakantiezegels": {},
"Vennootschapsbelasting": {},
"Vooruit ontvangen bedr.": {},
"is_group": 1,
"root_type": "Liability"
},
"FABRIKAGEREKENINGEN": {
},
"INDIRECTE KOSTEN": {
"is_group": 1,
"root_type": "Expense",
"INDIRECTE KOSTEN": {
"is_group": 1,
"root_type": "Expense"
},
"KOSTENREKENINGEN": {
"AFSCHRIJVINGEN": {
"Aanhangwagens": {},
"Aankoopkosten": {},
"Aanloopkosten": {},
"Auteursrechten": {},
"Bedrijfsgebouwen": {},
"Bedrijfsinventaris": {
"account_type": "Depreciation"
},
"Drankvergunningen": {},
"Fabrieksinventaris": {
"account_type": "Depreciation"
},
"Gebouwen": {},
"Gereedschappen": {},
"Goodwill": {},
"Grondverbetering": {},
"Heftrucks": {},
"Kantine-inventaris": {},
"Kantoorinventaris": {
"account_type": "Depreciation"
},
"Kantoormachines": {},
"Licenties": {},
"Machines 1": {},
"Magazijninventaris": {},
"Octrooien": {},
"Ontwikkelingskosten": {},
"Pachtersinvestering": {},
"Parkeerplaats": {},
"Personenauto's": {
"account_type": "Depreciation"
},
"Rijwielen en bromfietsen": {},
"Tonnagevergunningen": {},
"Verbouwingen": {},
"Vergunningen": {},
"Voorraadverschillen": {},
"Vrachtauto's": {},
"Winkels": {},
"Woon-winkelhuis": {},
"root_type": "Expense"
},
"KOSTENREKENINGEN": {
"AFSCHRIJVINGEN": {
"Aanhangwagens": {},
"Aankoopkosten": {},
"Aanloopkosten": {},
"Auteursrechten": {},
"Bedrijfsgebouwen": {},
"Bedrijfsinventaris": {
"account_type": "Depreciation"
},
"ALGEMENE KOSTEN": {
"Accountantskosten": {},
"Advieskosten": {},
"Assuranties 1": {},
"Bankkosten": {},
"Juridische kosten": {},
"Overige algemene kosten": {},
"Toev. Ass. eigen risico": {}
"Drankvergunningen": {},
"Fabrieksinventaris": {
"account_type": "Depreciation"
},
"BEDRIJFSKOSTEN": {
"Assuranties 2": {},
"Energie (krachtstroom)": {},
"Gereedschappen 1": {},
"Hulpmaterialen 1": {},
"Huur inventaris": {},
"Huur machines": {},
"Leasing invent.operational": {},
"Leasing mach. operational": {},
"Onderhoud inventaris": {},
"Onderhoud machines": {},
"Ophalen/vervoer afval": {},
"Overige bedrijfskosten": {}
"Gebouwen": {},
"Gereedschappen": {},
"Goodwill": {},
"Grondverbetering": {},
"Heftrucks": {},
"Kantine-inventaris": {},
"Kantoorinventaris": {
"account_type": "Depreciation"
},
"FINANCIERINGSKOSTEN 1": {
"Overige rentebaten": {},
"Overige rentelasten": {},
"Rente bankkrediet": {},
"Rente huurkoopcontracten": {},
"Rente hypotheek": {},
"Rente leasecontracten": {},
"Rente lening o/g": {},
"Rente lening u/g": {}
"Kantoormachines": {},
"Licenties": {},
"Machines 1": {},
"Magazijninventaris": {},
"Octrooien": {},
"Ontwikkelingskosten": {},
"Pachtersinvestering": {},
"Parkeerplaats": {},
"Personenauto's": {
"account_type": "Depreciation"
},
"HUISVESTINGSKOSTEN": {
"Assurantie onroerend goed": {},
"Belastingen onr. Goed": {},
"Energiekosten": {},
"Groot onderhoud onr. Goed": {},
"Huur": {},
"Huurwaarde woongedeelte": {},
"Onderhoud onroerend goed": {},
"Ontvangen huren": {},
"Overige huisvestingskosten": {},
"Pacht": {},
"Schoonmaakkosten": {},
"Toevoeging egalisatieres. Groot onderhoud": {}
},
"KANTOORKOSTEN": {
"Administratiekosten": {},
"Contributies/abonnementen": {},
"Huur kantoorapparatuur": {},
"Internetaansluiting": {},
"Kantoorbenodigdh./drukw.": {},
"Onderhoud kantoorinvent.": {},
"Overige kantoorkosten": {},
"Porti": {},
"Telefoon/telefax": {}
},
"OVERIGE BATEN EN LASTEN": {
"Betaalde schadevergoed.": {},
"Boekverlies vaste activa": {},
"Boekwinst van vaste activa": {},
"K.O. regeling OB": {},
"Kasverschillen": {},
"Kosten loonbelasting": {},
"Kosten omzetbelasting": {},
"Nadelige koersverschillen": {},
"Naheffing bedrijfsver.": {},
"Ontvangen schadevergoed.": {},
"Overige baten": {},
"Overige lasten": {},
"Voordelige koersverschil.": {}
},
"PERSONEELSKOSTEN": {
"Autokostenvergoeding": {},
"Bedrijfskleding": {},
"Belastingvrije uitkeringen": {},
"Bijzondere beloningen": {},
"Congressen, seminars en symposia": {},
"Gereedschapsgeld": {},
"Geschenken personeel": {},
"Gratificaties": {},
"Inhouding pensioenpremies": {},
"Inhouding sociale lasten": {},
"Kantinekosten": {},
"Lonen en salarissen": {},
"Loonwerk": {},
"Managementvergoedingen": {},
"Opleidingskosten": {},
"Oprenting stamrechtverpl.": {},
"Overhevelingstoeslag": {},
"Overige kostenverg.": {},
"Overige personeelskosten": {},
"Overige uitkeringen": {},
"Pensioenpremies": {},
"Provisie 1": {},
"Reiskosten": {},
"Rijwielvergoeding": {},
"Sociale lasten": {},
"Tanti\u00e8mes": {},
"Thuiswerkers": {},
"Toev. Backservice pens.verpl.": {},
"Toevoeging pensioenverpl.": {},
"Uitkering ziekengeld": {},
"Uitzendkrachten": {},
"Vakantiebonnen": {},
"Vakantiegeld": {},
"Vergoeding studiekosten": {},
"Wervingskosten personeel": {}
},
"VERKOOPKOSTEN": {
"Advertenties": {},
"Afschrijving dubieuze deb.": {},
"Beurskosten": {},
"Etalagekosten": {},
"Exportkosten": {},
"Kascorrecties": {},
"Overige verkoopkosten": {},
"Provisie": {},
"Reclame": {},
"Reis en verblijfkosten": {},
"Relatiegeschenken": {},
"Representatiekosten": {},
"Uitgaande vrachten": {},
"Veilingkosten": {},
"Verpakkingsmateriaal 1": {},
"Websitekosten": {}
},
"VERVOERSKOSTEN": {
"Assuranties auto's": {},
"Brandstoffen": {},
"Leasing auto's": {},
"Onderhoud personenauto's": {},
"Onderhoud vrachtauto's": {},
"Overige vervoerskosten": {},
"Priv\u00e9-gebruik auto's": {},
"Wegenbelasting": {}
},
"VOORRAAD GEREED PRODUCT EN ONDERHANDEN WERK": {
"Betalingskort. crediteuren": {},
"Garantiekosten": {},
"Hulpmaterialen": {},
"Inkomende vrachten": {
"account_type": "Expenses Included In Valuation"
},
"Inkoop import buiten EU hoog": {},
"Inkoop import buiten EU laag": {},
"Inkoop import buiten EU overig": {},
"Inkoopbonussen": {},
"Inkoopkosten": {},
"Inkoopprovisie": {},
"Inkopen BTW verlegd": {},
"Inkopen EU hoog tarief": {},
"Inkopen EU laag tarief": {},
"Inkopen EU overig": {},
"Inkopen hoog": {},
"Inkopen laag": {},
"Inkopen nul": {},
"Inkopen overig": {},
"Invoerkosten": {},
"Kosten inkoopvereniging": {},
"Kostprijs omzet grondstoffen": {
"account_type": "Cost of Goods Sold"
},
"Kostprijs omzet handelsgoederen": {},
"Onttrekking uitgev.garantie": {},
"Priv\u00e9-gebruik goederen": {},
"Stock aanpassing": {
"account_type": "Stock Adjustment"
},
"Tegenrekening inkoop": {},
"Toev. Voorz. incour. grondst.": {},
"Toevoeging garantieverpl.": {},
"Toevoeging voorz. incour. handelsgoed.": {},
"Uitbesteed werk": {},
"Voorz. Incourourant grondst.": {},
"Voorz.incour. handelsgoed.": {},
"root_type": "Expense"
},
"root_type": "Expense"
}
"Rijwielen en bromfietsen": {},
"Tonnagevergunningen": {},
"Verbouwingen": {},
"Vergunningen": {},
"Voorraadverschillen": {},
"Vrachtauto's": {},
"Winkels": {},
"Woon-winkelhuis": {},
"account_type": "Depreciation"
},
"ALGEMENE KOSTEN": {
"Accountantskosten": {},
"Advieskosten": {},
"Assuranties 1": {},
"Bankkosten": {},
"Juridische kosten": {},
"Overige algemene kosten": {},
"Toev. Ass. eigen risico": {}
},
"BEDRIJFSKOSTEN": {
"Assuranties 2": {},
"Energie (krachtstroom)": {},
"Gereedschappen 1": {},
"Hulpmaterialen 1": {},
"Huur inventaris": {},
"Huur machines": {},
"Leasing invent.operational": {},
"Leasing mach. operational": {},
"Onderhoud inventaris": {},
"Onderhoud machines": {},
"Ophalen/vervoer afval": {},
"Overige bedrijfskosten": {}
},
"FINANCIERINGSKOSTEN 1": {
"Overige rentebaten": {},
"Overige rentelasten": {},
"Rente bankkrediet": {},
"Rente huurkoopcontracten": {},
"Rente hypotheek": {},
"Rente leasecontracten": {},
"Rente lening o/g": {},
"Rente lening u/g": {}
},
"HUISVESTINGSKOSTEN": {
"Assurantie onroerend goed": {},
"Belastingen onr. Goed": {},
"Energiekosten": {},
"Groot onderhoud onr. Goed": {},
"Huur": {},
"Huurwaarde woongedeelte": {},
"Onderhoud onroerend goed": {},
"Ontvangen huren": {},
"Overige huisvestingskosten": {},
"Pacht": {},
"Schoonmaakkosten": {},
"Toevoeging egalisatieres. Groot onderhoud": {}
},
"KANTOORKOSTEN": {
"Administratiekosten": {},
"Contributies/abonnementen": {},
"Huur kantoorapparatuur": {},
"Internetaansluiting": {},
"Kantoorbenodigdh./drukw.": {},
"Onderhoud kantoorinvent.": {},
"Overige kantoorkosten": {},
"Porti": {},
"Telefoon/telefax": {}
},
"OVERIGE BATEN EN LASTEN": {
"Betaalde schadevergoed.": {},
"Boekverlies vaste activa": {},
"Boekwinst van vaste activa": {},
"K.O. regeling OB": {},
"Kasverschillen": {},
"Kosten loonbelasting": {},
"Kosten omzetbelasting": {},
"Nadelige koersverschillen": {},
"Naheffing bedrijfsver.": {},
"Ontvangen schadevergoed.": {},
"Overige baten": {},
"Overige lasten": {},
"Voordelige koersverschil.": {}
},
"PERSONEELSKOSTEN": {
"Autokostenvergoeding": {},
"Bedrijfskleding": {},
"Belastingvrije uitkeringen": {},
"Bijzondere beloningen": {},
"Congressen, seminars en symposia": {},
"Gereedschapsgeld": {},
"Geschenken personeel": {},
"Gratificaties": {},
"Inhouding pensioenpremies": {},
"Inhouding sociale lasten": {},
"Kantinekosten": {},
"Lonen en salarissen": {},
"Loonwerk": {},
"Managementvergoedingen": {},
"Opleidingskosten": {},
"Oprenting stamrechtverpl.": {},
"Overhevelingstoeslag": {},
"Overige kostenverg.": {},
"Overige personeelskosten": {},
"Overige uitkeringen": {},
"Pensioenpremies": {},
"Provisie 1": {},
"Reiskosten": {},
"Rijwielvergoeding": {},
"Sociale lasten": {},
"Tanti\u00e8mes": {},
"Thuiswerkers": {},
"Toev. Backservice pens.verpl.": {},
"Toevoeging pensioenverpl.": {},
"Uitkering ziekengeld": {},
"Uitzendkrachten": {},
"Vakantiebonnen": {},
"Vakantiegeld": {},
"Vergoeding studiekosten": {},
"Wervingskosten personeel": {}
},
"VERKOOPKOSTEN": {
"Advertenties": {},
"Afschrijving dubieuze deb.": {},
"Beurskosten": {},
"Etalagekosten": {},
"Exportkosten": {},
"Kascorrecties": {},
"Overige verkoopkosten": {},
"Provisie": {},
"Reclame": {},
"Reis en verblijfkosten": {},
"Relatiegeschenken": {},
"Representatiekosten": {},
"Uitgaande vrachten": {},
"Veilingkosten": {},
"Verpakkingsmateriaal 1": {},
"Websitekosten": {}
},
"VERVOERSKOSTEN": {
"Assuranties auto's": {},
"Brandstoffen": {},
"Leasing auto's": {},
"Onderhoud personenauto's": {},
"Onderhoud vrachtauto's": {},
"Overige vervoerskosten": {},
"Priv\u00e9-gebruik auto's": {},
"Wegenbelasting": {}
},
"root_type": "Expense"
},
"TUSSENREKENINGEN": {
"Betaalwijze cadeaubonnen": {
"account_type": "Cash"
},
"Betaalwijze chipknip": {
"account_type": "Cash"
},
"Betaalwijze contant": {
"account_type": "Cash"
},
"Betaalwijze pin": {
"account_type": "Cash"
},
"Inkopen Nederland hoog": {
"account_type": "Cash"
},
"Inkopen Nederland laag": {
"account_type": "Cash"
},
"Inkopen Nederland onbelast": {
"account_type": "Cash"
},
"Inkopen Nederland overig": {
"account_type": "Cash"
},
"Inkopen Nederland verlegd": {
"account_type": "Cash"
},
"Inkopen binnen EU hoog": {
"account_type": "Cash"
},
"Inkopen binnen EU laag": {
"account_type": "Cash"
},
"Inkopen binnen EU overig": {
"account_type": "Cash"
},
"Inkopen buiten EU hoog": {
"account_type": "Cash"
},
"Inkopen buiten EU laag": {
"account_type": "Cash"
},
"Inkopen buiten EU overig": {
"account_type": "Cash"
},
"Kassa 1": {
"account_type": "Cash"
},
"Kassa 2": {
"account_type": "Cash"
},
"Netto lonen": {
"account_type": "Cash"
},
"Tegenrekening Inkopen": {
"account_type": "Cash"
},
"Tussenrek. autom. betalingen": {
"account_type": "Cash"
},
"Tussenrek. autom. loonbetalingen": {
"account_type": "Cash"
},
"Tussenrek. cadeaubonbetalingen": {
"account_type": "Cash"
},
"Tussenrekening balans": {
"account_type": "Cash"
},
"Tussenrekening chipknip": {
"account_type": "Cash"
},
"Tussenrekening correcties": {
"account_type": "Cash"
},
"Tussenrekening pin": {
"account_type": "Cash"
},
"Vraagposten": {
"account_type": "Cash"
},
"root_type": "Asset"
},
"VASTE ACTIVA, EIGEN VERMOGEN, LANGLOPEND VREEMD VERMOGEN EN VOORZIENINGEN": {
"EIGEN VERMOGEN": {
@@ -665,7 +602,7 @@
"account_type": "Equity"
}
},
"root_type": "Equity"
"root_type": "Asset"
},
"VERKOOPRESULTATEN": {
"Diensten fabric. 0% niet-EU": {},
@@ -690,6 +627,67 @@
"Verleende Kredietbep. fabricage": {},
"Verleende Kredietbep. handel": {},
"root_type": "Income"
},
"VOORRAAD GEREED PRODUCT EN ONDERHANDEN WERK": {
"Betalingskort. crediteuren": {},
"Garantiekosten": {},
"Hulpmaterialen": {},
"Inkomende vrachten": {
"account_type": "Expenses Included In Valuation"
},
"Inkoop import buiten EU hoog": {},
"Inkoop import buiten EU laag": {},
"Inkoop import buiten EU overig": {},
"Inkoopbonussen": {},
"Inkoopkosten": {},
"Inkoopprovisie": {},
"Inkopen BTW verlegd": {},
"Inkopen EU hoog tarief": {},
"Inkopen EU laag tarief": {},
"Inkopen EU overig": {},
"Inkopen hoog": {},
"Inkopen laag": {},
"Inkopen nul": {},
"Inkopen overig": {},
"Invoerkosten": {},
"Kosten inkoopvereniging": {},
"Kostprijs omzet grondstoffen": {
"account_type": "Cost of Goods Sold"
},
"Kostprijs omzet handelsgoederen": {},
"Onttrekking uitgev.garantie": {},
"Priv\u00e9-gebruik goederen": {},
"Stock aanpassing": {
"account_type": "Stock Adjustment"
},
"Tegenrekening inkoop": {},
"Toev. Voorz. incour. grondst.": {},
"Toevoeging garantieverpl.": {},
"Toevoeging voorz. incour. handelsgoed.": {},
"Uitbesteed werk": {},
"Voorz. Incourourant grondst.": {},
"Voorz.incour. handelsgoed.": {},
"root_type": "Expense"
},
"VOORRAAD GRONDSTOFFEN, HULPMATERIALEN EN HANDELSGOEDEREN": {
"Emballage": {},
"Gereed product 1": {},
"Gereed product 2": {},
"Goederen 1": {},
"Goederen 2": {},
"Goederen in consignatie": {},
"Goederen onderweg": {},
"Grondstoffen 1": {},
"Grondstoffen 2": {},
"Halffabrikaten 1": {},
"Halffabrikaten 2": {},
"Hulpstoffen 1": {},
"Hulpstoffen 2": {},
"Kantoorbenodigdheden": {},
"Onderhanden werk": {},
"Verpakkingsmateriaal": {},
"Zegels": {},
"root_type": "Asset"
}
}
}

View File

@@ -5,13 +5,10 @@
import unittest
import frappe
from frappe.test_runner import make_test_records
from erpnext.accounts.doctype.account.account import merge_account, update_account_number
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"]
class TestAccount(unittest.TestCase):
def test_rename_account(self):
@@ -191,58 +188,6 @@ class TestAccount(unittest.TestCase):
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5")
def test_account_currency_sync(self):
"""
In a parent->child company setup, child should inherit parent account currency if explicitly specified.
"""
make_test_records("Company")
frappe.local.flags.pop("ignore_root_company_validation", None)
def create_bank_account():
acc = frappe.new_doc("Account")
acc.account_name = "_Test Bank JPY"
acc.parent_account = "Temporary Accounts - _TC6"
acc.company = "_Test Company 6"
return acc
acc = create_bank_account()
# Explicitly set currency
acc.account_currency = "JPY"
acc.insert()
self.assertTrue(
frappe.db.exists(
{
"doctype": "Account",
"account_name": "_Test Bank JPY",
"account_currency": "JPY",
"company": "_Test Company 7",
}
)
)
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
acc = create_bank_account()
# default currency is used
acc.insert()
self.assertTrue(
frappe.db.exists(
{
"doctype": "Account",
"account_name": "_Test Bank JPY",
"account_currency": "USD",
"company": "_Test Company 7",
}
)
)
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
def test_child_company_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)
@@ -352,7 +297,7 @@ def _make_test_records(verbose=None):
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Account Closing Balance", {
// refresh(frm) {
// },
// });

View File

@@ -1,164 +0,0 @@
{
"actions": [],
"creation": "2023-02-21 15:20:59.586811",
"default_view": "List",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"closing_date",
"account",
"cost_center",
"debit",
"credit",
"account_currency",
"debit_in_account_currency",
"credit_in_account_currency",
"project",
"company",
"finance_book",
"period_closing_voucher",
"is_period_closing_voucher_entry"
],
"fields": [
{
"fieldname": "closing_date",
"fieldtype": "Date",
"in_filter": 1,
"in_list_view": 1,
"label": "Closing Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
"search_index": 1
},
{
"fieldname": "account",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account",
"oldfieldname": "account",
"oldfieldtype": "Link",
"options": "Account",
"search_index": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Cost Center",
"oldfieldname": "cost_center",
"oldfieldtype": "Link",
"options": "Cost Center"
},
{
"fieldname": "debit",
"fieldtype": "Currency",
"label": "Debit Amount",
"oldfieldname": "debit",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "credit",
"fieldtype": "Currency",
"label": "Credit Amount",
"oldfieldname": "credit",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency"
},
{
"fieldname": "debit_in_account_currency",
"fieldtype": "Currency",
"label": "Debit Amount in Account Currency",
"options": "account_currency"
},
{
"fieldname": "credit_in_account_currency",
"fieldtype": "Currency",
"label": "Credit Amount in Account Currency",
"options": "account_currency"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
"search_index": 1
},
{
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
},
{
"fieldname": "period_closing_voucher",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Period Closing Voucher",
"options": "Period Closing Voucher",
"search_index": 1
},
{
"default": "0",
"fieldname": "is_period_closing_voucher_entry",
"fieldtype": "Check",
"label": "Is Period Closing Voucher Entry"
}
],
"icon": "fa fa-list",
"in_create": 1,
"links": [],
"modified": "2023-03-06 08:56:36.393237",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Closing Balance",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager"
},
{
"export": 1,
"read": 1,
"report": 1,
"role": "Auditor"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,126 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils import cint, cstr
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
class AccountClosingBalance(Document):
pass
def make_closing_entries(closing_entries, voucher_name, company, closing_date):
accounting_dimensions = get_accounting_dimensions()
previous_closing_entries = get_previous_closing_entries(
company, closing_date, accounting_dimensions
)
combined_entries = closing_entries + previous_closing_entries
merged_entries = aggregate_with_last_account_closing_balance(
combined_entries, accounting_dimensions
)
for key, value in merged_entries.items():
cle = frappe.new_doc("Account Closing Balance")
cle.update(value)
cle.update(value["dimensions"])
cle.update(
{
"period_closing_voucher": voucher_name,
"closing_date": closing_date,
}
)
cle.flags.ignore_permissions = True
cle.submit()
def aggregate_with_last_account_closing_balance(entries, accounting_dimensions):
merged_entries = {}
for entry in entries:
key, key_values = generate_key(entry, accounting_dimensions)
merged_entries.setdefault(
key,
{
"debit": 0,
"credit": 0,
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
},
)
merged_entries[key]["dimensions"] = key_values
merged_entries[key]["debit"] += entry.get("debit")
merged_entries[key]["credit"] += entry.get("credit")
merged_entries[key]["debit_in_account_currency"] += entry.get("debit_in_account_currency")
merged_entries[key]["credit_in_account_currency"] += entry.get("credit_in_account_currency")
return merged_entries
def generate_key(entry, accounting_dimensions):
key = [
cstr(entry.get("account")),
cstr(entry.get("account_currency")),
cstr(entry.get("cost_center")),
cstr(entry.get("project")),
cstr(entry.get("finance_book")),
cint(entry.get("is_period_closing_voucher_entry")),
]
key_values = {
"company": cstr(entry.get("company")),
"account": cstr(entry.get("account")),
"account_currency": cstr(entry.get("account_currency")),
"cost_center": cstr(entry.get("cost_center")),
"project": cstr(entry.get("project")),
"finance_book": cstr(entry.get("finance_book")),
"is_period_closing_voucher_entry": cint(entry.get("is_period_closing_voucher_entry")),
}
for dimension in accounting_dimensions:
key.append(cstr(entry.get(dimension)))
key_values[dimension] = cstr(entry.get(dimension))
return tuple(key), key_values
def get_previous_closing_entries(company, closing_date, accounting_dimensions):
entries = []
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
fields=["name"],
order_by="posting_date desc",
limit=1,
)
if last_period_closing_voucher:
account_closing_balance = frappe.qb.DocType("Account Closing Balance")
query = frappe.qb.from_(account_closing_balance).select(
account_closing_balance.company,
account_closing_balance.account,
account_closing_balance.account_currency,
account_closing_balance.debit,
account_closing_balance.credit,
account_closing_balance.debit_in_account_currency,
account_closing_balance.credit_in_account_currency,
account_closing_balance.cost_center,
account_closing_balance.project,
account_closing_balance.finance_book,
account_closing_balance.is_period_closing_voucher_entry,
)
for dimension in accounting_dimensions:
query = query.select(account_closing_balance[dimension])
query = query.where(
account_closing_balance.period_closing_voucher == last_period_closing_voucher[0].name
)
entries = query.run(as_dict=1)
return entries

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestAccountClosingBalance(FrappeTestCase):
pass

View File

@@ -50,15 +50,13 @@ class AccountingDimension(Document):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
frappe.enqueue(
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
)
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
def on_trash(self):
if frappe.flags.in_test:
delete_accounting_dimension(doc=self)
else:
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
def set_fieldname_and_label(self):
if not self.label:
@@ -271,12 +269,6 @@ def get_dimensions(with_cost_center_and_project=False):
as_dict=1,
)
if isinstance(with_cost_center_and_project, str):
if with_cost_center_and_project.lower().strip() == "true":
with_cost_center_and_project = True
else:
with_cost_center_and_project = False
if with_cost_center_and_project:
dimension_filters.extend(
[

View File

@@ -20,11 +20,5 @@ frappe.ui.form.on('Accounting Period', {
}
});
}
frm.set_query("document_type", "closed_documents", () => {
return {
query: "erpnext.controllers.queries.get_doctypes_for_closing",
}
});
}
});

View File

@@ -11,10 +11,6 @@ class OverlapError(frappe.ValidationError):
pass
class ClosedAccountingPeriod(frappe.ValidationError):
pass
class AccountingPeriod(Document):
def validate(self):
self.validate_overlap()
@@ -69,42 +65,3 @@ class AccountingPeriod(Document):
"closed_documents",
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
)
def validate_accounting_period_on_doc_save(doc, method=None):
if doc.doctype == "Bank Clearance":
return
elif doc.doctype == "Asset":
if doc.is_existing_asset:
return
else:
date = doc.available_for_use_date
elif doc.doctype == "Asset Repair":
date = doc.completion_date
else:
date = doc.posting_date
ap = frappe.qb.DocType("Accounting Period")
cd = frappe.qb.DocType("Closed Document")
accounting_period = (
frappe.qb.from_(ap)
.from_(cd)
.select(ap.name)
.where(
(ap.name == cd.parent)
& (ap.company == doc.company)
& (cd.closed == 1)
& (cd.document_type == doc.doctype)
& (date >= ap.start_date)
& (date <= ap.end_date)
)
).run(as_dict=1)
if accounting_period:
frappe.throw(
_("You cannot create a {0} within the closed Accounting Period {1}").format(
doc.doctype, frappe.bold(accounting_period[0]["name"])
),
ClosedAccountingPeriod,
)

View File

@@ -6,11 +6,9 @@ import unittest
import frappe
from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import (
ClosedAccountingPeriod,
OverlapError,
)
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
test_dependencies = ["Item"]
@@ -35,9 +33,9 @@ class TestAccountingPeriod(unittest.TestCase):
ap1.save()
doc = create_sales_invoice(
do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
)
self.assertRaises(ClosedAccountingPeriod, doc.save)
self.assertRaises(ClosedAccountingPeriod, doc.submit)
def tearDown(self):
for d in frappe.get_all("Accounting Period"):

View File

@@ -19,8 +19,6 @@
"column_break_17",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"journals_section",
"merge_similar_account_heads",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section",
@@ -33,16 +31,12 @@
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
"book_tax_discount_loss",
"print_settings",
"show_inclusive_tax_in_print",
"show_taxes_as_table_in_print",
"column_break_12",
"show_payment_schedule_in_print",
"currency_exchange_section",
"allow_stale",
"section_break_jpd0",
"auto_reconcile_payments",
"stale_days",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
@@ -60,13 +54,9 @@
"closing_settings_tab",
"period_closing_settings_section",
"acc_frozen_upto",
"ignore_account_closing_balance",
"column_break_25",
"frozen_accounts_modifier",
"report_settings_sb",
"banking_tab",
"enable_party_matching",
"enable_fuzzy_matching"
"report_settings_sb"
],
"fields": [
{
@@ -177,6 +167,11 @@
"fieldtype": "Int",
"label": "Stale Days"
},
{
"fieldname": "report_settings_sb",
"fieldtype": "Section Break",
"label": "Report Settings"
},
{
"default": "0",
"description": "Only select this if you have set up the Cash Flow Mapper documents",
@@ -186,7 +181,6 @@
},
{
"default": "0",
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order"
@@ -353,69 +347,6 @@
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account "
},
{
"default": "0",
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
},
{
"fieldname": "journals_section",
"fieldtype": "Section Break",
"label": "Journals"
},
{
"default": "0",
"description": "Rows with Same Account heads will be merged on Ledger",
"fieldname": "merge_similar_account_heads",
"fieldtype": "Check",
"label": "Merge Similar Account Heads"
},
{
"fieldname": "section_break_jpd0",
"fieldtype": "Section Break",
"label": "Payment Reconciliations"
},
{
"default": "0",
"fieldname": "auto_reconcile_payments",
"fieldtype": "Check",
"label": "Auto Reconcile Payments"
},
{
"default": "0",
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
},
{
"fieldname": "banking_tab",
"fieldtype": "Tab Break",
"label": "Banking"
},
{
"default": "0",
"description": "Auto match and set the Party in Bank Transactions",
"fieldname": "enable_party_matching",
"fieldtype": "Check",
"label": "Enable Automatic Party Matching"
},
{
"default": "0",
"depends_on": "enable_party_matching",
"description": "Approximately match the description/party name against parties",
"fieldname": "enable_fuzzy_matching",
"fieldtype": "Check",
"label": "Enable Fuzzy Matching"
},
{
"default": "0",
"description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ",
"fieldname": "ignore_account_closing_balance",
"fieldtype": "Check",
"label": "Ignore Account Closing Balance"
}
],
"icon": "icon-cog",
@@ -423,7 +354,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-07-27 15:05:34.000264",
"modified": "2022-11-27 21:49:52.538655",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -452,4 +383,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -14,32 +14,21 @@ from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
def on_update(self):
frappe.clear_cache()
def validate(self):
old_doc = self.get_doc_before_save()
clear_cache = False
frappe.db.set_default(
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
)
if old_doc.add_taxes_from_item_tax_template != self.add_taxes_from_item_tax_template:
frappe.db.set_default(
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
)
clear_cache = True
if old_doc.enable_common_party_accounting != self.enable_common_party_accounting:
frappe.db.set_default(
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
)
clear_cache = True
frappe.db.set_default(
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
)
self.validate_stale_days()
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()
self.enable_payment_schedule_in_print()
self.validate_pending_reposts()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:

View File

@@ -118,10 +118,6 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
}
plaid_success(token, response) {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', {
response: response,
}).then(() => {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
});
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
}
};

View File

@@ -56,7 +56,7 @@ class BankClearance(Document):
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, paid_amount, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) 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
@@ -81,7 +81,7 @@ class BankClearance(Document):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
query = (
loan_disbursements = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document"),
@@ -90,22 +90,17 @@ class BankClearance(Document):
ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"),
loan_disbursement.clearance_date.as_("clearance_date"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
)
if not self.include_reconciled_entries:
query = query.where(loan_disbursement.clearance_date.isnull())
loan_disbursements = query.run(as_dict=1)
).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
@@ -118,19 +113,16 @@ class BankClearance(Document):
ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"),
loan_repayment.clearance_date.as_("clearance_date"),
loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
)
if not self.include_reconciled_entries:
query = query.where(loan_repayment.clearance_date.isnull())
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))

View File

@@ -18,10 +18,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
onload: function (frm) {
// Set default filter dates
let today = frappe.datetime.get_today()
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
frm.doc.bank_statement_to_date = today;
frm.trigger('bank_account');
},
@@ -36,7 +32,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
refresh: function (frm) {
frm.disable_save();
frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool")
);
@@ -77,12 +72,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
})
});
},
frm.add_custom_button(__('Get Unreconciled Entries'), function() {
frm.trigger("make_reconciliation_tool");
});
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
after_save: function (frm) {
frm.trigger("make_reconciliation_tool");
},
bank_account: function (frm) {
@@ -96,7 +89,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
r.account,
"account_currency",
(r) => {
frm.doc.account_currency = r.account_currency;
frm.currency = r.account_currency;
frm.trigger("render_chart");
}
);
@@ -162,19 +155,19 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}
},
render_chart(frm) {
render_chart: frappe.utils.debounce((frm) => {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{
$reconciliation_tool_cards: frm.get_field(
"reconciliation_tool_cards"
).$wrapper,
bank_statement_closing_balance:
frm.doc.bank_statement_closing_balance,
frm.doc.bank_statement_closing_balance,
cleared_balance: frm.cleared_balance,
currency: frm.doc.account_currency,
currency: frm.currency,
}
);
},
}, 500),
render(frm) {
if (frm.doc.bank_account) {

View File

@@ -14,7 +14,6 @@
"to_reference_date",
"filter_by_reference_date",
"column_break_2",
"account_currency",
"account_opening_balance",
"bank_statement_closing_balance",
"section_break_1",
@@ -60,7 +59,7 @@
"fieldname": "account_opening_balance",
"fieldtype": "Currency",
"label": "Account Opening Balance",
"options": "account_currency",
"options": "Currency",
"read_only": 1
},
{
@@ -68,7 +67,7 @@
"fieldname": "bank_statement_closing_balance",
"fieldtype": "Currency",
"label": "Closing Balance",
"options": "account_currency"
"options": "Currency"
},
{
"fieldname": "section_break_1",
@@ -105,20 +104,13 @@
"fieldname": "filter_by_reference_date",
"fieldtype": "Check",
"label": "Filter by Reference Date"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Account Currency",
"options": "Currency"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-03-07 11:02:24.535714",
"modified": "2023-01-13 13:00:02.022919",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Reconciliation Tool",

View File

@@ -10,8 +10,7 @@ from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
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
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system,
get_entries,
@@ -29,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
filters = []
filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1])
filters.append(["unallocated_amount", ">", 0.0])
filters.append(["unallocated_amount", ">", 0])
if to_date:
filters.append(["date", "<=", to_date])
if from_date:
@@ -67,7 +66,7 @@ def get_account_balance(bank_account, till_date):
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0.0, 0.0
total_debit, total_credit = 0, 0
for d in data:
total_debit += flt(d.debit)
total_credit += flt(d.credit)
@@ -141,19 +140,17 @@ def create_journal_entry_bts(
second_account
)
)
company = frappe.get_value("Account", company_account, "company")
accounts = []
# Multi Currency?
accounts.append(
{
"account": second_account,
"credit_in_account_currency": bank_transaction.deposit,
"debit_in_account_currency": bank_transaction.withdrawal,
"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
"debit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0
else 0,
"party_type": party_type,
"party": party,
"cost_center": get_default_cost_center(company),
}
)
@@ -161,12 +158,15 @@ def create_journal_entry_bts(
{
"account": company_account,
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal,
"debit_in_account_currency": bank_transaction.deposit,
"cost_center": get_default_cost_center(company),
"credit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0
else 0,
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
}
)
company = frappe.get_value("Account", company_account, "company")
journal_entry_dict = {
"voucher_type": entry_type,
"company": company,
@@ -185,22 +185,16 @@ def create_journal_entry_bts(
journal_entry.insert()
journal_entry.submit()
if bank_transaction.deposit > 0.0:
if bank_transaction.deposit > 0:
paid_amount = bank_transaction.deposit
else:
paid_amount = bank_transaction.withdrawal
vouchers = json.dumps(
[
{
"payment_doctype": "Journal Entry",
"payment_name": journal_entry.name,
"amount": paid_amount,
}
]
[{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
)
return reconcile_vouchers(bank_transaction_name, vouchers)
return reconcile_vouchers(bank_transaction.name, vouchers)
@frappe.whitelist()
@@ -224,7 +218,7 @@ def create_payment_entry_bts(
as_dict=True,
)[0]
paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_value("Account", company_account, "company")
@@ -263,15 +257,9 @@ def create_payment_entry_bts(
payment_entry.submit()
vouchers = json.dumps(
[
{
"payment_doctype": "Payment Entry",
"payment_name": payment_entry.name,
"amount": paid_amount,
}
]
[{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
)
return reconcile_vouchers(bank_transaction_name, vouchers)
return reconcile_vouchers(bank_transaction.name, vouchers)
@frappe.whitelist()
@@ -353,7 +341,59 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers)
company_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0
for voucher in vouchers:
voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
total_amount += get_paid_amount(
frappe._dict(
{
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
}
),
transaction.currency,
company_account,
)
if total_amount > transaction.unallocated_amount:
frappe.throw(
_(
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
)
)
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers:
gl_entry = frappe.db.get_value(
"GL Entry",
dict(
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
),
["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
as_dict=1,
)
gl_amount, transaction_amount = (
(gl_entry.credit, transaction.deposit)
if gl_entry.credit > 0
else (gl_entry.debit, transaction.withdrawal)
)
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
transaction.append(
"payment_entries",
{
"payment_document": voucher["payment_entry"].doctype,
"payment_entry": voucher["payment_entry"].name,
"allocated_amount": allocated_amount,
},
)
transaction.save()
transaction.update_allocations()
return frappe.get_doc("Bank Transaction", bank_transaction_name)
@@ -372,9 +412,9 @@ def get_linked_payments(
bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
)[0]
(gl_account, company) = (bank_account.account, bank_account.company)
(account, company) = (bank_account.account, bank_account.company)
matching = check_matching(
gl_account,
account,
company,
transaction,
document_types,
@@ -384,27 +424,7 @@ def get_linked_payments(
from_reference_date,
to_reference_date,
)
return subtract_allocations(gl_account, matching)
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []
for voucher in vouchers:
rows = get_total_allocated_amount(voucher[1], voucher[2])
amount = None
for row in rows:
if row["gl_account"] == gl_account:
amount = row["total"]
break
if amount:
l = list(voucher)
l[3] -= amount
copied.append(tuple(l))
else:
copied.append(voucher)
return copied
return matching
def check_matching(
@@ -418,7 +438,6 @@ def check_matching(
from_reference_date,
to_reference_date,
):
exact_match = True if "exact_match" in document_types else False
# combine all types of vouchers
subquery = get_queries(
bank_account,
@@ -430,11 +449,10 @@ def check_matching(
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
)
filters = {
"amount": transaction.unallocated_amount,
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
"reference_no": transaction.reference_number,
"party_type": transaction.party_type,
"party": transaction.party,
@@ -443,9 +461,7 @@ def check_matching(
matching_vouchers = []
matching_vouchers.extend(
get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
)
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
for query in subquery:
matching_vouchers.extend(
@@ -467,10 +483,10 @@ def get_queries(
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
):
# get queries to get matching vouchers
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
amount_condition = "=" if "exact_match" in document_types else "<="
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
queries = []
# get matching queries from all the apps
@@ -481,7 +497,7 @@ def get_queries(
company,
transaction,
document_types,
exact_match,
amount_condition,
account_from_to,
from_date,
to_date,
@@ -500,7 +516,7 @@ def get_matching_queries(
company,
transaction,
document_types,
exact_match,
amount_condition,
account_from_to,
from_date,
to_date,
@@ -510,8 +526,8 @@ def get_matching_queries(
):
queries = []
if "payment_entry" in document_types:
query = get_pe_matching_query(
exact_match,
pe_amount_matching = get_pe_matching_query(
amount_condition,
account_from_to,
transaction,
from_date,
@@ -520,11 +536,11 @@ def get_matching_queries(
from_reference_date,
to_reference_date,
)
queries.append(query)
queries.extend([pe_amount_matching])
if "journal_entry" in document_types:
query = get_je_matching_query(
exact_match,
je_amount_matching = get_je_matching_query(
amount_condition,
transaction,
from_date,
to_date,
@@ -532,70 +548,34 @@ def get_matching_queries(
from_reference_date,
to_reference_date,
)
queries.append(query)
queries.extend([je_amount_matching])
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
query = get_si_matching_query(exact_match)
queries.append(query)
if transaction.deposit > 0 and "sales_invoice" in document_types:
si_amount_matching = get_si_matching_query(amount_condition)
queries.extend([si_amount_matching])
if transaction.withdrawal > 0.0:
if transaction.withdrawal > 0:
if "purchase_invoice" in document_types:
query = get_pi_matching_query(exact_match)
queries.append(query)
if "bank_transaction" in document_types:
query = get_bt_matching_query(exact_match, transaction)
queries.append(query)
pi_amount_matching = get_pi_matching_query(amount_condition)
queries.extend([pi_amount_matching])
return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
def get_loan_vouchers(bank_account, transaction, document_types, filters):
vouchers = []
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
if transaction.deposit > 0.0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
return vouchers
def get_bt_matching_query(exact_match, transaction):
# get matching bank transaction query
# find bank transactions in the same bank account with opposite sign
# same bank account must have same company and currency
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
return f"""
SELECT
(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
+ 1) AS rank,
'Bank Transaction' AS doctype,
name,
unallocated_amount AS paid_amount,
reference_number AS reference_no,
date AS reference_date,
party,
party_type,
date AS posting_date,
currency
FROM
`tabBank Transaction`
WHERE
status != 'Reconciled'
AND name != '{transaction.name}'
AND bank_account = '{transaction.bank_account}'
AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
"""
def get_ld_matching_query(bank_account, exact_match, filters):
def get_ld_matching_query(bank_account, amount_condition, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get(
@@ -623,17 +603,17 @@ def get_ld_matching_query(bank_account, exact_match, filters):
.where(loan_disbursement.disbursement_account == bank_account)
)
if exact_match:
if amount_condition:
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
else:
query.where(loan_disbursement.disbursed_amount > 0.0)
query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
vouchers = query.run(as_list=True)
return vouchers
def get_lr_matching_query(bank_account, exact_match, filters):
def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get(
@@ -664,10 +644,10 @@ def get_lr_matching_query(bank_account, exact_match, filters):
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
if exact_match:
if amount_condition:
query.where(loan_repayment.amount_paid == filters.get("amount"))
else:
query.where(loan_repayment.amount_paid > 0.0)
query.where(loan_repayment.amount_paid <= filters.get("amount"))
vouchers = query.run()
@@ -675,7 +655,7 @@ def get_lr_matching_query(bank_account, exact_match, filters):
def get_pe_matching_query(
exact_match,
amount_condition,
account_from_to,
transaction,
from_date,
@@ -685,7 +665,7 @@ def get_pe_matching_query(
to_reference_date,
):
# get matching payment entries query
if transaction.deposit > 0.0:
if transaction.deposit > 0:
currency_field = "paid_to_account_currency as currency"
else:
currency_field = "paid_from_account_currency as currency"
@@ -700,8 +680,7 @@ def get_pe_matching_query(
return f"""
SELECT
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Payment Entry' as doctype,
name,
@@ -715,19 +694,20 @@ def get_pe_matching_query(
FROM
`tabPayment Entry`
WHERE
docstatus = 1
paid_amount {amount_condition} %(amount)s
AND docstatus = 1
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
AND ifnull(clearance_date, '') = ""
AND {account_from_to} = %(bank_account)s
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
{filter_by_date}
{filter_by_reference_no}
order by{order_by}
"""
def get_je_matching_query(
exact_match,
amount_condition,
transaction,
from_date,
to_date,
@@ -739,7 +719,7 @@ def get_je_matching_query(
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
order_by = " je.posting_date"
filter_by_reference_no = ""
@@ -751,29 +731,26 @@ def get_je_matching_query(
return f"""
SELECT
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
+ 1) AS rank ,
'Journal Entry' AS doctype,
'Journal Entry' as doctype,
je.name,
jea.{cr_or_dr}_in_account_currency AS paid_amount,
je.cheque_no AS reference_no,
je.cheque_date AS reference_date,
je.pay_to_recd_from AS party,
jea.{cr_or_dr}_in_account_currency as paid_amount,
je.cheque_no as reference_no,
je.cheque_date as reference_date,
je.pay_to_recd_from as party,
jea.party_type,
je.posting_date,
jea.account_currency AS currency
jea.account_currency as currency
FROM
`tabJournal Entry Account` AS jea
`tabJournal Entry Account` as jea
JOIN
`tabJournal Entry` AS je
`tabJournal Entry` as je
ON
jea.parent = je.name
WHERE
je.docstatus = 1
AND je.voucher_type NOT IN ('Opening Entry')
AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
(je.clearance_date is null or je.clearance_date='0000-00-00')
AND jea.account = %(bank_account)s
AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
AND je.docstatus = 1
{filter_by_date}
{filter_by_reference_no}
@@ -781,12 +758,11 @@ def get_je_matching_query(
"""
def get_si_matching_query(exact_match):
# get matching sales invoice query
def get_si_matching_query(amount_condition):
# get matchin sales invoice query
return f"""
SELECT
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Sales Invoice' as doctype,
si.name,
@@ -804,20 +780,18 @@ def get_si_matching_query(exact_match):
`tabSales Invoice` as si
ON
sip.parent = si.name
WHERE
si.docstatus = 1
AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
AND sip.account = %(bank_account)s
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
AND sip.amount {amount_condition} %(amount)s
AND si.docstatus = 1
"""
def get_pi_matching_query(exact_match):
# get matching purchase invoice query when they are also used as payment entries (is_paid)
def get_pi_matching_query(amount_condition):
# get matching purchase invoice query
return f"""
SELECT
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Purchase Invoice' as doctype,
name,
@@ -831,9 +805,9 @@ def get_pi_matching_query(exact_match):
FROM
`tabPurchase Invoice`
WHERE
docstatus = 1
paid_amount {amount_condition} %(amount)s
AND docstatus = 1
AND is_paid = 1
AND ifnull(clearance_date, '') = ""
AND cash_bank_account = %(bank_account)s
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
AND cash_bank_account = %(bank_account)s
"""

View File

@@ -1,178 +0,0 @@
from typing import Tuple, Union
import frappe
from frappe.utils import flt
from rapidfuzz import fuzz, process
class AutoMatchParty:
"""
Matches by Account/IBAN and then by Party Name/Description sequentially.
Returns when a result is obtained.
Result (if present) is of the form: (Party Type, Party,)
"""
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
def get(self, key):
return self.__dict__.get(key, None)
def match(self) -> Union[Tuple, None]:
result = None
result = AutoMatchbyAccountIBAN(
bank_party_account_number=self.bank_party_account_number,
bank_party_iban=self.bank_party_iban,
deposit=self.deposit,
).match()
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
).match()
return result
class AutoMatchbyAccountIBAN:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
def get(self, key):
return self.__dict__.get(key, None)
def match(self):
if not (self.bank_party_account_number or self.bank_party_iban):
return None
result = self.match_account_in_party()
return result
def match_account_in_party(self) -> Union[Tuple, None]:
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
result = None
parties = get_parties_in_order(self.deposit)
or_filters = self.get_or_filters()
for party in parties:
party_result = frappe.db.get_all(
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
)
if party == "Employee" and not party_result:
# Search in Bank Accounts first for Employee, and then Employee record
if "bank_account_no" in or_filters:
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
party_result = frappe.db.get_all(
party, or_filters=or_filters, pluck="name", limit_page_length=1
)
if party_result:
result = (
party,
party_result[0],
)
break
return result
def get_or_filters(self) -> dict:
or_filters = {}
if self.bank_party_account_number:
or_filters["bank_account_no"] = self.bank_party_account_number
if self.bank_party_iban:
or_filters["iban"] = self.bank_party_iban
return or_filters
class AutoMatchbyPartyNameDescription:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
def get(self, key):
return self.__dict__.get(key, None)
def match(self) -> Union[Tuple, None]:
# fuzzy search by customer/supplier & employee
if not (self.bank_party_name or self.description):
return None
result = self.match_party_name_desc_in_party()
return result
def match_party_name_desc_in_party(self) -> Union[Tuple, None]:
"""Fuzzy search party name and/or description against parties in the system"""
result = None
parties = get_parties_in_order(self.deposit)
for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
for field in ["bank_party_name", "description"]:
if not self.get(field):
continue
result, skip = self.fuzzy_search_and_return_result(party, names, field)
if result or skip:
break
if result or skip:
# Skip If: It was hard to distinguish between close matches and so match is None
# OR if the right match was found
break
return result
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
skip = False
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
party_name, skip = self.process_fuzzy_result(result)
if not party_name:
return None, skip
return (
party,
party_name,
), skip
def process_fuzzy_result(self, result: Union[list, None]):
"""
If there are multiple valid close matches return None as result may be faulty.
Return the result only if one accurate match stands out.
Returns: Result, Skip (whether or not to discontinue matching)
"""
PARTY, SCORE, CUTOFF = 0, 1, 80
if not result or not len(result):
return None, False
first_result = result[0]
if len(result) == 1:
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
second_result = result[1]
if first_result[SCORE] > CUTOFF:
# If multiple matches with the same score, return None but discontinue matching
# Matches were found but were too close to distinguish between
if first_result[SCORE] == second_result[SCORE]:
return None, True
return first_result[PARTY], True
else:
return None, False
def get_parties_in_order(deposit: float) -> list:
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
if flt(deposit) > 0:
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
return parties

View File

@@ -12,13 +12,8 @@ frappe.ui.form.on("Bank Transaction", {
};
});
},
refresh(frm) {
frm.add_custom_button(__('Unreconcile Transaction'), () => {
frm.call('remove_payment_entries')
.then( () => frm.refresh() );
});
},
bank_account: function (frm) {
bank_account: function(frm) {
set_bank_statement_filter(frm);
},
@@ -39,7 +34,6 @@ frappe.ui.form.on("Bank Transaction", {
"Journal Entry",
"Sales Invoice",
"Purchase Invoice",
"Bank Transaction",
];
}
});
@@ -55,7 +49,7 @@ const update_clearance_date = (frm, cdt, cdn) => {
frappe
.xcall(
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
{ doctype: cdt, docname: cdn, bt_name: frm.doc.name }
{ doctype: cdt, docname: cdn }
)
.then((e) => {
if (e == "success") {

View File

@@ -20,11 +20,9 @@
"currency",
"section_break_10",
"description",
"reference_number",
"column_break_10",
"transaction_id",
"transaction_type",
"section_break_14",
"reference_number",
"transaction_id",
"payment_entries",
"section_break_18",
"allocated_amount",
@@ -33,11 +31,7 @@
"unallocated_amount",
"party_section",
"party_type",
"party",
"column_break_3czf",
"bank_party_name",
"bank_party_account_number",
"bank_party_iban"
"party"
],
"fields": [
{
@@ -67,7 +61,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled"
"options": "\nPending\nSettled\nUnreconciled\nReconciled"
},
{
"fieldname": "bank_account",
@@ -196,40 +190,11 @@
"label": "Withdrawal",
"oldfieldname": "credit",
"options": "currency"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "transaction_type",
"fieldtype": "Data",
"label": "Transaction Type",
"length": 50
},
{
"fieldname": "column_break_3czf",
"fieldtype": "Column Break"
},
{
"fieldname": "bank_party_name",
"fieldtype": "Data",
"label": "Party Name/Account Holder (Bank Statement)"
},
{
"fieldname": "bank_party_iban",
"fieldtype": "Data",
"label": "Party IBAN (Bank Statement)"
},
{
"fieldname": "bank_party_account_number",
"fieldtype": "Data",
"label": "Party Account No. (Bank Statement)"
}
],
"is_submittable": 1,
"links": [],
"modified": "2023-06-06 13:58:12.821411",
"modified": "2022-03-21 19:05:04.208222",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -1,6 +1,9 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from functools import reduce
import frappe
from frappe.utils import flt
@@ -15,159 +18,73 @@ class BankTransaction(StatusUpdater):
self.clear_linked_payment_entries()
self.set_status()
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
self.auto_set_party()
_saving_flag = False
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
def on_update_after_submit(self):
"Run on save(). Avoid recursion caused by multiple saves"
if not self._saving_flag:
self._saving_flag = True
self.clear_linked_payment_entries()
self.update_allocations()
self._saving_flag = False
self.update_allocations()
self.clear_linked_payment_entries()
self.set_status(update=True)
def on_cancel(self):
self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True)
def update_allocations(self):
"The doctype does not allow modifications after submission, so write to the db direct"
if self.payment_entries:
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
allocated_amount = reduce(
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
)
else:
allocated_amount = 0.0
allocated_amount = 0
amount = abs(flt(self.withdrawal) - flt(self.deposit))
self.db_set("allocated_amount", flt(allocated_amount))
self.db_set("unallocated_amount", amount - flt(allocated_amount))
self.reload()
self.set_status(update=True)
if allocated_amount:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
frappe.db.set_value(
self.doctype,
self.name,
"unallocated_amount",
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
)
def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if 0.0 >= self.unallocated_amount:
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
else:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
frappe.db.set_value(
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
)
added = False
for voucher in vouchers:
# Can't add same voucher twice
found = False
for pe in self.payment_entries:
if (
pe.payment_document == voucher["payment_doctype"]
and pe.payment_entry == voucher["payment_name"]
):
found = True
if not found:
pe = {
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
"allocated_amount": 0.0, # Temporary
}
child = self.append("payment_entries", pe)
added = True
# runs on_update_after_submit
if added:
self.save()
def allocate_payment_entries(self):
"""Refactored from bank reconciliation tool.
Non-zero allocations must be amended/cleared manually
Get the bank transaction amount (b) and remove as we allocate
For each payment_entry if allocated_amount == 0:
- get the amount already allocated against all transactions (t), need latest date
- get the voucher amount (from gl) (v)
- allocate (a = v - t)
- a = 0: should already be cleared, so clear & remove payment_entry
- 0 < a <= u: allocate a & clear
- 0 < a, a > u: allocate u
- 0 > a: Error: already over-allocated
- clear means: set the latest transaction date as clearance date
"""
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
remaining_amount = self.unallocated_amount
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
self, payment_entry
)
if 0.0 == unallocated_amount:
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
self.db_delete_payment_entry(payment_entry)
elif remaining_amount <= 0.0:
self.db_delete_payment_entry(payment_entry)
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
payment_entry.db_set("allocated_amount", unallocated_amount)
remaining_amount -= unallocated_amount
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
payment_entry.db_set("allocated_amount", remaining_amount)
remaining_amount = 0.0
elif 0.0 > unallocated_amount:
self.db_delete_payment_entry(payment_entry)
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
amount = self.deposit or self.withdrawal
if amount == self.allocated_amount:
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
self.reload()
def db_delete_payment_entry(self, payment_entry):
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
@frappe.whitelist()
def remove_payment_entries(self):
for payment_entry in self.payment_entries:
self.remove_payment_entry(payment_entry)
# runs on_update_after_submit
self.save()
def remove_payment_entry(self, payment_entry):
"Clear payment entry and clearance"
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.remove(payment_entry)
def clear_linked_payment_entries(self, for_cancel=False):
if for_cancel:
for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel)
else:
self.allocate_payment_entries()
for payment_entry in self.payment_entries:
if payment_entry.payment_document == "Sales Invoice":
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
clearance_date = None if for_cancel else self.date
set_voucher_clearance(
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
def clear_simple_entry(self, payment_entry, for_cancel=False):
if payment_entry.payment_document == "Payment Entry":
if (
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
== "Internal Transfer"
):
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
return
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
)
def auto_set_party(self):
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
if self.party_type and self.party:
return
result = AutoMatchParty(
bank_party_account_number=self.bank_party_account_number,
bank_party_iban=self.bank_party_iban,
bank_party_name=self.bank_party_name,
description=self.description,
deposit=self.deposit,
).match()
if result:
party_type, party = result
frappe.db.set_value(
"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
)
def clear_sales_invoice(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
"clearance_date",
clearance_date,
)
@frappe.whitelist()
@@ -176,114 +93,38 @@ def get_doctypes_for_bank_reconciliation():
return frappe.get_hooks("bank_reconciliation_doctypes")
def get_clearance_details(transaction, payment_entry):
"""
There should only be one bank gle for a voucher.
Could be none for a Bank Transaction.
But if a JE, could affect two banks.
Should only clear the voucher if all bank gles are allocated.
"""
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
bt_allocations = get_total_allocated_amount(
payment_entry.payment_document, payment_entry.payment_entry
def get_reconciled_bank_transactions(payment_entry):
reconciled_bank_transactions = frappe.get_all(
"Bank Transaction Payments",
filters={"payment_entry": payment_entry.payment_entry},
fields=["parent"],
)
unallocated_amount = min(
transaction.unallocated_amount,
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
)
unmatched_gles = len(gles)
latest_transaction = transaction
for gle in gles:
if gle["gl_account"] == gl_bank_account:
if gle["amount"] <= 0.0:
frappe.throw(
frappe._("Voucher {0} value is broken: {1}").format(
payment_entry.payment_entry, gle["amount"]
)
)
unmatched_gles -= 1
unallocated_amount = gle["amount"]
for a in bt_allocations:
if a["gl_account"] == gle["gl_account"]:
unallocated_amount = gle["amount"] - a["total"]
if frappe.utils.getdate(transaction.date) < a["latest_date"]:
latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
else:
# Must be a Journal Entry affecting more than one bank
for a in bt_allocations:
if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
unmatched_gles -= 1
return unallocated_amount, unmatched_gles == 0, latest_transaction
return reconciled_bank_transactions
def get_related_bank_gl_entries(doctype, docname):
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
def get_total_allocated_amount(payment_entry):
return frappe.db.sql(
"""
SELECT
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
gle.account AS gl_account
SUM(btp.allocated_amount) as allocated_amount,
bt.name
FROM
`tabGL Entry` gle
`tabBank Transaction Payments` as btp
LEFT JOIN
`tabAccount` ac ON ac.name=gle.account
`tabBank Transaction` bt ON bt.name=btp.parent
WHERE
ac.account_type = 'Bank'
AND gle.voucher_type = %(doctype)s
AND gle.voucher_no = %(docname)s
AND is_cancelled = 0
""",
dict(doctype=doctype, docname=docname),
btp.payment_document = %s
AND
btp.payment_entry = %s
AND
bt.docstatus = 1""",
(payment_entry.payment_document, payment_entry.payment_entry),
as_dict=True,
)
return result
def get_total_allocated_amount(doctype, docname):
"""
Gets the sum of allocations for a voucher on each bank GL account
along with the latest bank transaction name & date
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
"""
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
SELECT total, latest_name, latest_date, gl_account FROM (
SELECT
ROW_NUMBER() OVER w AS rownum,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
FIRST_VALUE(bt.name) OVER w AS latest_name,
FIRST_VALUE(bt.date) OVER w AS latest_date,
ba.account AS gl_account
FROM
`tabBank Transaction Payments` btp
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
WHERE
btp.payment_document = %(doctype)s
AND btp.payment_entry = %(docname)s
AND bt.docstatus = 1
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
) temp
WHERE
rownum = 1
""",
dict(doctype=doctype, docname=docname),
as_dict=True,
)
for row in result:
# Why is this *sometimes* a byte string?
if isinstance(row["latest_name"], bytes):
row["latest_name"] = row["latest_name"].decode()
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
return result
def get_paid_amount(payment_entry, currency, gl_bank_account):
def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
@@ -304,13 +145,10 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
)
elif payment_entry.payment_document == "Journal Entry":
return abs(
frappe.db.get_value(
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
"sum(debit_in_account_currency-credit_in_account_currency)",
)
or 0
return frappe.db.get_value(
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": bank_account},
"sum(credit_in_account_currency)",
)
elif payment_entry.payment_document == "Expense Claim":
@@ -328,12 +166,6 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
)
elif payment_entry.payment_document == "Bank Transaction":
dep, wth = frappe.db.get_value(
"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
)
return abs(flt(wth) - flt(dep))
else:
frappe.throw(
"Please reconcile {0}: {1} manually".format(
@@ -342,55 +174,18 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
)
def set_voucher_clearance(doctype, docname, clearance_date, self):
if doctype in [
"Payment Entry",
"Journal Entry",
"Purchase Invoice",
"Expense Claim",
"Loan Repayment",
"Loan Disbursement",
]:
if (
doctype == "Payment Entry"
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
):
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=doctype, parent=docname),
"clearance_date",
clearance_date,
)
elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund
bt = frappe.get_doc(doctype, docname)
if clearance_date:
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
bt.add_payment_entries(vouchers)
else:
for pe in bt.payment_entries:
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
bt.remove(pe)
bt.save()
break
def get_reconciled_bank_transactions(doctype, docname):
return frappe.get_all(
"Bank Transaction Payments",
filters={"payment_document": doctype, "payment_entry": docname},
pluck="parent",
)
@frappe.whitelist()
def unclear_reference_payment(doctype, docname, bt_name):
bt = frappe.get_doc("Bank Transaction", bt_name)
set_voucher_clearance(doctype, docname, None, bt)
return docname
def unclear_reference_payment(doctype, docname):
if frappe.db.exists(doctype, docname):
doc = frappe.get_doc(doctype, docname)
if doctype == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=doc.payment_document, parent=doc.payment_entry),
"clearance_date",
None,
)
else:
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
return doc.payment_entry

View File

@@ -1,151 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
class TestAutoMatchParty(FrappeTestCase):
@classmethod
def setUpClass(cls):
create_bank_account()
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1)
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1)
return super().setUpClass()
@classmethod
def tearDownClass(cls):
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0)
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
def test_match_by_account_number(self):
create_supplier_for_match(account_no="000000003716541159")
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
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="DE02000000003716541159")
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="c5455a224602afaa51592a9d9250600d",
account_no="000000003716541159",
iban="DE02000000003716541159",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "John Doe & Co.")
def test_match_by_party_name(self):
create_supplier_for_match(supplier_name="Jackson Ella W.")
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
party_name="Ella Jackson",
iban="DE04000000003716545346",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "Jackson Ella W.")
def test_match_by_description(self):
create_supplier_for_match(supplier_name="Microsoft")
doc = create_bank_transaction(
description="Auftraggeber: microsoft payments Buchungstext: msft ..e3006b5hdy. ref. j375979555927627/5536",
withdrawal=1200,
transaction_id="8df880a2d09c3bed3fea358ca5168c5a",
party_name="",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "Microsoft")
def test_skip_match_if_multiple_close_results(self):
create_supplier_for_match(supplier_name="Adithya Medical & General Stores")
create_supplier_for_match(supplier_name="Adithya Medical And General Stores")
doc = create_bank_transaction(
description="Paracetamol Consignment, SINV-0009",
withdrawal=24.85,
transaction_id="3a1da4ee2dc5a980138d56ef3460cbd9",
party_name="Adithya Medical & General",
)
# Mapping is skipped as both Supplier names have the same match score
self.assertEqual(doc.party_type, None)
self.assertEqual(doc.party, None)
def create_supplier_for_match(supplier_name="John Doe & Co.", iban=None, account_no=None):
if frappe.db.exists("Supplier", {"supplier_name": supplier_name}):
# Update related Bank Account details
if not (iban or account_no):
return
frappe.db.set_value(
dt="Bank Account",
dn={"party": supplier_name},
field={"iban": iban, "bank_account_no": account_no},
)
return
# Create Supplier and Bank Account for the same
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.supplier_group = "Services"
supplier.supplier_type = "Company"
supplier.insert()
if not frappe.db.exists("Bank", "TestBank"):
bank = frappe.new_doc("Bank")
bank.bank_name = "TestBank"
bank.insert(ignore_if_duplicate=True)
if not frappe.db.exists("Bank Account", supplier.name + " - " + "TestBank"):
bank_account = frappe.new_doc("Bank Account")
bank_account.account_name = supplier.name
bank_account.bank = "TestBank"
bank_account.iban = iban
bank_account.bank_account_no = account_no
bank_account.party_type = "Supplier"
bank_account.party = supplier.name
bank_account.insert()
def create_bank_transaction(
description=None,
withdrawal=0,
deposit=0,
transaction_id=None,
party_name=None,
account_no=None,
iban=None,
):
doc = frappe.new_doc("Bank Transaction")
doc.update(
{
"doctype": "Bank Transaction",
"description": description or "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
"date": nowdate(),
"withdrawal": withdrawal,
"deposit": deposit,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank",
"transaction_id": transaction_id,
"bank_party_name": party_name,
"bank_party_account_number": account_no,
"bank_party_iban": iban,
}
)
doc.insert()
doc.submit()
doc.reload()
return doc

View File

@@ -125,27 +125,14 @@ def validate_expense_against_budget(args, expense_amount=0):
if not args.account:
return
default_dimensions = [
{
"fieldname": "project",
"document_type": "Project",
},
{
"fieldname": "cost_center",
"document_type": "Cost Center",
},
]
for dimension in default_dimensions + get_accounting_dimensions(as_list=False):
budget_against = dimension.get("fieldname")
for budget_against in ["project", "cost_center"] + get_accounting_dimensions():
if (
args.get(budget_against)
and args.account
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
):
doctype = dimension.get("document_type")
doctype = frappe.unscrub(budget_against)
if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])

View File

@@ -325,14 +325,14 @@ def get_template(template_type):
if template_type == "Blank Template":
for root_type in get_root_types():
writer.writerow(["", "", "", "", 1, "", root_type])
writer.writerow(["", "", "", 1, "", root_type])
for account in get_mandatory_group_accounts():
writer.writerow(["", "", "", "", 1, account, "Asset"])
writer.writerow(["", "", "", 1, account, "Asset"])
for account_type in get_mandatory_account_types():
writer.writerow(
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
)
else:
writer = get_sample_template(writer)

View File

@@ -35,21 +35,6 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
}
},
validate_rounding_loss: function(frm) {
let allowance = frm.doc.rounding_loss_allowance;
if (!(allowance >= 0 && allowance < 1)) {
frappe.throw(__("Rounding Loss Allowance should be between 0 and 1"));
}
},
rounding_loss_allowance: function(frm) {
frm.events.validate_rounding_loss(frm);
},
validate: function(frm) {
frm.events.validate_rounding_loss(frm);
},
get_entries: function(frm, account) {
frappe.call({
method: "get_accounts_data",
@@ -141,8 +126,7 @@ var get_account_details = function(frm, cdt, cdn) {
company: frm.doc.company,
posting_date: frm.doc.posting_date,
party_type: row.party_type,
party: row.party,
rounding_loss_allowance: frm.doc.rounding_loss_allowance
party: row.party
},
callback: function(r){
$.extend(row, r.message);

View File

@@ -8,7 +8,6 @@
"engine": "InnoDB",
"field_order": [
"posting_date",
"rounding_loss_allowance",
"column_break_2",
"company",
"section_break_4",
@@ -97,19 +96,11 @@
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"default": "0.05",
"description": "Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...}\nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account",
"fieldname": "rounding_loss_allowance",
"fieldtype": "Float",
"label": "Rounding Loss Allowance",
"precision": "9"
}
],
"is_submittable": 1,
"links": [],
"modified": "2023-06-20 07:29:06.972434",
"modified": "2022-12-29 19:38:24.416529",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation",

View File

@@ -12,19 +12,13 @@ from frappe.utils import flt, get_link_to_form
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
from erpnext.accounts.utils import get_currency_precision
from erpnext.setup.utils import get_exchange_rate
class ExchangeRateRevaluation(Document):
def validate(self):
self.validate_rounding_loss_allowance()
self.set_total_gain_loss()
def validate_rounding_loss_allowance(self):
if not (self.rounding_loss_allowance >= 0 and self.rounding_loss_allowance < 1):
frappe.throw(_("Rounding Loss Allowance should be between 0 and 1"))
def set_total_gain_loss(self):
total_gain_loss = 0
@@ -93,22 +87,11 @@ class ExchangeRateRevaluation(Document):
return True
def fetch_and_calculate_accounts_data(self):
accounts = self.get_accounts_data()
if accounts:
for acc in accounts:
self.append("accounts", acc)
@frappe.whitelist()
def get_accounts_data(self):
self.validate_mandatory()
account_details = self.get_account_balance_from_gle(
company=self.company,
posting_date=self.posting_date,
account=None,
party_type=None,
party=None,
rounding_loss_allowance=self.rounding_loss_allowance,
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
)
accounts_with_new_balance = self.calculate_new_account_balance(
self.company, self.posting_date, account_details
@@ -120,9 +103,7 @@ class ExchangeRateRevaluation(Document):
return accounts_with_new_balance
@staticmethod
def get_account_balance_from_gle(
company, posting_date, account, party_type, party, rounding_loss_allowance
):
def get_account_balance_from_gle(company, posting_date, account, party_type, party):
account_details = []
if company and posting_date:
@@ -189,23 +170,6 @@ class ExchangeRateRevaluation(Document):
.run(as_dict=True)
)
# round off balance based on currency precision
# and consider debit-credit difference allowance
currency_precision = get_currency_precision()
rounding_loss_allowance = float(rounding_loss_allowance) or 0.05
for acc in account_details:
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
if abs(acc.balance_in_account_currency) <= rounding_loss_allowance:
acc.balance_in_account_currency = 0
acc.balance = flt(acc.balance, currency_precision)
if abs(acc.balance) <= rounding_loss_allowance:
acc.balance = 0
acc.zero_balance = (
True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False
)
return account_details
@staticmethod
@@ -258,8 +222,8 @@ class ExchangeRateRevaluation(Document):
new_balance_in_base_currency = 0
new_balance_in_account_currency = 0
current_exchange_rate = (
calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) or 0.0
current_exchange_rate = calculate_exchange_rate_using_last_gle(
company, d.account, d.party_type, d.party
)
gain_loss = new_balance_in_account_currency - (
@@ -379,24 +343,6 @@ class ExchangeRateRevaluation(Document):
"credit": 0,
}
)
journal_entry_accounts.append(journal_account)
journal_entry_accounts.append(
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit": 0,
"credit": 0,
"debit_in_account_currency": abs(d.gain_loss) if d.gain_loss < 0 else 0,
"credit_in_account_currency": abs(d.gain_loss) if d.gain_loss > 0 else 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
# Base currency has balance
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
@@ -412,22 +358,22 @@ class ExchangeRateRevaluation(Document):
}
)
journal_entry_accounts.append(journal_account)
journal_entry_accounts.append(journal_account)
journal_entry_accounts.append(
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit": abs(d.gain_loss) if d.gain_loss < 0 else 0,
"credit": abs(d.gain_loss) if d.gain_loss > 0 else 0,
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
journal_entry_accounts.append(
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_total_debit_credit()
@@ -544,8 +490,6 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
conditions.append(gl.company == company)
conditions.append(gl.account == account)
conditions.append(gl.is_cancelled == 0)
conditions.append((gl.debit > 0) | (gl.credit > 0))
conditions.append((gl.debit_in_account_currency > 0) | (gl.credit_in_account_currency > 0))
if party_type:
conditions.append(gl.party_type == party_type)
if party:
@@ -575,9 +519,7 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
@frappe.whitelist()
def get_account_details(
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float = None
):
def get_account_details(company, posting_date, account, party_type=None, party=None):
if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory"))
@@ -595,12 +537,7 @@ def get_account_details(
"account_currency": account_currency,
}
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
company=company,
posting_date=posting_date,
account=account,
party_type=party_type,
party=party,
rounding_loss_allowance=rounding_loss_allowance,
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
)
if account_balance and (

View File

@@ -73,7 +73,6 @@
"fieldname": "current_exchange_rate",
"fieldtype": "Float",
"label": "Current Exchange Rate",
"precision": "9",
"read_only": 1
},
{
@@ -93,7 +92,6 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "New Exchange Rate",
"precision": "9",
"reqd": 1
},
{
@@ -149,7 +147,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-06-22 12:39:56.446722",
"modified": "2022-12-29 19:38:52.915295",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation Account",

View File

@@ -8,6 +8,17 @@ frappe.ui.form.on('Fiscal Year', {
frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1));
}
},
refresh: function (frm) {
if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) {
frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm));
frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'"));
} else {
frm.set_intro("");
}
},
set_as_default: function(frm) {
return frm.call('set_as_default');
},
year_start_date: function(frm) {
if (!frm.doc.is_short_year) {
let year_end_date =

View File

@@ -4,7 +4,7 @@
import frappe
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate
@@ -14,6 +14,22 @@ class FiscalYearIncorrectDate(frappe.ValidationError):
class FiscalYear(Document):
@frappe.whitelist()
def set_as_default(self):
frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name)
global_defaults = frappe.get_doc("Global Defaults")
global_defaults.check_permission("write")
global_defaults.on_update()
# clear cache
frappe.clear_cache()
msgprint(
_(
"{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect."
).format(self.name)
)
def validate(self):
self.validate_dates()
self.validate_overlap()
@@ -61,6 +77,13 @@ class FiscalYear(Document):
frappe.cache().delete_value("fiscal_years")
def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name:
frappe.throw(
_(
"You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings"
).format(self.name)
)
frappe.cache().delete_value("fiscal_years")
def validate_overlap(self):

View File

@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
},
refresh: function(frm) {

View File

@@ -51,7 +51,7 @@ class JournalEntry(AccountsController):
self.validate_multi_currency()
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
self.set_total_debit_credit()
# Do not validate while importing via data import
if not frappe.flags.in_import:
self.validate_total_debit_and_credit()
@@ -69,7 +69,6 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table()
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
if self.docstatus == 0:
self.apply_tax_withholding()
@@ -131,13 +130,6 @@ class JournalEntry(AccountsController):
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_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):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
@@ -241,30 +233,25 @@ class JournalEntry(AccountsController):
self.remove(d)
def update_asset_value(self):
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if (
d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and d.debit
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
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)
continue
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
asset.set_status()
@@ -326,45 +313,38 @@ class JournalEntry(AccountsController):
d.db_update()
def unlink_asset_reference(self):
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if (
self.voucher_type == "Depreciation Entry"
and d.reference_type == "Asset"
and d.reference_name
and d.account_type == "Depreciation"
and d.debit
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
fb_idx = None
for s in asset.get("schedules"):
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
fb_idx = cint(s.finance_book_id) or 1
break
if not fb_idx:
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.set_status()
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"
)
if journal_entry_for_scrap == self.name:
frappe.throw(
_("Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.")
)
idx = cint(s.finance_book_id) or 1
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
asset.set_status()
break
else:
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
asset.set_status()
def unlink_inter_company_jv(self):
if (
@@ -396,15 +376,6 @@ class JournalEntry(AccountsController):
d.idx, d.account
)
)
elif (
d.party_type
and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type
):
frappe.throw(
_("Row {0}: Account {1} and Party Type {2} have different account types").format(
d.idx, d.account, d.party_type
)
)
def check_credit_limit(self):
customers = list(
@@ -688,6 +659,7 @@ class JournalEntry(AccountsController):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
if self.difference:
frappe.throw(
@@ -907,8 +879,6 @@ 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.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"):
update_outstanding = "No"
@@ -916,13 +886,7 @@ class JournalEntry(AccountsController):
update_outstanding = "Yes"
if gl_map:
make_gl_entries(
gl_map,
cancel=cancel,
adv_adj=adv_adj,
merge_entries=merge_entries,
update_outstanding=update_outstanding,
)
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist()
def get_balance(self, difference_account=None):
@@ -956,7 +920,6 @@ class JournalEntry(AccountsController):
blank_row.debit_in_account_currency = abs(diff)
blank_row.debit = abs(diff)
self.set_total_debit_credit()
self.validate_total_debit_and_credit()
@frappe.whitelist()

View File

@@ -287,6 +287,10 @@ class TestJournalEntry(unittest.TestCase):
jv.submit()
def test_inter_company_jv(self):
frappe.db.set_value("Account", "Sales Expenses - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1)
jv = make_journal_entry(
"Sales Expenses - _TC",
"Buildings - _TC",

View File

@@ -2,21 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on("Journal Entry Template", {
onload: function(frm) {
if(frm.is_new()) {
frappe.call({
type: "GET",
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
callback: function(r){
if(r.message) {
frm.set_df_property("naming_series", "options", r.message.split("\n"));
frm.set_value("naming_series", r.message.split("\n")[0]);
frm.refresh_field("naming_series");
}
}
});
}
},
refresh: function(frm) {
frappe.model.set_default_values(frm.doc);
@@ -34,6 +19,18 @@ frappe.ui.form.on("Journal Entry Template", {
return { filters: filters };
});
frappe.call({
type: "GET",
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
callback: function(r){
if(r.message){
frm.set_df_property("naming_series", "options", r.message.split("\n"));
frm.set_value("naming_series", r.message.split("\n")[0]);
frm.refresh_field("naming_series");
}
}
});
},
voucher_type: function(frm) {
var add_accounts = function(doc, r) {

View File

@@ -141,7 +141,7 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
)
if points_to_redeem > loyalty_program_details.loyalty_points:
frappe.throw(_("You don't have enough Loyalty Points to redeem"))
frappe.throw(_("You don't have enought Loyalty Points to redeem"))
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)

View File

@@ -122,10 +122,13 @@ frappe.ui.form.on('Payment Entry', {
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
const child = locals[cdt][cdn];
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name});
payment_term_list = payment_term_list.map(pt => pt.payment_term);
return {
query: "erpnext.controllers.queries.get_payment_terms_for_references",
filters: {
'reference': child.reference_name
'name': ['in', payment_term_list]
}
}
}
@@ -242,6 +245,8 @@ frappe.ui.form.on('Payment Entry', {
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
party_account_currency, "references");
frm.set_currency_labels(["amount"], company_currency, "deductions");
cur_frm.set_df_property("source_exchange_rate", "description",
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
@@ -610,7 +615,7 @@ frappe.ui.form.on('Payment Entry', {
frm.events.set_unallocated_amount(frm);
},
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
get_outstanding_invoice: function(frm) {
const today = frappe.datetime.get_today();
const fields = [
{fieldtype:"Section Break", label: __("Posting Date")},
@@ -640,29 +645,12 @@ frappe.ui.form.on('Payment Entry', {
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
];
let btn_text = "";
if (get_outstanding_invoices) {
btn_text = "Get Outstanding Invoices";
}
else if (get_orders_to_be_billed) {
btn_text = "Get Outstanding Orders";
}
frappe.prompt(fields, function(filters){
frappe.flags.allocate_payment_amount = true;
frm.events.validate_filters_data(frm, filters);
frm.doc.cost_center = filters.cost_center;
frm.events.get_outstanding_documents(frm, filters, get_outstanding_invoices, get_orders_to_be_billed);
}, __("Filters"), __(btn_text));
},
get_outstanding_invoices: function(frm) {
frm.events.get_outstanding_invoices_or_orders(frm, true, false);
},
get_outstanding_orders: function(frm) {
frm.events.get_outstanding_invoices_or_orders(frm, false, true);
frm.events.get_outstanding_documents(frm, filters);
}, __("Filters"), __("Get Outstanding Documents"));
},
validate_filters_data: function(frm, filters) {
@@ -688,7 +676,7 @@ frappe.ui.form.on('Payment Entry', {
}
},
get_outstanding_documents: function(frm, filters, get_outstanding_invoices, get_orders_to_be_billed) {
get_outstanding_documents: function(frm, filters) {
frm.clear_table("references");
if(!frm.doc.party) {
@@ -712,13 +700,6 @@ frappe.ui.form.on('Payment Entry', {
args[key] = filters[key];
}
if (get_outstanding_invoices) {
args["get_outstanding_invoices"] = true;
}
else if (get_orders_to_be_billed) {
args["get_orders_to_be_billed"] = true;
}
frappe.flags.allocate_payment_amount = filters['allocate_payment_amount'];
return frappe.call({
@@ -894,12 +875,12 @@ frappe.ui.form.on('Payment Entry', {
if(frm.doc.payment_type == "Receive"
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges)
unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
} else if (frm.doc.payment_type == "Pay"
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {
unallocated_amount = (frm.doc.base_paid_amount + flt(frm.doc.base_total_taxes_and_charges) - (total_deductions
unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
}
}
@@ -926,7 +907,7 @@ frappe.ui.form.on('Payment Entry', {
function(d) { return flt(d.amount) }));
frm.set_value("difference_amount", difference_amount - total_deductions +
flt(frm.doc.base_total_taxes_and_charges));
frm.doc.base_total_taxes_and_charges);
frm.events.hide_unhide_fields(frm);
},
@@ -992,49 +973,30 @@ frappe.ui.form.on('Payment Entry', {
},
callback: function(r, rt) {
if(r.message) {
const write_off_row = $.map(frm.doc["deductions"] || [], function(t) {
var write_off_row = $.map(frm.doc["deductions"] || [], function(t) {
return t.account==r.message[account] ? t : null; });
const difference_amount = flt(frm.doc.difference_amount,
var row = [];
var difference_amount = flt(frm.doc.difference_amount,
precision("difference_amount"));
const add_deductions = (details) => {
let row = null;
if (!write_off_row.length && difference_amount) {
row = frm.add_child("deductions");
row.account = details[account];
row.cost_center = details["cost_center"];
} else {
row = write_off_row[0];
}
if (row) {
row.amount = flt(row.amount) + difference_amount;
} else {
frappe.msgprint(__("No gain or loss in the exchange rate"))
}
refresh_field("deductions");
};
if (!r.message[account]) {
frappe.prompt({
label: __("Please Specify Account"),
fieldname: account,
fieldtype: "Link",
options: "Account",
get_query: () => ({
filters: {
company: frm.doc.company,
}
})
}, (values) => {
const details = Object.assign({}, r.message, values);
add_deductions(details);
}, __(frappe.unscrub(account)));
if (!write_off_row.length && difference_amount) {
row = frm.add_child("deductions");
row.account = r.message[account];
row.cost_center = r.message["cost_center"];
} else {
add_deductions(r.message);
row = write_off_row[0];
}
if (row) {
row.amount = flt(row.amount) + difference_amount;
} else {
frappe.msgprint(__("No gain or loss in the exchange rate"))
}
refresh_field("deductions");
frm.events.set_unallocated_amount(frm);
}
}

View File

@@ -48,8 +48,7 @@
"base_received_amount",
"base_received_amount_after_tax",
"section_break_14",
"get_outstanding_invoices",
"get_outstanding_orders",
"get_outstanding_invoice",
"references",
"section_break_34",
"total_allocated_amount",
@@ -356,6 +355,12 @@
"fieldtype": "Section Break",
"label": "Reference"
},
{
"depends_on": "eval:doc.docstatus==0",
"fieldname": "get_outstanding_invoice",
"fieldtype": "Button",
"label": "Get Outstanding Invoice"
},
{
"fieldname": "references",
"fieldtype": "Table",
@@ -723,24 +728,12 @@
"fieldname": "section_break_60",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"depends_on": "eval:doc.docstatus==0",
"fieldname": "get_outstanding_invoices",
"fieldtype": "Button",
"label": "Get Outstanding Invoices"
},
{
"depends_on": "eval:doc.docstatus==0",
"fieldname": "get_outstanding_orders",
"fieldtype": "Button",
"label": "Get Outstanding Orders"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-06-19 11:38:04.387219",
"modified": "2023-02-14 04:52:30.478523",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

File diff suppressed because it is too large Load Diff

View File

@@ -5,13 +5,12 @@ import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
InvalidPaymentEntry,
get_payment_entry,
get_reference_details,
)
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
make_purchase_invoice,
@@ -52,38 +51,6 @@ class TestPaymentEntry(FrappeTestCase):
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 0)
def test_payment_against_sales_order_usd_to_inr(self):
so = make_sales_order(
customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
)
so.conversion_rate = 50
so.submit()
pe = get_payment_entry("Sales Order", so.name)
pe.source_exchange_rate = 55
pe.received_amount = 5500
pe.insert()
pe.submit()
# there should be no difference amount
pe.reload()
self.assertEqual(pe.difference_amount, 0)
self.assertEqual(pe.deductions, [])
expected_gle = dict(
(d[0], d)
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]]
)
self.validate_gl_entries(pe.name, expected_gle)
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 100)
pe.cancel()
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
self.assertEqual(so_advance_paid, 0)
def test_payment_entry_for_blocked_supplier_invoice(self):
supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
@@ -289,25 +256,10 @@ class TestPaymentEntry(FrappeTestCase):
},
)
si.save()
si.submit()
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].allocated_amount, 236.0)
self.assertEqual(pe.paid_amount, 212.4)
self.assertEqual(pe.deductions[0].amount, 23.6)
pe.submit()
si.load_from_db()
@@ -317,190 +269,6 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
def test_payment_entry_against_payment_terms_with_discount_amount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
si.payment_terms_template = "Test Discount Amount Template"
create_payment_terms_template_with_discount(
name="30 Credit Days with Rs.50 Discount",
discount_type="Amount",
discount=50,
template_name="Test Discount Amount Template",
)
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 18,
},
)
si.save()
si.submit()
# Set reference date past discount cut off date
pe_1 = get_payment_entry(
"Sales Invoice",
si.name,
bank_account="_Test Cash - _TC",
reference_date=frappe.utils.add_days(si.posting_date, 2),
)
self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
# Test if tax loss is booked on enabling configuration
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].allocated_amount, 236.0)
self.assertEqual(pe.paid_amount, 186)
self.assertEqual(pe.deductions[0].amount, 50.0)
pe.submit()
si.load_from_db()
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 186)
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
@change_settings(
"Accounts Settings",
{
"allow_multi_currency_invoices_against_single_party_account": 1,
"book_tax_discount_loss": 1,
},
)
def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
self,
):
"""
1. Multi-currency SI with single currency accounting (company currency)
2. PE with early payment discount
3. Test if Paid Amount is calculated in company currency
4. Test if deductions are calculated in company currency
SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
"""
si = create_sales_invoice(
customer="_Test Customer",
currency="USD",
conversion_rate=50,
do_not_save=1,
)
create_payment_terms_template_with_discount()
si.payment_terms_template = "Test Discount Template"
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.save()
si.submit()
pe = get_payment_entry(
"Sales Invoice",
si.name,
bank_account="_Test Bank - _TC",
)
pe.reference_no = si.name
pe.reference_date = nowdate()
# Early payment discount loss on income
self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
self.assertEqual(pe.received_amount, 4500.0)
self.assertEqual(pe.deductions[0].amount, 500.0)
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
self.assertEqual(pe.difference_amount, 0.0)
pe.insert()
pe.submit()
expected_gle = dict(
(d[0], d)
for d in [
["Debtors - _TC", 0, 5000, si.name],
["_Test Bank - _TC", 4500, 0, None],
["Write Off - _TC", 500.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
"""
1. Multi-currency SI with multi-currency accounting
2. PE with early payment discount and also exchange loss
3. Test if Paid Amount is calculated in transaction currency
4. Test if deductions are calculated in base/company currency
5. Test if exchange loss is reflected in difference
"""
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_save=1,
)
create_payment_terms_template_with_discount()
si.payment_terms_template = "Test Discount Template"
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.save()
si.submit()
pe = get_payment_entry(
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
)
pe.reference_no = si.name
pe.reference_date = nowdate()
# Early payment discount loss on income
self.assertEqual(pe.paid_amount, 90.0)
self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
self.assertEqual(pe.deductions[0].amount, 500.0)
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
# Exchange loss
self.assertEqual(pe.difference_amount, 300.0)
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 300.0,
},
)
pe.insert()
pe.submit()
self.assertEqual(pe.difference_amount, 0.0)
expected_gle = dict(
(d[0], d)
for d in [
["_Test Receivable USD - _TC", 0, 5000, si.name],
["_Test Bank - _TC", 4200, 0, None],
["Write Off - _TC", 500.0, 0, None],
["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -1014,194 +782,6 @@ class TestPaymentEntry(FrappeTestCase):
employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
create_payment_entry(party_type="Employee", party=employee, save=True)
def test_duplicate_payment_entry_allocate_amount(self):
si = create_sales_invoice()
pe_draft = get_payment_entry("Sales Invoice", si.name)
pe_draft.insert()
pe = get_payment_entry("Sales Invoice", si.name)
pe.submit()
self.assertRaises(frappe.ValidationError, pe_draft.submit)
def test_duplicate_payment_entry_partial_allocate_amount(self):
si = create_sales_invoice()
pe_draft = get_payment_entry("Sales Invoice", si.name)
pe_draft.insert()
pe = get_payment_entry("Sales Invoice", si.name)
pe.received_amount = si.total / 2
pe.references[0].allocated_amount = si.total / 2
pe.submit()
self.assertRaises(frappe.ValidationError, pe_draft.submit)
def test_details_update_on_reference_table(self):
so = make_sales_order(
customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
)
so.conversion_rate = 50
so.submit()
pe = get_payment_entry("Sales Order", so.name)
pe.references.clear()
pe.paid_from = "Debtors - _TC"
pe.paid_from_account_currency = "INR"
pe.source_exchange_rate = 50
pe.save()
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
expected_response = {
"total_amount": 5000.0,
"outstanding_amount": 5000.0,
"exchange_rate": 1.0,
"due_date": None,
"bill_no": None,
}
self.assertDictEqual(ref_details, expected_response)
@change_settings(
"Accounts Settings",
{
"unlink_payment_on_cancellation_of_invoice": 1,
"delete_linked_ledger_entries": 1,
"allow_multi_currency_invoices_against_single_party_account": 1,
},
)
def test_overallocation_validation_on_payment_terms(self):
"""
Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown.
"""
customer = create_customer()
create_payment_terms_template()
# Validate allocation on base/company currency
si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200)
si1.payment_terms_template = "Test Receivable Template"
si1.save().submit()
si1.reload()
pe = get_payment_entry(si1.doctype, si1.name).save()
# Allocated amount should be according to the payment schedule
for idx, schedule in enumerate(si1.payment_schedule):
with self.subTest(idx=idx):
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 400
pe.references[0].allocated_amount = 200
pe.references[1].allocated_amount = 200
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si1.cancel()
si1.delete()
# Validate allocation on foreign currency
si2 = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=80,
do_not_save=1,
)
si2.payment_terms_template = "Test Receivable Template"
si2.save().submit()
si2.reload()
pe = get_payment_entry(si2.doctype, si2.name).save()
# Allocated amount should be according to the payment schedule
for idx, schedule in enumerate(si2.payment_schedule):
with self.subTest(idx=idx):
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 200
pe.references[0].allocated_amount = 100
pe.references[1].allocated_amount = 100
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si2.cancel()
si2.delete()
# Validate allocation in base/company currency on a foreign currency document
# when invoice is made is foreign currency, but posted to base/company currency debtors account
si3 = create_sales_invoice(
customer=customer,
currency="USD",
conversion_rate=80,
do_not_save=1,
)
si3.payment_terms_template = "Test Receivable Template"
si3.save().submit()
si3.reload()
pe = get_payment_entry(si3.doctype, si3.name).save()
# Allocated amount should be equal to payment term outstanding
self.assertEqual(len(pe.references), 2)
for idx, ref in enumerate(pe.references):
with self.subTest(idx=idx):
self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount)
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 16000
pe.references[0].allocated_amount = 8000
pe.references[1].allocated_amount = 8000
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si3.cancel()
si3.delete()
@change_settings(
"Accounts Settings",
{
"unlink_payment_on_cancellation_of_invoice": 1,
"delete_linked_ledger_entries": 1,
"allow_multi_currency_invoices_against_single_party_account": 1,
},
)
def test_overallocation_validation_shouldnt_misfire(self):
"""
Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled
"""
customer = create_customer()
create_payment_terms_template()
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
template.allocate_payment_based_on_payment_terms = 0
template.save()
# Validate allocation on base/company currency
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
si.payment_terms_template = "Test Receivable Template"
si.save().submit()
si.reload()
pe = get_payment_entry(si.doctype, si.name).save()
# There will no term based allocation
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.references[0].payment_term, None)
self.assertEqual(flt(pe.references[0].allocated_amount), flt(si.grand_total))
pe.save()
# specify a term
pe.references[0].payment_term = template.terms[0].payment_term
# no validation error should be thrown
pe.save()
pe.paid_amount = si.grand_total + 1
pe.references[0].allocated_amount = si.grand_total + 1
self.assertRaises(frappe.ValidationError, pe.save)
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
template.allocate_payment_based_on_payment_terms = 1
template.save()
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
@@ -1259,27 +839,24 @@ def create_payment_terms_template():
).insert()
def create_payment_terms_template_with_discount(
name=None, discount_type=None, discount=None, template_name=None
):
create_payment_term(name or "30 Credit Days with 10% Discount")
template_name = template_name or "Test Discount Template"
def create_payment_terms_template_with_discount():
if not frappe.db.exists("Payment Terms Template", template_name):
frappe.get_doc(
create_payment_term("30 Credit Days with 10% Discount")
if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
payment_term_template = frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": template_name,
"template_name": "Test Discount Template",
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": name or "30 Credit Days with 10% Discount",
"payment_term": "30 Credit Days with 10% Discount",
"invoice_portion": 100,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 2,
"discount_type": discount_type or "Percentage",
"discount": discount or 10,
"discount": 10,
"discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 1,
}
@@ -1291,17 +868,3 @@ def create_payment_terms_template_with_discount(
def create_payment_term(name):
if not frappe.db.exists("Payment Term", name):
frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
def create_customer(name="_Test Customer 2 USD", currency="USD"):
customer = None
if frappe.db.exists("Customer", name):
customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.default_currency = currency
customer.type = "Individual"
customer.save()
customer = customer.name
return customer

View File

@@ -3,7 +3,6 @@
"creation": "2016-06-15 15:56:30.815503",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account",
"cost_center",
@@ -18,7 +17,9 @@
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "cost_center",
@@ -27,30 +28,37 @@
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1,
"reqd": 1
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount (Company Currency)",
"options": "Company:company:default_currency",
"reqd": 1
"label": "Amount",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
"label": "Description",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-03-06 07:11:57.739619",
"modified": "2020-09-12 20:38:08.110674",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
@@ -58,6 +66,5 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
"sort_order": "DESC"
}

View File

@@ -82,36 +82,6 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
this.frm.change_custom_button_type('Allocate', null, 'default');
}
// check for any running reconciliation jobs
if (this.frm.doc.receivable_payable_account) {
this.frm.call({
doc: this.frm.doc,
method: 'is_auto_process_enabled',
callback: (r) => {
if (r.message) {
this.frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
"args": {
for_filter: {
company: this.frm.doc.company,
party_type: this.frm.doc.party_type,
party: this.frm.doc.party,
receivable_payable_account: this.frm.doc.receivable_payable_account
}
}
}).then(r => {
if (r.message) {
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
this.frm.dashboard.add_comment(msg, "yellow");
}
});
}
}
});
}
}
company() {
@@ -302,32 +272,4 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}
};
frappe.ui.form.on('Payment Reconciliation Allocation', {
allocated_amount: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
// filter invoice
let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number));
// filter payment
let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name));
frm.call({
doc: frm.doc,
method: 'calculate_difference_on_allocation_change',
args: {
payment_entry: payment,
invoice: invoice,
allocated_amount: row.allocated_amount
},
callback: (r) => {
if (r.message) {
row.difference_amount = r.message;
frm.refresh();
}
}
});
}
});
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));

View File

@@ -6,12 +6,10 @@ import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
is_any_doc_running,
)
from erpnext.accounts.utils import (
QueryPaymentLedger,
get_outstanding_invoices,
@@ -126,29 +124,12 @@ class PaymentReconciliation(Document):
return list(journal_entries)
def get_return_invoices(self):
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doc = qb.DocType(voucher_type)
self.return_invoices = (
qb.from_(doc)
.select(
ConstantColumn(voucher_type).as_("voucher_type"),
doc.name.as_("voucher_no"),
doc.return_against,
)
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
)
.run(as_dict=True)
)
def get_dr_or_cr_notes(self):
self.build_qb_filter_conditions(get_return_invoices=True)
ple = qb.DocType("Payment Ledger Entry")
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
if erpnext.get_party_account_type(self.party_type) == "Receivable":
self.common_filter_conditions.append(ple.account_type == "Receivable")
@@ -156,10 +137,19 @@ class PaymentReconciliation(Document):
self.common_filter_conditions.append(ple.account_type == "Payable")
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
self.get_return_invoices()
return_invoices = [
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
]
# get return invoices
doc = qb.DocType(voucher_type)
return_invoices = (
qb.from_(doc)
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
& (IfNull(doc.return_against, "") == "")
)
.run(as_dict=True)
)
outstanding_dr_or_cr = []
if return_invoices:
@@ -211,15 +201,6 @@ class PaymentReconciliation(Document):
accounting_dimensions=self.accounting_dimension_filter_conditions,
)
cr_dr_notes = (
[x.voucher_no for x in self.return_invoices]
if self.party_type in ["Customer", "Supplier"]
else []
)
# Filter out cr/dr notes from outstanding invoices list
# Happens when non-standalone cr/dr notes are linked with another invoice through journal entry
non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes]
if self.invoice_limit:
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
@@ -240,31 +221,15 @@ class PaymentReconciliation(Document):
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
difference_amount = 0
if frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
) != frappe.get_cached_value("Company", self.company, "default_currency"):
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount
@frappe.whitelist()
def is_auto_process_enabled(self):
return frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments")
@frappe.whitelist()
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
new_difference_amount = self.get_difference_amount(
payment_entry[0], invoice[0], allocated_amount
)
return new_difference_amount
@frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
@@ -327,7 +292,9 @@ class PaymentReconciliation(Document):
}
)
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
@frappe.whitelist()
def reconcile(self):
self.validate_allocation()
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@@ -347,42 +314,16 @@ class PaymentReconciliation(Document):
payment_details = self.get_payment_details(row, dr_or_cr)
reconciled_entry.append(payment_details)
if payment_details.difference_amount and row.reference_type not in [
"Sales Invoice",
"Purchase Invoice",
]:
if payment_details.difference_amount:
self.make_difference_entry(payment_details)
if entry_list:
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
reconcile_against_document(entry_list)
if dr_or_cr_notes:
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
@frappe.whitelist()
def reconcile(self):
if frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
running_doc = is_any_doc_running(
dict(
company=self.company,
party_type=self.party_type,
party=self.party,
receivable_payable_account=self.receivable_payable_account,
)
)
if running_doc:
frappe.throw(
_("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format(
get_link_to_form("Auto Reconcile", running_doc)
)
)
return
self.validate_allocation()
self.reconcile_allocations()
msgprint(_("Successfully Reconciled"))
self.get_unreconciled_entries()
def make_difference_entry(self, row):
@@ -436,8 +377,6 @@ class PaymentReconciliation(Document):
journal_entry.save()
journal_entry.submit()
return journal_entry
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
{
@@ -603,16 +542,6 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company):
def get_difference_row(inv):
if inv.difference_amount != 0 and inv.difference_account:
difference_row = {
"account": inv.difference_account,
inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
"cost_center": erpnext.get_default_cost_center(company),
}
return difference_row
for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
@@ -657,9 +586,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
],
}
)
if difference_entry := get_difference_row(inv):
jv.append("accounts", difference_entry)
jv.flags.ignore_mandatory = True
jv.submit()

View File

@@ -5,19 +5,16 @@ import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_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
from erpnext.accounts.party import get_party_account
from erpnext.stock.doctype.item.test_item import create_item
test_dependencies = ["Item"]
class TestPaymentReconciliation(FrappeTestCase):
def setUp(self):
@@ -166,9 +163,7 @@ class TestPaymentReconciliation(FrappeTestCase):
def create_payment_reconciliation(self):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = (
self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
)
pr.party_type = "Customer"
pr.party = self.customer
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
@@ -354,11 +349,6 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
si.reload()
@@ -400,11 +390,6 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# check PR tool output
@@ -429,11 +414,6 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# assert outstanding
@@ -470,11 +450,6 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
self.assertEqual(pr.get("invoices"), [])
@@ -849,88 +824,6 @@ class TestPaymentReconciliation(FrappeTestCase):
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
@change_settings(
"Accounts Settings",
{
"allow_multi_currency_invoices_against_single_party_account": 1,
},
)
def test_no_difference_amount_for_base_currency_accounts(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debit_to
si.save().submit()
# Make payment using Payment Entry
pe1 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer,
paid_from=self.debit_to,
paid_to=self.bank,
paid_amount=100,
)
pe1.save()
pe1.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer
pr.receivable_payable_account = self.debit_to
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0)
def test_reconciliation_purchase_invoice_against_return(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD", currency="USD", conversion_rate=50
).submit()
pi_return = frappe.get_doc(pi.as_dict())
pi_return.name = None
pi_return.docstatus = 0
pi_return.is_return = 1
pi_return.conversion_rate = 80
pi_return.items[0].qty = -pi_return.items[0].qty
pi_return.submit()
self.company = "_Test Company"
self.party_type = "Supplier"
self.customer = "_Test Supplier USD"
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = []
payments = []
for invoice in pr.invoices:
if invoice.invoice_number == pi.name:
invoices.append(invoice.as_dict())
break
for payment in pr.payments:
if payment.reference_name == pi_return.name:
payments.append(payment.as_dict())
break
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
pr.reconcile()
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -497,16 +497,10 @@ def get_amount(ref_doc, payment_account=None):
if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
elif dt == "POS Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:

View File

@@ -6,7 +6,6 @@ import unittest
import frappe
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate
@@ -75,29 +74,6 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD")
def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice(
customer="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
)
pr = make_payment_request(
dt="Purchase Invoice",
dn=si_usd.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
pe = pr.create_payment_entry()
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
def test_payment_entry(self):
frappe.db.set_value(
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"

View File

@@ -2,11 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on('Payment Terms Template', {
refresh: function(frm) {
frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms);
},
setup: function(frm) {
allocate_payment_based_on_payment_terms: function(frm) {
frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms);
}
});

View File

@@ -11,7 +11,7 @@ from frappe.utils import flt
class PaymentTermsTemplate(Document):
def validate(self):
self.validate_invoice_portion()
self.validate_terms()
self.check_duplicate_terms()
def validate_invoice_portion(self):
total_portion = 0
@@ -23,12 +23,9 @@ class PaymentTermsTemplate(Document):
_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red"
)
def validate_terms(self):
def check_duplicate_terms(self):
terms = []
for term in self.terms:
if self.allocate_payment_based_on_payment_terms and not term.payment_term:
frappe.throw(_("Row {0}: Payment Term is mandatory").format(term.idx))
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
if term_info in terms:
frappe.msgprint(

View File

@@ -4,13 +4,12 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, flt
from frappe.utils import flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year
from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController
@@ -21,17 +20,9 @@ class PeriodClosingVoucher(AccountsController):
def on_submit(self):
self.db_set("gle_processing_status", "In Progress")
get_opening_entries = False
if not frappe.db.exists(
"Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)}
):
get_opening_entries = True
self.make_gl_entries(get_opening_entries=get_opening_entries)
self.make_gl_entries()
def on_cancel(self):
self.validate_future_closing_vouchers()
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
gle_count = frappe.db.count(
@@ -44,7 +35,6 @@ class PeriodClosingVoucher(AccountsController):
voucher_type="Period Closing Voucher",
voucher_no=self.name,
queue="long",
enqueue_after_commit=True,
)
frappe.msgprint(
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
@@ -52,27 +42,8 @@ class PeriodClosingVoucher(AccountsController):
else:
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
self.delete_closing_entries()
def validate_future_closing_vouchers(self):
if frappe.db.exists(
"Period Closing Voucher",
{"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company},
):
frappe.throw(
_(
"You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first"
)
)
def delete_closing_entries(self):
closing_balance = frappe.qb.DocType("Account Closing Balance")
frappe.qb.from_(closing_balance).delete().where(
closing_balance.period_closing_voucher == self.name
).run()
def validate_account_head(self):
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
if closing_account_type not in ["Liability", "Equity"]:
frappe.throw(
@@ -85,6 +56,8 @@ class PeriodClosingVoucher(AccountsController):
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
def validate_posting_date(self):
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
validate_fiscal_year(
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
)
@@ -93,8 +66,6 @@ class PeriodClosingVoucher(AccountsController):
self.posting_date, self.fiscal_year, company=self.company
)[1]
self.check_if_previous_year_closed()
pce = frappe.db.sql(
"""select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
@@ -107,65 +78,28 @@ class PeriodClosingVoucher(AccountsController):
)
)
def check_if_previous_year_closed(self):
last_year_closing = add_days(self.year_start_date, -1)
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
if previous_fiscal_year and not frappe.db.exists(
"GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company}
):
return
if previous_fiscal_year and not frappe.db.exists(
"Period Closing Voucher",
{"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company},
):
frappe.throw(_("Previous Year is not closed, please close it first"))
def make_gl_entries(self, get_opening_entries=False):
def make_gl_entries(self):
gl_entries = self.get_gl_entries()
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
if len(gl_entries) > 5000:
frappe.enqueue(
process_gl_entries,
gl_entries=gl_entries,
closing_entries=closing_entries,
voucher_name=self.name,
company=self.company,
closing_date=self.posting_date,
queue="long",
)
frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True,
)
else:
process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = []
for acc in self.get_balances_based_on_dimensions(
group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries
):
closing_entries.append(self.get_closing_entries(acc))
return closing_entries
if gl_entries:
if len(gl_entries) > 5000:
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True,
)
else:
process_gl_entries(gl_entries)
def get_gl_entries(self):
gl_entries = []
# pl account
for acc in self.get_balances_based_on_dimensions(
group_by_account=True, report_type="Profit and Loss"
):
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_pl_account(acc))
# closing liability account
for acc in self.get_balances_based_on_dimensions(
group_by_account=False, report_type="Profit and Loss"
):
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_closing_account(acc))
@@ -174,8 +108,6 @@ class PeriodClosingVoucher(AccountsController):
def get_gle_for_pl_account(self, acc):
gl_entry = self.get_gl_dict(
{
"company": self.company,
"closing_date": self.posting_date,
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
@@ -188,7 +120,6 @@ class PeriodClosingVoucher(AccountsController):
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"is_period_closing_voucher_entry": 1,
},
item=acc,
)
@@ -198,8 +129,6 @@ class PeriodClosingVoucher(AccountsController):
def get_gle_for_closing_account(self, acc):
gl_entry = self.get_gl_dict(
{
"company": self.company,
"closing_date": self.posting_date,
"account": self.closing_account_head,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
@@ -212,36 +141,12 @@ class PeriodClosingVoucher(AccountsController):
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
"is_period_closing_voucher_entry": 1,
},
item=acc,
)
self.update_default_dimensions(gl_entry, acc)
return gl_entry
def get_closing_entries(self, acc):
closing_entry = self.get_gl_dict(
{
"company": self.company,
"closing_date": self.posting_date,
"period_closing_voucher": self.name,
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": flt(acc.debit_in_account_currency),
"debit": flt(acc.debit),
"credit_in_account_currency": flt(acc.credit_in_account_currency),
"credit": flt(acc.credit),
},
item=acc,
)
for dimension in self.accounting_dimensions:
closing_entry.update({dimension: acc.get(dimension)})
return closing_entry
def update_default_dimensions(self, gl_entry, acc):
if not self.accounting_dimensions:
self.accounting_dimensions = get_accounting_dimensions()
@@ -249,95 +154,56 @@ class PeriodClosingVoucher(AccountsController):
for dimension in self.accounting_dimensions:
gl_entry.update({dimension: acc.get(dimension)})
def get_balances_based_on_dimensions(
self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False
):
def get_pl_balances_based_on_dimensions(self, group_by_account=False):
"""Get balance for dimension-wise pl accounts"""
qb_dimension_fields = ["cost_center", "finance_book", "project"]
dimension_fields = ["t1.cost_center", "t1.finance_book"]
self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions:
qb_dimension_fields.append(dimension)
dimension_fields.append("t1.{0}".format(dimension))
if group_by_account:
qb_dimension_fields.append("account")
dimension_fields.append("t1.account")
account_filters = {
"company": self.company,
"is_group": 0,
}
if report_type:
account_filters.update({"report_type": report_type})
accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
gl_entry = frappe.qb.DocType("GL Entry")
query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
if not for_aggregation:
query = query.select(
(Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
"bal_in_account_currency"
),
(Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
)
else:
query = query.select(
(Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"),
(Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"),
(Sum(gl_entry.debit)).as_("debit"),
(Sum(gl_entry.credit)).as_("credit"),
)
for dimension in qb_dimension_fields:
query = query.select(gl_entry[dimension])
query = query.where(
(gl_entry.company == self.company)
& (gl_entry.is_cancelled == 0)
& (gl_entry.account.isin(accounts))
return frappe.db.sql(
"""
select
t2.account_currency,
{dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
where
t1.is_cancelled = 0
and t1.account = t2.name
and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2
and t2.company = %s
and t1.posting_date between %s and %s
group by {dimension_fields}
""".format(
dimension_fields=", ".join(dimension_fields)
),
(self.company, self.get("year_start_date"), self.posting_date),
as_dict=1,
)
if get_opening_entries:
query = query.where(
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
| gl_entry.is_opening
== "Yes"
)
else:
query = query.where(
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
& gl_entry.is_opening
== "No"
)
if for_aggregation:
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
for dimension in qb_dimension_fields:
query = query.groupby(gl_entry[dimension])
return query.run(as_dict=1)
def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
def process_gl_entries(gl_entries):
from erpnext.accounts.general_ledger import make_gl_entries
try:
if gl_entries:
make_gl_entries(gl_entries, merge_entries=False)
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
make_gl_entries(gl_entries, merge_entries=False)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
)
def make_reverse_gl_entries(voucher_type, voucher_no):

View File

@@ -5,7 +5,7 @@
import unittest
import frappe
from frappe.utils import add_months, today
from frappe.utils import today
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
@@ -16,17 +16,16 @@ from erpnext.accounts.utils import get_fiscal_year, now
class TestPeriodClosingVoucher(unittest.TestCase):
def test_closing_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
posting_date=now(),
save=False,
)
jv1.company = company
@@ -34,18 +33,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
jv1.submit()
jv2 = make_journal_entry(
posting_date="2021-03-15",
amount=600,
account1="Cost of Goods Sold - TPC",
account2="Cash - TPC",
cost_center=cost_center,
posting_date=now(),
save=False,
)
jv2.company = company
jv2.save()
jv2.submit()
pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
pcv = self.make_period_closing_voucher()
surplus_account = pcv.closing_account_head
expected_gle = (
@@ -66,7 +65,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
def test_cost_center_wise_posting(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
@@ -83,7 +81,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
posting_date="2021-03-15",
)
create_sales_invoice(
company=company,
@@ -94,10 +91,9 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
posting_date="2021-03-15",
)
pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False)
pcv = self.make_period_closing_voucher(submit=False)
pcv.save()
pcv.submit()
surplus_account = pcv.closing_account_head
@@ -132,13 +128,12 @@ class TestPeriodClosingVoucher(unittest.TestCase):
def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
create_sales_invoice(
si = create_sales_invoice(
company=company,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
@@ -147,7 +142,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
posting_date="2021-03-15",
)
jv = make_journal_entry(
@@ -155,14 +149,14 @@ class TestPeriodClosingVoucher(unittest.TestCase):
account2="Sales - TPC",
amount=400,
cost_center=cost_center,
posting_date="2021-03-15",
posting_date=now(),
)
jv.company = company
jv.finance_book = create_finance_book().name
jv.save()
jv.submit()
pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
pcv = self.make_period_closing_voucher()
surplus_account = pcv.closing_account_head
expected_gle = (
@@ -182,148 +176,15 @@ class TestPeriodClosingVoucher(unittest.TestCase):
)
self.assertSequenceEqual(pcv_gle, expected_gle)
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
repost_doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"company": company,
"posting_date": "2020-03-15",
"based_on": "Item and Warehouse",
"item_code": "Test Item 1",
"warehouse": warehouse,
}
)
self.assertRaises(frappe.ValidationError, repost_doc.save)
repost_doc.posting_date = add_months(today(), 13)
repost_doc.save()
def test_gl_entries_restrictions(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
self.make_period_closing_voucher(posting_date="2021-03-31")
jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
save=False,
)
jv1.company = company
jv1.save()
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
company = create_company()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center1,
save=False,
)
jv1.company = company
jv1.save()
jv1.submit()
jv2 = make_journal_entry(
posting_date="2021-03-15",
amount=200,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
save=False,
)
jv2.company = company
jv2.save()
jv2.submit()
pcv1 = self.make_period_closing_voucher(posting_date="2021-03-31")
closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center1,
"period_closing_voucher": pcv1.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
self.assertEqual(closing_balance.credit, 400)
self.assertEqual(closing_balance.credit_in_account_currency, 400)
jv3 = make_journal_entry(
posting_date="2022-03-15",
amount=300,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
save=False,
)
jv3.company = company
jv3.save()
jv3.submit()
pcv2 = self.make_period_closing_voucher(posting_date="2022-03-31")
cc1_closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center1,
"period_closing_voucher": pcv2.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
cc2_closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center2,
"period_closing_voucher": pcv2.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
self.assertEqual(cc1_closing_balance.credit, 400)
self.assertEqual(cc1_closing_balance.credit_in_account_currency, 400)
self.assertEqual(cc2_closing_balance.credit, 500)
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
def make_period_closing_voucher(self, posting_date=None, submit=True):
def make_period_closing_voucher(self, submit=True):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
pcv = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": posting_date or today(),
"posting_date": posting_date or today(),
"transaction_date": today(),
"posting_date": today(),
"company": "Test PCV Company",
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
"cost_center": cost_center,

View File

@@ -123,29 +123,22 @@ frappe.ui.form.on('POS Closing Entry', {
row.expected_amount = row.opening_amount;
}
await Promise.all([
frappe.call({
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
pos_profile: frm.doc.pos_profile,
user: frm.doc.user
},
callback: (r) => {
let pos_invoices = r.message;
for (let doc of pos_invoices) {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
}
}
})
])
const pos_inv_promises = frm.doc.pos_transactions.map(
row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
);
const pos_invoices = await Promise.all(pos_inv_promises);
for (let doc of pos_invoices) {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
}
frappe.dom.unfreeze();
}
});

View File

@@ -112,8 +112,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
party_type: "Customer",
account: this.frm.doc.debit_to,
price_list: this.frm.doc.selling_price_list,
pos_profile: pos_profile,
company_address: this.frm.doc.company_address
pos_profile: pos_profile
}, () => {
this.apply_pricing_rule();
});

View File

@@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
@@ -674,22 +673,18 @@ def get_bin_qty(item_code, warehouse):
def get_pos_reserved_qty(item_code, warehouse):
p_inv = frappe.qb.DocType("POS Invoice")
p_item = frappe.qb.DocType("POS Invoice Item")
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
.select(Sum(p_item.qty).as_("qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
& (p_inv.is_return == 0)
& (p_item.docstatus == 1)
& (p_item.item_code == item_code)
& (p_item.warehouse == warehouse)
)
).run(as_dict=True)
reserved_qty = frappe.db.sql(
"""select sum(p_item.qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
and ifnull(p.consolidated_invoice, '') = ''
and p_item.docstatus = 1
and p_item.item_code = %s
and p_item.warehouse = %s
""",
(item_code, warehouse),
as_dict=1,
)
return reserved_qty[0].qty or 0 if reserved_qty else 0

View File

@@ -469,7 +469,7 @@
"options": "UOM"
},
{
"description": "If rate is zero then item will be treated as \"Free Item\"",
"description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate",
"fieldtype": "Currency",
"label": "Free Item Rate"
@@ -670,4 +670,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -16,10 +16,8 @@ from erpnext.stock.doctype.item.test_item import create_item
class TestProcessDeferredAccounting(unittest.TestCase):
def test_creation_of_ledger_entry_on_submit(self):
"""test creation of gl entries on submission of document"""
change_acc_settings(acc_frozen_upto="2023-05-31", book_deferred_entries_based_on="Months")
deferred_account = create_account(
account_name="Deferred Revenue for Accounts Frozen",
account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC",
company="_Test Company",
)
@@ -31,11 +29,11 @@ class TestProcessDeferredAccounting(unittest.TestCase):
item.save()
si = create_sales_invoice(
item=item.name, rate=3000, update_stock=0, posting_date="2023-07-01", do_not_submit=True
item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True
)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2023-05-01"
si.items[0].service_end_date = "2023-07-31"
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
@@ -43,9 +41,9 @@ class TestProcessDeferredAccounting(unittest.TestCase):
process_deferred_accounting = doc = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
posting_date="2023-07-01",
start_date="2023-05-01",
end_date="2023-06-30",
posting_date="2019-01-01",
start_date="2019-01-01",
end_date="2019-01-31",
type="Income",
)
)
@@ -54,16 +52,11 @@ class TestProcessDeferredAccounting(unittest.TestCase):
process_deferred_accounting.submit()
expected_gle = [
["Debtors - _TC", 3000, 0.0, "2023-07-01"],
[deferred_account, 0.0, 3000, "2023-07-01"],
["Sales - _TC", 0.0, 1000, "2023-06-30"],
[deferred_account, 1000, 0.0, "2023-06-30"],
["Sales - _TC", 0.0, 1000, "2023-06-30"],
[deferred_account, 1000, 0.0, "2023-06-30"],
[deferred_account, 33.85, 0.0, "2019-01-31"],
["Sales - _TC", 0.0, 33.85, "2019-01-31"],
]
check_gl_entries(self, si.name, expected_gle, "2023-07-01")
change_acc_settings()
check_gl_entries(self, si.name, expected_gle, "2019-01-10")
def test_pda_submission_and_cancellation(self):
pda = frappe.get_doc(
@@ -77,10 +70,3 @@ class TestProcessDeferredAccounting(unittest.TestCase):
)
pda.submit()
pda.cancel()
def change_acc_settings(acc_frozen_upto="", book_deferred_entries_based_on="Days"):
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.acc_frozen_upto = acc_frozen_upto
acc_settings.book_deferred_entries_based_on = book_deferred_entries_based_on
acc_settings.save()

View File

@@ -1,130 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Process Payment Reconciliation", {
onload: function(frm) {
// set queries
frm.set_query("party_type", function() {
return {
"filters": {
"name": ["in", Object.keys(frappe.boot.party_account_types)],
}
}
});
frm.set_query('receivable_payable_account', function(doc) {
return {
filters: {
"company": doc.company,
"is_group": 0,
"account_type": frappe.boot.party_account_types[doc.party_type]
}
};
});
frm.set_query('cost_center', function(doc) {
return {
filters: {
"company": doc.company,
"is_group": 0,
}
};
});
frm.set_query('bank_cash_account', function(doc) {
return {
filters:[
['Account', 'company', '=', doc.company],
['Account', 'is_group', '=', 0],
['Account', 'account_type', 'in', ['Bank', 'Cash']]
]
};
});
},
refresh: function(frm) {
if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) {
let execute_btn = __("Start / Resume")
frm.add_custom_button(execute_btn, () => {
frm.call({
method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc',
args: {
docname: frm.doc.name
}
}).then(r => {
if(!r.exc) {
frappe.show_alert(__("Job Started"));
frm.reload_doc();
}
});
});
}
if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count",
args: {
"docname": frm.docname,
}
}).then(r => {
if (r.message) {
let progress = 0;
let description = "";
if (r.message.processed) {
progress = (r.message.processed/r.message.total) * 100;
description = r.message.processed + "/" + r.message.total + " processed";
} else if (r.message.total == 0 && frm.doc.status == "Completed") {
progress = 100;
}
frm.dashboard.add_progress('Reconciliation Progress', progress, description);
}
})
}
if (frm.doc.docstatus==1 && frm.doc.status == 'Running') {
let execute_btn = __("Pause")
frm.add_custom_button(execute_btn, () => {
frm.call({
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc",
args: {
"docname": frm.docname,
}
}).then(r => {
if (!r.exc) {
frappe.show_alert(__("Job Paused"));
frm.reload_doc()
}
});
});
}
},
company(frm) {
frm.set_value('party', '');
frm.set_value('receivable_payable_account', '');
},
party_type(frm) {
frm.set_value('party', '');
},
party(frm) {
frm.set_value('receivable_payable_account', '');
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
return frappe.call({
method: "erpnext.accounts.party.get_party_account",
args: {
company: frm.doc.company,
party_type: frm.doc.party_type,
party: frm.doc.party
},
callback: (r) => {
if (!r.exc && r.message) {
frm.set_value("receivable_payable_account", r.message);
}
frm.refresh();
}
});
}
}
});

View File

@@ -1,173 +0,0 @@
{
"actions": [],
"autoname": "format:ACC-PPR-{#####}",
"beta": 1,
"creation": "2023-03-30 21:28:39.793927",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"party_type",
"column_break_io6c",
"party",
"receivable_payable_account",
"filter_section",
"from_invoice_date",
"to_invoice_date",
"column_break_kegk",
"from_payment_date",
"to_payment_date",
"column_break_uj04",
"cost_center",
"bank_cash_account",
"section_break_2n02",
"status",
"error_log",
"section_break_a8yx",
"amended_from"
],
"fields": [
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nQueued\nRunning\nPaused\nCompleted\nPartially Reconciled\nFailed\nCancelled",
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "party_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Party Type",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "column_break_io6c",
"fieldtype": "Column Break"
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Party",
"options": "party_type",
"reqd": 1
},
{
"fieldname": "receivable_payable_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Receivable/Payable Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "filter_section",
"fieldtype": "Section Break",
"label": "Filters"
},
{
"fieldname": "from_invoice_date",
"fieldtype": "Date",
"label": "From Invoice Date"
},
{
"fieldname": "to_invoice_date",
"fieldtype": "Date",
"label": "To Invoice Date"
},
{
"fieldname": "column_break_kegk",
"fieldtype": "Column Break"
},
{
"fieldname": "from_payment_date",
"fieldtype": "Date",
"label": "From Payment Date"
},
{
"fieldname": "to_payment_date",
"fieldtype": "Date",
"label": "To Payment Date"
},
{
"fieldname": "column_break_uj04",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "bank_cash_account",
"fieldtype": "Link",
"label": "Bank/Cash Account",
"options": "Account"
},
{
"fieldname": "section_break_2n02",
"fieldtype": "Section Break",
"label": "Status"
},
{
"depends_on": "eval:doc.error_log",
"fieldname": "error_log",
"fieldtype": "Long Text",
"label": "Error Log"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Process Payment Reconciliation",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_a8yx",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-04-21 17:19:30.912953",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "company"
}

View File

@@ -1,503 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.utils import get_link_to_form
from frappe.utils.scheduler import is_scheduler_inactive
class ProcessPaymentReconciliation(Document):
def validate(self):
self.validate_receivable_payable_account()
self.validate_bank_cash_account()
def validate_receivable_payable_account(self):
if self.receivable_payable_account:
if self.company != frappe.db.get_value("Account", self.receivable_payable_account, "company"):
frappe.throw(
_("Receivable/Payable Account: {0} doesn't belong to company {1}").format(
frappe.bold(self.receivable_payable_account), frappe.bold(self.company)
)
)
def validate_bank_cash_account(self):
if self.bank_cash_account:
if self.company != frappe.db.get_value("Account", self.bank_cash_account, "company"):
frappe.throw(
_("Bank/Cash Account {0} doesn't belong to company {1}").format(
frappe.bold(self.bank_cash_account), frappe.bold(self.company)
)
)
def before_save(self):
self.status = ""
self.error_log = ""
def on_submit(self):
self.db_set("status", "Queued")
self.db_set("error_log", None)
def on_cancel(self):
self.db_set("status", "Cancelled")
log = frappe.db.get_value(
"Process Payment Reconciliation Log", filters={"process_pr": self.name}
)
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled")
@frappe.whitelist()
def get_reconciled_count(docname: str | None = None) -> float:
current_status = {}
if docname:
reconcile_log = frappe.db.get_value(
"Process Payment Reconciliation Log", filters={"process_pr": docname}, fieldname="name"
)
if reconcile_log:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": reconcile_log},
fields=["reconciled_entries", "total_allocations"],
as_list=1,
)
current_status["processed"], current_status["total"] = res[0]
return current_status
def get_pr_instance(doc: str):
process_payment_reconciliation = frappe.get_doc("Process Payment Reconciliation", doc)
pr = frappe.get_doc("Payment Reconciliation")
fields = [
"company",
"party_type",
"party",
"receivable_payable_account",
"from_invoice_date",
"to_invoice_date",
"from_payment_date",
"to_payment_date",
]
d = {}
for field in fields:
d[field] = process_payment_reconciliation.get(field)
pr.update(d)
pr.invoice_limit = 1000
pr.payment_limit = 1000
return pr
def is_job_running(job_name: str) -> bool:
jobs = frappe.db.get_all("RQ Job", filters={"status": ["in", ["started", "queued"]]})
for x in jobs:
if x.job_name == job_name:
return True
return False
@frappe.whitelist()
def pause_job_for_doc(docname: str | None = None):
if docname:
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Paused")
@frappe.whitelist()
def trigger_job_for_doc(docname: str | None = None):
"""
Trigger background job
"""
if not docname:
return
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
get_link_to_form("Accounts Settings", "Accounts Settings")
)
)
return
if not is_scheduler_inactive():
if frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Queued":
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
job_name = f"start_processing_{docname}"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
queue="long",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=docname,
)
elif frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Paused":
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
if log:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Running")
# Resume tasks for running doc
job_name = f"start_processing_{docname}"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
queue="long",
is_async=True,
job_name=job_name,
doc=docname,
)
else:
frappe.msgprint(_("Scheduler is Inactive. Can't trigger job now."))
def trigger_reconciliation_for_queued_docs():
"""
Will be called from Cron Job
Fetch queued docs and start reconciliation process for each one
"""
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.msgprint(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
get_link_to_form("Accounts Settings", "Accounts Settings")
)
)
return
if not is_scheduler_inactive():
# Get all queued documents
all_queued = frappe.db.get_all(
"Process Payment Reconciliation",
filters={"docstatus": 1, "status": "Queued"},
order_by="creation desc",
as_list=1,
)
docs_to_trigger = []
unique_filters = set()
queue_size = 5
fields = ["company", "party_type", "party", "receivable_payable_account"]
def get_filters_as_tuple(fields, doc):
filters = ()
for x in fields:
filters += tuple(doc.get(x))
return filters
for x in all_queued:
doc = frappe.get_doc("Process Payment Reconciliation", x)
filters = get_filters_as_tuple(fields, doc)
if filters not in unique_filters:
unique_filters.add(filters)
docs_to_trigger.append(doc.name)
if len(docs_to_trigger) == queue_size:
break
# trigger reconcilation process for queue_size unique filters
for doc in docs_to_trigger:
trigger_job_for_doc(doc)
else:
frappe.msgprint(_("Scheduler is Inactive. Can't trigger jobs now."))
def reconcile_based_on_filters(doc: None | str = None) -> None:
"""
Identify current state of document and execute next tasks in background
"""
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if not log:
log = frappe.new_doc("Process Payment Reconciliation Log")
log.process_pr = doc
log.status = "Running"
log = log.save()
job_name = f"process_{doc}_fetch_and_allocate"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
queue="long",
timeout="3600",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=doc,
)
else:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": log},
fields=["allocated", "reconciled"],
as_list=1,
)
allocated, reconciled = res[0]
if not allocated:
job_name = f"process__{doc}_fetch_and_allocate"
if not is_job_running(job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
queue="long",
timeout="3600",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
doc=doc,
)
elif not reconciled:
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
elif reconciled:
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
def get_next_allocation(log: str) -> list:
if log:
allocations = []
next = frappe.db.get_all(
"Process Payment Reconciliation Log Allocations",
filters={"parent": log, "reconciled": 0},
fields=["reference_type", "reference_name"],
order_by="idx",
limit=1,
)
if next:
allocations = frappe.db.get_all(
"Process Payment Reconciliation Log Allocations",
filters={
"parent": log,
"reconciled": 0,
"reference_type": next[0].reference_type,
"reference_name": next[0].reference_name,
},
fields=["*"],
order_by="idx",
)
return allocations
return []
def fetch_and_allocate(doc: str) -> None:
"""
Fetch Invoices and Payments based on filters applied. FIFO ordering is used for allocation.
"""
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if log:
if not frappe.db.get_value("Process Payment Reconciliation Log", log, "allocated"):
reconcile_log = frappe.get_doc("Process Payment Reconciliation Log", log)
pr = get_pr_instance(doc)
pr.get_unreconciled_entries()
if len(pr.invoices) > 0 and len(pr.payments) > 0:
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
for x in pr.get("allocation"):
reconcile_log.append(
"allocations",
x.as_dict().update(
{
"parenttype": "Process Payment Reconciliation Log",
"parent": reconcile_log.name,
"name": None,
"reconciled": False,
}
),
)
reconcile_log.allocated = True
reconcile_log.total_allocations = len(reconcile_log.get("allocations"))
reconcile_log.reconciled_entries = 0
reconcile_log.save()
# generate reconcile job name
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
def reconcile(doc: None | str = None) -> None:
if doc:
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
if log:
res = frappe.get_all(
"Process Payment Reconciliation Log",
filters={"name": log},
fields=["reconciled_entries", "total_allocations"],
as_list=1,
limit=1,
)
reconciled_entries, total_allocations = res[0]
if reconciled_entries != total_allocations:
try:
# Fetch next allocation
allocations = get_next_allocation(log)
pr = get_pr_instance(doc)
# pass allocation to PR instance
for x in allocations:
pr.append("allocation", x)
# reconcile
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
# If Payment Entry, update details only for newly linked references
# This is for performance
if allocations[0].reference_type == "Payment Entry":
references = [(x.invoice_type, x.invoice_number) for x in allocations]
pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name)
pe.flags.ignore_validate_update_after_submit = True
pe.set_missing_ref_details(update_ref_details_only_for=references)
pe.save()
# Update reconciled flag
allocation_names = [x.name for x in allocations]
ppa = qb.DocType("Process Payment Reconciliation Log Allocations")
qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run()
# Update reconciled count
reconciled_count = frappe.db.count(
"Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True}
)
frappe.db.set_value(
"Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count
)
except Exception as err:
# Update the parent doc about the exception
frappe.db.rollback()
traceback = frappe.get_traceback()
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"error_log",
message,
)
if reconciled_entries and total_allocations and reconciled_entries < total_allocations:
frappe.db.set_value(
"Process Payment Reconciliation Log", log, "status", "Partially Reconciled"
)
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"status",
"Partially Reconciled",
)
else:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Failed")
frappe.db.set_value(
"Process Payment Reconciliation",
doc,
"status",
"Failed",
)
finally:
if reconciled_entries == total_allocations:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
else:
if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"):
# trigger next batch in job
# generate reconcile job name
allocation = get_next_allocation(log)
if allocation:
reconcile_job_name = (
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
)
else:
reconcile_job_name = f"process_{doc}_reconcile"
if not is_job_running(reconcile_job_name):
job = frappe.enqueue(
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
queue="long",
timeout="3600",
is_async=True,
job_name=reconcile_job_name,
enqueue_after_commit=True,
doc=doc,
)
else:
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
@frappe.whitelist()
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None
if for_filter:
if type(for_filter) == str:
for_filter = frappe.json.loads(for_filter)
running_doc = frappe.db.get_value(
"Process Payment Reconciliation",
filters={
"docstatus": 1,
"status": ["in", ["Running", "Paused"]],
"company": for_filter.get("company"),
"party_type": for_filter.get("party_type"),
"party": for_filter.get("party"),
"receivable_payable_account": for_filter.get("receivable_payable_account"),
},
fieldname="name",
)
else:
running_doc = frappe.db.get_value(
"Process Payment Reconciliation", filters={"docstatus": 1, "status": "Running"}
)
return running_doc

View File

@@ -1,15 +0,0 @@
from frappe import _
def get_data():
return {
"fieldname": "process_pr",
"transactions": [
{
"label": _("Reconciliation Logs"),
"items": [
"Process Payment Reconciliation Log",
],
},
],
}

View File

@@ -1,15 +0,0 @@
frappe.listview_settings['Process Payment Reconciliation'] = {
add_fields: ["status"],
get_indicator: function(doc) {
let colors = {
'Queued': 'orange',
'Paused': 'orange',
'Completed': 'green',
'Partially Reconciled': 'orange',
'Running': 'blue',
'Failed': 'red',
};
let status = doc.status;
return [__(status), colors[status], 'status,=,'+status];
},
};

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestProcessPaymentReconciliation(FrappeTestCase):
pass

View File

@@ -1,17 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Process Payment Reconciliation Log", {
refresh(frm) {
if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
let progress = 0;
if (frm.doc.reconciled_entries != 0) {
progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100;
} else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){
progress = 100;
}
frm.dashboard.add_progress(__('Reconciliation Progress'), progress);
}
},
});

View File

@@ -1,137 +0,0 @@
{
"actions": [],
"autoname": "format:PPR-LOG-{##}",
"beta": 1,
"creation": "2023-03-13 15:00:09.149681",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"process_pr",
"section_break_fvdw",
"status",
"tasks_section",
"allocated",
"reconciled",
"column_break_yhin",
"total_allocations",
"reconciled_entries",
"section_break_4ywv",
"error_log",
"allocations_section",
"allocations"
],
"fields": [
{
"fieldname": "allocations",
"fieldtype": "Table",
"label": "Allocations",
"options": "Process Payment Reconciliation Log Allocations",
"read_only": 1
},
{
"default": "0",
"description": "All allocations have been successfully reconciled",
"fieldname": "reconciled",
"fieldtype": "Check",
"label": "Reconciled",
"read_only": 1
},
{
"fieldname": "total_allocations",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Total Allocations",
"read_only": 1
},
{
"default": "0",
"description": "Invoices and Payments have been Fetched and Allocated",
"fieldname": "allocated",
"fieldtype": "Check",
"label": "Allocated",
"read_only": 1
},
{
"fieldname": "reconciled_entries",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Reconciled Entries",
"read_only": 1
},
{
"fieldname": "tasks_section",
"fieldtype": "Section Break",
"label": "Tasks"
},
{
"fieldname": "allocations_section",
"fieldtype": "Section Break",
"label": "Allocations"
},
{
"fieldname": "column_break_yhin",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4ywv",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.error_log",
"fieldname": "error_log",
"fieldtype": "Long Text",
"label": "Reconciliation Error Log",
"read_only": 1
},
{
"fieldname": "process_pr",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Parent Document",
"options": "Process Payment Reconciliation",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_fvdw",
"fieldtype": "Section Break",
"label": "Status"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Running\nPaused\nReconciled\nPartially Reconciled\nFailed\nCancelled",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-04-21 17:36:26.642617",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "allocated, reconciled, total_allocations, reconciled_entries",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ProcessPaymentReconciliationLog(Document):
pass

View File

@@ -1,15 +0,0 @@
frappe.listview_settings['Process Payment Reconciliation Log'] = {
add_fields: ["status"],
get_indicator: function(doc) {
var colors = {
'Partially Reconciled': 'orange',
'Paused': 'orange',
'Reconciled': 'green',
'Failed': 'red',
'Cancelled': 'red',
'Running': 'blue',
};
let status = doc.status;
return [__(status), colors[status], "status,=,"+status];
},
};

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestProcessPaymentReconciliationLog(FrappeTestCase):
pass

View File

@@ -1,170 +0,0 @@
{
"actions": [],
"creation": "2023-03-13 13:51:27.351463",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"reference_row",
"column_break_3",
"invoice_type",
"invoice_number",
"section_break_6",
"allocated_amount",
"unreconciled_amount",
"column_break_8",
"amount",
"is_advance",
"section_break_5",
"difference_amount",
"column_break_7",
"difference_account",
"exchange_rate",
"currency",
"reconciled"
],
"fields": [
{
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"options": "reference_type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Row",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "invoice_type",
"fieldtype": "Link",
"label": "Invoice Type",
"options": "DocType",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "invoice_number",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Invoice Number",
"options": "invoice_type",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "currency",
"reqd": 1
},
{
"fieldname": "unreconciled_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Unreconciled Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "is_advance",
"fieldtype": "Data",
"hidden": 1,
"label": "Is Advance",
"read_only": 1
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "difference_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Difference Amount",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "difference_account",
"fieldtype": "Link",
"label": "Difference Account",
"options": "Account",
"read_only": 1
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"default": "0",
"fieldname": "reconciled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Reconciled"
}
],
"istable": 1,
"links": [],
"modified": "2023-03-20 21:05:43.121945",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log Allocations",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ProcessPaymentReconciliationLogAllocations(Document):
pass

View File

@@ -1,6 +1,6 @@
<div class="page-break">
<div id="header-html" class="hidden-pdf">
{% if letter_head.content %}
{% if letter_head %}
<div class="letter-head text-center">{{ letter_head.content }}</div>
<hr style="height:2px;border-width:0;color:black;background-color:black;">
{% endif %}
@@ -15,12 +15,7 @@
</div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div>
{% if filters.party[0] == filters.party_name[0] %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
{% else %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party[0] }}</b></h5>
<h5 style="float: left; margin-left:15px">{{ _("Customer Name: ") }} <b>{{filters.party_name[0] }}</b></h5>
{% endif %}
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
<h5 style="float: right;">
{{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}}

View File

@@ -63,20 +63,6 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frm.set_value('to_date', frappe.datetime.get_today());
}
},
report: function(frm){
let filters = {
'company': frm.doc.company,
}
if(frm.doc.report == 'Accounts Receivable'){
filters['account_type'] = 'Receivable';
}
frm.set_query("account", function() {
return {
filters: filters
};
});
},
customer_collection: function(frm){
frm.set_value('collection_name', '');
if(frm.doc.customer_collection){

View File

@@ -6,24 +6,17 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"report",
"section_break_11",
"from_date",
"posting_date",
"company",
"account",
"group_by",
"cost_center",
"territory",
"column_break_14",
"to_date",
"finance_book",
"currency",
"project",
"payment_terms_template",
"sales_partner",
"sales_person",
"based_on_payment_terms",
"section_break_3",
"customer_collection",
"collection_name",
@@ -41,8 +34,6 @@
"terms_and_conditions",
"section_break_1",
"enable_auto_email",
"column_break_ocfq",
"sender",
"section_break_18",
"frequency",
"filter_duration",
@@ -72,14 +63,14 @@
"reqd": 1
},
{
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
"depends_on": "eval:doc.enable_auto_email == 0;",
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"mandatory_depends_on": "eval:doc.frequency == '';"
},
{
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
"depends_on": "eval:doc.enable_auto_email == 0;",
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
@@ -92,7 +83,6 @@
"options": "PSOA Cost Center"
},
{
"depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "project",
"fieldtype": "Table MultiSelect",
"label": "Project",
@@ -110,7 +100,7 @@
{
"fieldname": "section_break_11",
"fieldtype": "Section Break",
"label": "Report Filters"
"label": "General Ledger Filters"
},
{
"fieldname": "column_break_14",
@@ -170,14 +160,12 @@
},
{
"default": "Group by Voucher (Consolidated)",
"depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "group_by",
"fieldtype": "Select",
"label": "Group By",
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
},
{
"depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -296,82 +284,10 @@
"fieldtype": "Link",
"label": "Terms and Conditions",
"options": "Terms and Conditions"
},
{
"default": "1",
"fieldname": "include_break",
"fieldtype": "Check",
"label": "Page Break After Each SoA"
},
{
"default": "0",
"depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "show_net_values_in_party_account",
"fieldtype": "Check",
"label": "Show Net Values in Party Account"
},
{
"fieldname": "sender",
"fieldtype": "Link",
"label": "Sender",
"options": "Email Account"
},
{
"fieldname": "column_break_ocfq",
"fieldtype": "Column Break"
},
{
"fieldname": "report",
"fieldtype": "Select",
"label": "Report",
"options": "General Ledger\nAccounts Receivable",
"reqd": 1
},
{
"default": "Today",
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date"
},
{
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
"fieldname": "payment_terms_template",
"fieldtype": "Link",
"label": "Payment Terms Template",
"options": "Payment Terms Template"
},
{
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
"fieldname": "sales_partner",
"fieldtype": "Link",
"label": "Sales Partner",
"options": "Sales Partner"
},
{
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
"fieldname": "sales_person",
"fieldtype": "Link",
"label": "Sales Person",
"options": "Sales Person"
},
{
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
"fieldname": "territory",
"fieldtype": "Link",
"label": "Territory",
"options": "Territory"
},
{
"default": "0",
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
"fieldname": "based_on_payment_terms",
"fieldtype": "Check",
"label": "Based On Payment Terms"
}
],
"links": [],
"modified": "2023-06-23 10:13:15.051950",
"modified": "2021-09-06 21:00:45.732505",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -14,7 +14,6 @@ from frappe.www.printview import get_print_style
from erpnext import get_company_currency
from erpnext.accounts.party import get_party_account_currency
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute as get_ar_soa
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import (
execute as get_ageing,
)
@@ -24,7 +23,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document):
def validate(self):
if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
self.subject = "Statement Of Accounts for {{ customer.name }}"
if not self.body:
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
@@ -43,10 +42,29 @@ class ProcessStatementOfAccounts(Document):
def get_report_pdf(doc, consolidated=True):
statement_dict = {}
ageing = ""
base_template_path = "frappe/www/printview.html"
template_path = (
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
)
for entry in doc.customers:
if doc.include_ageing:
ageing = set_ageing(doc, entry)
ageing_filters = frappe._dict(
{
"company": doc.company,
"report_date": doc.to_date,
"ageing_based_on": doc.ageing_based_on,
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"customer": entry.customer,
}
)
col1, ageing = get_ageing(ageing_filters)
if ageing:
ageing[0]["ageing_based_on"] = doc.ageing_based_on
tax_id = frappe.get_doc("Customer", entry.customer).tax_id
presentation_currency = (
@@ -54,25 +72,58 @@ def get_report_pdf(doc, consolidated=True):
or doc.currency
or get_company_currency(doc.company)
)
if doc.letter_head:
from frappe.www.printview import get_letter_head
filters = get_common_filters(doc)
letter_head = get_letter_head(doc, 0)
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
else:
filters.update(get_ar_filters(doc, entry))
filters = frappe._dict(
{
"from_date": doc.from_date,
"to_date": doc.to_date,
"company": doc.company,
"finance_book": doc.finance_book if doc.finance_book else None,
"account": [doc.account] if doc.account else None,
"party_type": "Customer",
"party": [entry.customer],
"presentation_currency": presentation_currency,
"group_by": doc.group_by,
"currency": doc.currency,
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
"project": [p.project_name for p in doc.project],
"show_opening_entries": 0,
"include_default_book_entries": 0,
"tax_id": tax_id if tax_id else None,
}
)
col, res = get_soa(filters)
if doc.report == "General Ledger":
col, res = get_soa(filters)
for x in [0, -2, -1]:
res[x]["account"] = res[x]["account"].replace("'", "")
if len(res) == 3:
continue
else:
ar_res = get_ar_soa(filters)
col, res = ar_res[0], ar_res[1]
for x in [0, -2, -1]:
res[x]["account"] = res[x]["account"].replace("'", "")
statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
if len(res) == 3:
continue
html = frappe.render_template(
template_path,
{
"filters": filters,
"data": res,
"ageing": ageing[0] if (doc.include_ageing and ageing) else None,
"letter_head": letter_head if doc.letter_head else None,
"terms_and_conditions": frappe.db.get_value(
"Terms and Conditions", doc.terms_and_conditions, "terms"
)
if doc.terms_and_conditions
else None,
},
)
html = frappe.render_template(
base_template_path,
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
)
statement_dict[entry.customer] = html
if not bool(statement_dict):
return False
@@ -85,110 +136,6 @@ def get_report_pdf(doc, consolidated=True):
return statement_dict
def set_ageing(doc, entry):
ageing_filters = frappe._dict(
{
"company": doc.company,
"report_date": doc.to_date,
"ageing_based_on": doc.ageing_based_on,
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"customer": entry.customer,
}
)
col1, ageing = get_ageing(ageing_filters)
if ageing:
ageing[0]["ageing_based_on"] = doc.ageing_based_on
return ageing
def get_common_filters(doc):
return frappe._dict(
{
"company": doc.company,
"finance_book": doc.finance_book if doc.finance_book else None,
"account": [doc.account] if doc.account else None,
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
}
)
def get_gl_filters(doc, entry, tax_id, presentation_currency):
return {
"from_date": doc.from_date,
"to_date": doc.to_date,
"party_type": "Customer",
"party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency,
"group_by": doc.group_by,
"currency": doc.currency,
"project": [p.project_name for p in doc.project],
"show_opening_entries": 0,
"include_default_book_entries": 0,
"tax_id": tax_id if tax_id else None,
"show_net_values_in_party_account": doc.show_net_values_in_party_account,
}
def get_ar_filters(doc, entry):
return {
"report_date": doc.posting_date if doc.posting_date else None,
"customer": entry.customer,
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
"sales_partner": doc.sales_partner if doc.sales_partner else None,
"sales_person": doc.sales_person if doc.sales_person else None,
"territory": doc.territory if doc.territory else None,
"based_on_payment_terms": doc.based_on_payment_terms,
"report_name": "Accounts Receivable",
"ageing_based_on": doc.ageing_based_on,
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
def get_html(doc, filters, entry, col, res, ageing):
base_template_path = "frappe/www/printview.html"
template_path = (
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
if doc.report == "General Ledger"
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
)
if doc.letter_head:
from frappe.www.printview import get_letter_head
letter_head = get_letter_head(doc, 0)
html = frappe.render_template(
template_path,
{
"filters": filters,
"data": res,
"report": {"report_name": doc.report, "columns": col},
"ageing": ageing[0] if (doc.include_ageing and ageing) else None,
"letter_head": letter_head if doc.letter_head else None,
"terms_and_conditions": frappe.db.get_value(
"Terms and Conditions", doc.terms_and_conditions, "terms"
)
if doc.terms_and_conditions
else None,
},
)
html = frappe.render_template(
base_template_path,
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
)
return html
def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name):
fields_dict = {
"Customer Group": "customer_group",
@@ -206,7 +153,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
]
return frappe.get_list(
"Customer",
fields=["name", "customer_name", "email_id"],
fields=["name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]],
)
@@ -229,7 +176,7 @@ def get_customers_based_on_sales_person(sales_person):
if sales_person_records.get("Customer"):
return frappe.get_list(
"Customer",
fields=["name", "customer_name", "email_id"],
fields=["name", "email_id"],
filters=[["name", "in", list(sales_person_records["Customer"])]],
)
else:
@@ -278,7 +225,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == "Sales Partner":
customers = frappe.get_list(
"Customer",
fields=["name", "customer_name", "email_id"],
fields=["name", "email_id"],
filters=[["default_sales_partner", "=", collection_name]],
)
else:
@@ -297,12 +244,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
continue
customer_list.append(
{
"name": customer.name,
"customer_name": customer.customer_name,
"primary_email": primary_email,
"billing_email": billing_email,
}
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
)
return customer_list
@@ -379,7 +321,7 @@ def send_emails(document_name, from_scheduler=False):
queue="short",
method=frappe.sendmail,
recipients=recipients,
sender=doc.sender or frappe.session.user,
sender=frappe.session.user,
cc=cc,
subject=subject,
message=message,

View File

@@ -1,344 +0,0 @@
<style>
.print-format {
padding: 4mm;
font-size: 8.0pt !important;
}
.print-format td {
vertical-align:middle !important;
}
</style>
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
<h4 class="text-center">
{{ filters.customer }}
</h4>
<h6 class="text-center">
{% if (filters.tax_id) %}
{{ _("Tax Id: ") }}{{ filters.tax_id }}
{% endif %}
</h6>
<h5 class="text-center">
{{ _(filters.ageing_based_on) }}
{{ _("Until") }}
{{ frappe.format(filters.report_date, 'Date') }}
</h5>
<div class="clearfix">
<div class="pull-left">
{% if(filters.payment_terms) %}
<strong>{{ _("Payment Terms") }}:</strong> {{ filters.payment_terms }}
{% endif %}
</div>
<div class="pull-right">
{% if(filters.credit_limit) %}
<strong>{{ _("Credit Limit") }}:</strong> {{ frappe.utils.fmt_money(filters.credit_limit) }}
{% endif %}
</div>
</div>
{% if(filters.show_future_payments) %}
{% set balance_row = data.slice(-1).pop() %}
{% for i in report.columns %}
{% if i.fieldname == 'age' %}
{% set elem = i %}
{% endif %}
{% endfor %}
{% set start = report.columns.findIndex(elem) %}
{% set range1 = report.columns[start].label %}
{% set range2 = report.columns[start+1].label %}
{% set range3 = report.columns[start+2].label %}
{% set range4 = report.columns[start+3].label %}
{% set range5 = report.columns[start+4].label %}
{% set range6 = report.columns[start+5].label %}
{% if(balance_row) %}
<table class="table table-bordered table-condensed">
<caption class="text-right">(Amount in {{ data[0]["currency"] ~ "" }})</caption>
<colgroup>
<col style="width: 30mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
</colgroup>
<thead>
<tr>
<th>{{ _(" ") }}</th>
<th>{{ _(range1) }}</th>
<th>{{ _(range2) }}</th>
<th>{{ _(range3) }}</th>
<th>{{ _(range4) }}</th>
<th>{{ _(range5) }}</th>
<th>{{ _(range6) }}</th>
<th>{{ _("Total") }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ _("Total Outstanding") }}</td>
<td class="text-right">
{{ format_number(balance_row["age"], null, 2) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
</td>
<td class="text-right">
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
</td>
</tr>
<td>{{ _("Future Payments") }}</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="text-right">
{{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
</td>
<tr class="cvs-footer">
<th class="text-left">{{ _("Cheques Required") }}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th class="text-right">
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
</tr>
</tbody>
</table>
{% endif %}
{% endif %}
<table class="table table-bordered">
<thead>
<tr>
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
<th style="width: 10%">{{ _("Date") }}</th>
<th style="width: 4%">{{ _("Age (Days)") }}</th>
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<th style="width: 14%">{{ _("Reference") }}</th>
<th style="width: 10%">{{ _("Sales Person") }}</th>
{% else %}
<th style="width: 24%">{{ _("Reference") }}</th>
{% endif %}
{% if not(filters.show_future_payments) %}
<th style="width: 20%">
{% if (filters.customer or filters.supplier or filters.customer_name) %}
{{ _("Remarks") }}
{% else %}
{{ _("Party") }}
{% endif %}
</th>
{% endif %}
<th style="width: 10%; text-align: right">{{ _("Invoiced Amount") }}</th>
{% if not(filters.show_future_payments) %}
<th style="width: 10%; text-align: right">{{ _("Paid Amount") }}</th>
<th style="width: 10%; text-align: right">
{% if report.report_name == "Accounts Receivable" %}
{{ _('Credit Note') }}
{% else %}
{{ _('Debit Note') }}
{% endif %}
</th>
{% endif %}
<th style="width: 10%; text-align: right">{{ _("Outstanding Amount") }}</th>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<th style="width: 12%">{{ _("Customer LPO No.") }}</th>
{% endif %}
<th style="width: 10%">{{ _("Future Payment Ref") }}</th>
<th style="width: 10%">{{ _("Future Payment Amount") }}</th>
<th style="width: 10%">{{ _("Remaining Balance") }}</th>
{% endif %}
{% else %}
<th style="width: 40%">
{% if (filters.customer or filters.supplier or filters.customer_name) %}
{{ _("Remarks")}}
{% else %}
{{ _("Party") }}
{% endif %}
</th>
<th style="width: 15%">{{ _("Total Invoiced Amount") }}</th>
<th style="width: 15%">{{ _("Total Paid Amount") }}</th>
<th style="width: 15%">
{% if report.report_name == "Accounts Receivable Summary" %}
{{ _('Credit Note Amount') }}
{% else %}
{{ _('Debit Note Amount') }}
{% endif %}
</th>
<th style="width: 15%">{{ _("Total Outstanding Amount") }}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for i in range(data|length) %}
<tr>
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
{% if(data[i]["party"]) %}
<td>{{ (data[i]["posting_date"]) }}</td>
<td style="text-align: right">{{ data[i]["age"] }}</td>
<td>
{% if not(filters.show_future_payments) %}
{{ data[i]["voucher_type"] }}
<br>
{% endif %}
{{ data[i]["voucher_no"] }}
</td>
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<td>{{ data[i]["sales_person"] }}</td>
{% endif %}
{% if not (filters.show_future_payments) %}
<td>
{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
{{ data[i]["party"] }}
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
<br> {{ data[i]["customer_name"] }}
{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
<br> {{ data[i]["supplier_name"] }}
{% endif %}
{% endif %}
<div>
{% if data[i]["remarks"] %}
{{ _("Remarks") }}:
{{ data[i]["remarks"] }}
{% endif %}
</div>
</td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
{% if not(filters.show_future_payments) %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<td style="text-align: right">
{{ data[i]["po_no"] }}</td>
{% endif %}
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
{% endif %}
{% else %}
<td></td>
{% if not(filters.show_future_payments) %}
<td></td>
{% endif %}
{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
<td></td>
{% endif %}
<td></td>
<td style="text-align: right"><b>{{ _("Total") }}</b></td>
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }}</td>
{% if not(filters.show_future_payments) %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} </td>
{% endif %}
<td style="text-align: right">
{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
{% if(filters.show_future_payments) %}
{% if(report.report_name == "Accounts Receivable") %}
<td style="text-align: right">
{{ data[i]["po_no"] }}</td>
{% endif %}
<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
{% endif %}
{% endif %}
{% else %}
{% if(data[i]["party"] or "&nbsp;") %}
{% if not(data[i]["is_total_row"]) %}
<td>
{% if(not(filters.customer | filters.supplier)) %}
{{ data[i]["party"] }}
{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
<br> {{ data[i]["customer_name"] }}
{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
<br> {{ data[i]["supplier_name"] }}
{% endif %}
{% endif %}
<br>{{ _("Remarks") }}:
{{ data[i]["remarks"] }}
</td>
{% else %}
<td><b>{{ _("Total") }}</b></td>
{% endif %}
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
{% endif %}
{% endif %}
</tr>
{% endfor %}
<td></td>
<td></td>
<td></td>
<td></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
</tbody>
</table>
<br>
{% if ageing %}
<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
{{ _("up to " ) }} {{ frappe.format(filters.report_date, 'Date')}}
</h4>
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">30 Days</th>
<th style="width: 25%">60 Days</th>
<th style="width: 25%">90 Days</th>
<th style="width: 25%">120 Days</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
</tr>
</tbody>
</table>
{% endif %}
<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>

View File

@@ -1,12 +1,12 @@
{
"actions": [],
"allow_workflow": 1,
"creation": "2020-08-03 16:35:21.852178",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
"customer_name",
"billing_email",
"primary_email"
],
@@ -27,21 +27,14 @@
},
{
"fieldname": "billing_email",
"fieldtype": "Data",
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Billing Email"
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2023-04-26 13:02:41.964499",
"modified": "2020-08-03 22:55:38.875601",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts Customer",
@@ -50,6 +43,5 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -82,11 +82,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
if(doc.docstatus == 1 && doc.outstanding_amount != 0
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
__('Create')
);
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}
@@ -303,7 +299,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
apply_tds(frm) {
var me = this;
me.frm.set_value("tax_withheld_vouchers", []);
if (!me.frm.doc.apply_tds) {
me.frm.set_value("tax_withholding_category", '');
me.frm.set_df_property("tax_withholding_category", "hidden", 1);

View File

@@ -89,7 +89,6 @@
"column_break8",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance",
@@ -119,7 +118,6 @@
"paid_amount",
"advances_section",
"allocate_advances_automatically",
"only_include_allocated_payments",
"get_advances",
"advances",
"advance_tax",
@@ -547,7 +545,6 @@
"depends_on": "update_stock",
"fieldname": "rejected_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Rejected Warehouse",
"no_copy": 1,
"options": "Warehouse",
@@ -1370,7 +1367,6 @@
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
"ignore_user_permissions": 1,
"width": "50px"
},
{
@@ -1554,30 +1550,17 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
},
{
"default": "0",
"depends_on": "allocate_advances_automatically",
"description": "Advance payments allocated against orders will only be fetched",
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
},
{
"default": "0",
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company Default Round Off Cost Center"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-07-04 17:23:59.145031",
"modified": "2023-01-28 19:18:56.586321",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [

View File

@@ -117,7 +117,7 @@ class PurchaseInvoice(BuyingController):
self.validate_expense_account()
self.set_against_expense_account()
self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
self.create_remarks()
self.set_status()
self.validate_purchase_receipt_if_update_stock()
@@ -581,7 +581,6 @@ class PurchaseInvoice(BuyingController):
self.make_supplier_gl_entry(gl_entries)
self.make_item_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries)
if self.check_asset_cwip_enabled():
self.get_asset_gl_entry(gl_entries)
@@ -1364,7 +1363,7 @@ class PurchaseInvoice(BuyingController):
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
self.company, "Purchase Invoice", self.name
)
gl_entries.append(
@@ -1374,9 +1373,7 @@ class PurchaseInvoice(BuyingController):
"against": self.supplier,
"debit_in_account_currency": self.rounding_adjustment,
"debit": self.base_rounding_adjustment,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else (self.cost_center or round_off_cost_center),
"cost_center": self.cost_center or round_off_cost_center,
},
item=self,
)

View File

@@ -637,6 +637,13 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
gle_filters={"account": "Stock In Hand - TCP1"},
)
# assert loss booked in COGS
self.assertGLEs(
return_pi,
[{"credit": 0, "debit": 200}],
gle_filters={"account": "Cost of Goods Sold - TCP1"},
)
def test_return_with_lcv(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
@@ -1655,67 +1662,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
self.assertTrue(return_pi.docstatus == 1)
def test_gl_entries_for_standalone_debit_note(self):
make_purchase_invoice(qty=5, rate=500, update_stock=True)
returned_inv = make_purchase_invoice(qty=-5, rate=5, update_stock=True, is_return=True)
# override the rate with valuation rate
sle = frappe.get_all(
"Stock Ledger Entry",
fields=["stock_value_difference", "actual_qty"],
filters={"voucher_no": returned_inv.name},
)[0]
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
automatically_fetch_payment_terms()
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
0,
)
po = create_purchase_order(do_not_save=1)
po.payment_terms_template = "_Test Payment Term Template"
po.save()
po.submit()
pr = create_pr_against_po(po.name, received_qty=4)
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
1,
)
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
automatically_fetch_payment_terms(enable=0)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
"allocate_payment_based_on_payment_terms",
0,
)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@@ -176,7 +176,6 @@
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty",
"no_copy": 1,
"read_only": 1
},
{
@@ -421,7 +420,6 @@
{
"fieldname": "rejected_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Rejected Warehouse",
"options": "Warehouse"
},
@@ -882,7 +880,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-07-04 17:22:21.501152",
"modified": "2022-11-29 13:01:20.438217",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -892,4 +890,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -93,12 +93,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
if (doc.docstatus == 1 && doc.outstanding_amount!=0
&& !(cint(doc.is_return) && doc.return_against)) {
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
__('Create')
);
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
cur_frm.add_custom_button(__('Payment'),
this.make_payment_entry, __('Create'));
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if(doc.docstatus==1 && !doc.is_return) {
@@ -334,7 +331,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
make_inter_company_invoice() {
let me = this;
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
frm: me.frm
@@ -670,6 +666,19 @@ frappe.ui.form.on('Sales Invoice', {
}
}
// expense account
frm.fields_dict['items'].grid.get_field('expense_account').get_query = function(doc) {
if (erpnext.is_perpetual_inventory_enabled(doc.company)) {
return {
filters: {
'report_type': 'Profit and Loss',
'company': doc.company,
"is_group": 0
}
}
}
}
// discount account
frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) {
return {
@@ -887,8 +896,6 @@ frappe.ui.form.on('Sales Invoice', {
frm.events.append_time_log(frm, timesheet, 1.0);
}
});
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");
},
async get_exchange_rate(frm, from_currency, to_currency) {
@@ -928,6 +935,9 @@ frappe.ui.form.on('Sales Invoice', {
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
row.timesheet_detail = time_log.name;
row.project_name = time_log.project_name;
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");
},
calculate_timesheet_totals: function(frm) {

View File

@@ -32,6 +32,9 @@
"cost_center",
"dimension_col_break",
"project",
"column_break_27",
"campaign",
"source",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -79,7 +82,6 @@
"column_break5",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance",
@@ -121,7 +123,6 @@
"account_for_change_amount",
"advances_section",
"allocate_advances_automatically",
"only_include_allocated_payments",
"get_advances",
"advances",
"write_off_section",
@@ -202,9 +203,7 @@
"more_information",
"status",
"inter_company_invoice_reference",
"campaign",
"represents_company",
"source",
"customer_group",
"col_break23",
"is_internal_customer",
@@ -320,7 +319,6 @@
},
{
"default": "0",
"depends_on": "eval: !doc.is_debit_note",
"fieldname": "is_return",
"fieldtype": "Check",
"hide_days": 1,
@@ -1960,7 +1958,6 @@
},
{
"default": "0",
"depends_on": "eval: !doc.is_return",
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
"fieldname": "is_debit_note",
"fieldtype": "Check",
@@ -2086,6 +2083,10 @@
"fieldname": "company_addr_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_52",
"fieldtype": "Column Break"
@@ -2130,19 +2131,6 @@
"label": "Repost Required",
"no_copy": 1,
"read_only": 1
},
{
"depends_on": "allocate_advances_automatically",
"description": "Advance payments allocated against orders will only be fetched",
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
},
{
"default": "0",
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company default Cost Center for Round off"
}
],
"icon": "fa fa-file-text",
@@ -2155,10 +2143,11 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-06-19 16:02:05.309332",
"modified": "2022-11-07 16:02:07.972258",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [

View File

@@ -145,7 +145,7 @@ class SalesInvoice(SellingController):
self.set_against_income_account()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
if not self.is_return:
self.validate_serial_numbers()
else:
@@ -1012,16 +1012,10 @@ class SalesInvoice(SellingController):
def check_prev_docstatus(self):
for d in self.get("items"):
if (
d.sales_order
and frappe.db.get_value("Sales Order", d.sales_order, "docstatus", cache=True) != 1
):
if d.sales_order and frappe.db.get_value("Sales Order", d.sales_order, "docstatus") != 1:
frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order))
if (
d.delivery_note
and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus", cache=True) != 1
):
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def make_gl_entries(self, gl_entries=None, from_repost=False):
@@ -1075,7 +1069,6 @@ class SalesInvoice(SellingController):
self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries)
self.make_discount_gl_entries(gl_entries)
# merge gl entries before adding pos entries
@@ -1186,12 +1179,7 @@ class SalesInvoice(SellingController):
if self.is_return:
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
)
asset.db_set("disposal_date", None)
@@ -1206,12 +1194,7 @@ class SalesInvoice(SellingController):
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
)
asset.db_set("disposal_date", self.posting_date)
@@ -1467,7 +1450,7 @@ class SalesInvoice(SellingController):
and not self.is_internal_transfer()
):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
self.company, "Sales Invoice", self.name
)
gl_entries.append(
@@ -1479,9 +1462,7 @@ class SalesInvoice(SellingController):
self.rounding_adjustment, self.precision("rounding_adjustment")
),
"credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")),
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else (self.cost_center or round_off_cost_center),
"cost_center": self.cost_center or round_off_cost_center,
},
item=self,
)

View File

@@ -15,7 +15,6 @@ def get_data():
},
"internal_links": {
"Sales Order": ["items", "sales_order"],
"Delivery Note": ["items", "delivery_note"],
"Timesheet": ["timesheets", "time_sheet"],
},
"transactions": [

View File

@@ -1900,22 +1900,16 @@ class TestSalesInvoice(unittest.TestCase):
si = self.create_si_to_test_tax_breakup()
itemised_tax_data = get_itemised_tax_breakup_data(si)
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
expected_itemised_tax = [
{
"item": "_Test Item",
"taxable_amount": 10000.0,
"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0},
},
{
"item": "_Test Item 2",
"taxable_amount": 5000.0,
"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0},
},
]
expected_itemised_tax = {
"_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}},
"_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}},
}
expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0}
self.assertEqual(itemised_tax_data, expected_itemised_tax)
self.assertEqual(itemised_tax, expected_itemised_tax)
self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount)
frappe.flags.country = None
@@ -2049,27 +2043,28 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.total_taxes_and_charges, 228.82)
self.assertEqual(si.rounding_adjustment, -0.01)
expected_values = [
["_Test Account Service Tax - _TC", 0.0, 114.41],
["_Test Account VAT - _TC", 0.0, 114.41],
[si.debit_to, 1500, 0.0],
["Round Off - _TC", 0.01, 0.01],
["Sales - _TC", 0.0, 1271.18],
]
expected_values = dict(
(d[0], d)
for d in [
[si.debit_to, 1500, 0.0],
["_Test Account Service Tax - _TC", 0.0, 114.41],
["_Test Account VAT - _TC", 0.0, 114.41],
["Sales - _TC", 0.0, 1271.18],
]
)
gl_entries = frappe.db.sql(
"""select account, sum(debit) as debit, sum(credit) as credit
"""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
group by account
order by account asc""",
si.name,
as_dict=1,
)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_rounding_adjustment_3(self):
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
@@ -2124,14 +2119,13 @@ class TestSalesInvoice(unittest.TestCase):
["_Test Account Service Tax - _TC", 0.0, 240.43],
["_Test Account VAT - _TC", 0.0, 240.43],
["Sales - _TC", 0.0, 4007.15],
["Round Off - _TC", 0.02, 0.01],
["Round Off - _TC", 0.01, 0],
]
)
gl_entries = frappe.db.sql(
"""select account, sum(debit) as debit, sum(credit) as credit
"""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
group by account
order by account asc""",
si.name,
as_dict=1,
@@ -3316,13 +3310,6 @@ class TestSalesInvoice(unittest.TestCase):
)
self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_return_negative_rate(self):
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
self.assertRaises(frappe.ValidationError, si.save)
si.items[0].rate = 10
si.save()
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()

View File

@@ -603,8 +603,7 @@
"in_list_view": 1,
"label": "Batch No",
"options": "Batch",
"print_hide": 1,
"search_index": 1
"print_hide": 1
},
{
"fieldname": "col_break5",
@@ -891,7 +890,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-07-25 11:58:10.723833",
"modified": "2022-10-17 12:51:44.825398",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -1,5 +1,4 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2017-12-25 16:50:53.878430",
"doctype": "DocType",
@@ -112,12 +111,11 @@
"read_only": 1
}
],
"links": [],
"modified": "2023-04-10 22:02:20.406087",
"modified": "2019-11-17 23:24:11.395882",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Shareholder",
"naming_rule": "By \"Naming Series\" field",
"name_case": "Title Case",
"owner": "Administrator",
"permissions": [
{
@@ -160,7 +158,6 @@
"search_fields": "folio_no",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}

Some files were not shown because too many files have changed in this diff Show More