Merge branch 'frappe:develop' into rounded-row-wise-tax
This commit is contained in:
commit
6a27cbd61d
18
.github/helper/install.sh
vendored
18
.github/helper/install.sh
vendored
@ -4,7 +4,9 @@ set -e
|
|||||||
|
|
||||||
cd ~ || exit
|
cd ~ || exit
|
||||||
|
|
||||||
sudo apt update && sudo apt install redis-server libcups2-dev
|
sudo apt update
|
||||||
|
sudo apt remove mysql-server mysql-client
|
||||||
|
sudo apt install libcups2-dev redis-server mariadb-client-10.6
|
||||||
|
|
||||||
pip install frappe-bench
|
pip install frappe-bench
|
||||||
|
|
||||||
@ -25,14 +27,14 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
if [ "$DB" == "mariadb" ];then
|
if [ "$DB" == "mariadb" ];then
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||||
|
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
||||||
|
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$DB" == "postgres" ];then
|
if [ "$DB" == "postgres" ];then
|
||||||
@ -68,6 +70,6 @@ if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
|||||||
|
|
||||||
wait $wkpid
|
wait $wkpid
|
||||||
|
|
||||||
bench start &> bench_run_logs.txt &
|
bench start &>> ~/frappe-bench/bench_start.log &
|
||||||
CI=Yes bench build --app frappe &
|
CI=Yes bench build --app frappe &
|
||||||
bench --site test_site reinstall --yes
|
bench --site test_site reinstall --yes
|
||||||
|
20
.github/workflows/initiate_release.yml
vendored
20
.github/workflows/initiate_release.yml
vendored
@ -30,23 +30,3 @@ jobs:
|
|||||||
head: version-${{ matrix.version }}-hotfix
|
head: version-${{ matrix.version }}-hotfix
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
|
||||||
beta-release:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: octokit/request-action@v2.x
|
|
||||||
with:
|
|
||||||
route: POST /repos/{owner}/{repo}/pulls
|
|
||||||
owner: frappe
|
|
||||||
repo: erpnext
|
|
||||||
title: |-
|
|
||||||
"chore: release v15 beta"
|
|
||||||
body: "Automated beta release."
|
|
||||||
base: version-15-beta
|
|
||||||
head: develop
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
|
66
.github/workflows/patch.yml
vendored
66
.github/workflows/patch.yml
vendored
@ -23,12 +23,12 @@ jobs:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: mariadb:10.3
|
image: mariadb:10.6
|
||||||
env:
|
env:
|
||||||
MARIADB_ROOT_PASSWORD: 'root'
|
MARIADB_ROOT_PASSWORD: 'root'
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
@ -45,9 +45,7 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: "actions/setup-python@v4"
|
uses: "actions/setup-python@v4"
|
||||||
with:
|
with:
|
||||||
python-version: |
|
python-version: '3.10'
|
||||||
3.7
|
|
||||||
3.10
|
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
@ -102,40 +100,60 @@ jobs:
|
|||||||
- name: Run Patch Tests
|
- name: Run Patch Tests
|
||||||
run: |
|
run: |
|
||||||
cd ~/frappe-bench/
|
cd ~/frappe-bench/
|
||||||
wget https://erpnext.com/files/v10-erpnext.sql.gz
|
bench remove-app payments --force
|
||||||
bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
|
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
|
||||||
|
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
|
||||||
|
|
||||||
|
wget https://erpnext.com/files/v13-erpnext.sql.gz
|
||||||
|
bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
|
||||||
|
|
||||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
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
|
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||||
|
|
||||||
for version in $(seq 12 13)
|
|
||||||
do
|
|
||||||
echo "Updating to v$version"
|
|
||||||
branch_name="version-$version-hotfix"
|
|
||||||
|
|
||||||
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
|
function update_to_version() {
|
||||||
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
|
version=$1
|
||||||
|
|
||||||
git -C "apps/frappe" checkout -q -f $branch_name
|
branch_name="version-$version-hotfix"
|
||||||
git -C "apps/erpnext" checkout -q -f $branch_name
|
echo "Updating to v$version"
|
||||||
|
|
||||||
rm -rf ~/frappe-bench/env
|
# Fetch and checkout branches
|
||||||
bench setup env --python python3.7
|
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
|
||||||
bench pip install -e ./apps/payments
|
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
|
||||||
bench pip install -e ./apps/erpnext
|
git -C "apps/frappe" checkout -q -f $branch_name
|
||||||
|
git -C "apps/erpnext" checkout -q -f $branch_name
|
||||||
|
|
||||||
bench --site test_site migrate
|
# Resetup env and install apps
|
||||||
done
|
pgrep honcho | xargs kill
|
||||||
|
rm -rf ~/frappe-bench/env
|
||||||
|
bench -v setup env
|
||||||
|
bench pip install -e ./apps/erpnext
|
||||||
|
bench start &>> ~/frappe-bench/bench_start.log &
|
||||||
|
|
||||||
|
bench --site test_site migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
update_to_version 14
|
||||||
|
|
||||||
echo "Updating to latest version"
|
echo "Updating to latest version"
|
||||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||||
|
|
||||||
|
pgrep honcho | xargs kill
|
||||||
rm -rf ~/frappe-bench/env
|
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
|
bench pip install -e ./apps/erpnext
|
||||||
|
bench start &>> ~/frappe-bench/bench_start.log &
|
||||||
|
|
||||||
bench --site test_site migrate
|
bench --site test_site migrate
|
||||||
bench --site test_site install-app payments
|
|
||||||
|
- name: Show bench output
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
cd ~/frappe-bench
|
||||||
|
cat bench_start.log || true
|
||||||
|
cd logs
|
||||||
|
for f in ./*.log*; do
|
||||||
|
echo "Printing log: $f";
|
||||||
|
cat $f
|
||||||
|
done
|
||||||
|
6
.github/workflows/server-tests-mariadb.yml
vendored
6
.github/workflows/server-tests-mariadb.yml
vendored
@ -47,7 +47,7 @@ jobs:
|
|||||||
MARIADB_ROOT_PASSWORD: 'root'
|
MARIADB_ROOT_PASSWORD: 'root'
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
@ -123,6 +123,10 @@ jobs:
|
|||||||
CI_BUILD_ID: ${{ github.run_id }}
|
CI_BUILD_ID: ${{ github.run_id }}
|
||||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||||
|
|
||||||
|
- name: Show bench output
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: cat ~/frappe-bench/bench_start.log || true
|
||||||
|
|
||||||
- name: Upload coverage data
|
- name: Upload coverage data
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
@ -40,6 +40,7 @@ repos:
|
|||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies: [
|
additional_dependencies: [
|
||||||
'flake8-bugbear',
|
'flake8-bugbear',
|
||||||
|
'flake8-tuple',
|
||||||
]
|
]
|
||||||
args: ['--config', '.github/helper/.flake8_strict']
|
args: ['--config', '.github/helper/.flake8_strict']
|
||||||
exclude: ".*setup.py$"
|
exclude: ".*setup.py$"
|
||||||
|
@ -137,9 +137,6 @@ frappe.ui.form.on("Account", {
|
|||||||
args: {
|
args: {
|
||||||
old: frm.doc.name,
|
old: frm.doc.name,
|
||||||
new: data.name,
|
new: data.name,
|
||||||
is_group: frm.doc.is_group,
|
|
||||||
root_type: frm.doc.root_type,
|
|
||||||
company: frm.doc.company,
|
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (!r.exc) {
|
if (!r.exc) {
|
||||||
|
@ -18,6 +18,10 @@ class BalanceMismatchError(frappe.ValidationError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAccountMergeError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Account(NestedSet):
|
class Account(NestedSet):
|
||||||
nsm_parent_field = "parent_account"
|
nsm_parent_field = "parent_account"
|
||||||
|
|
||||||
@ -460,25 +464,34 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def merge_account(old, new, is_group, root_type, company):
|
def merge_account(old, new):
|
||||||
# Validate properties before merging
|
# Validate properties before merging
|
||||||
new_account = frappe.get_cached_doc("Account", new)
|
new_account = frappe.get_cached_doc("Account", new)
|
||||||
|
old_account = frappe.get_cached_doc("Account", old)
|
||||||
|
|
||||||
if not new_account:
|
if not new_account:
|
||||||
throw(_("Account {0} does not exist").format(new))
|
throw(_("Account {0} does not exist").format(new))
|
||||||
|
|
||||||
if (new_account.is_group, new_account.root_type, new_account.company) != (
|
if (
|
||||||
cint(is_group),
|
cint(new_account.is_group),
|
||||||
root_type,
|
new_account.root_type,
|
||||||
company,
|
new_account.company,
|
||||||
|
cstr(new_account.account_currency),
|
||||||
|
) != (
|
||||||
|
cint(old_account.is_group),
|
||||||
|
old_account.root_type,
|
||||||
|
old_account.company,
|
||||||
|
cstr(old_account.account_currency),
|
||||||
):
|
):
|
||||||
throw(
|
throw(
|
||||||
_(
|
msg=_(
|
||||||
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
|
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency"""
|
||||||
)
|
),
|
||||||
|
title=("Invalid Accounts"),
|
||||||
|
exc=InvalidAccountMergeError,
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_group and new_account.parent_account == old:
|
if old_account.is_group and new_account.parent_account == old:
|
||||||
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account"))
|
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account"))
|
||||||
|
|
||||||
frappe.rename_doc("Account", old, new, merge=1, force=1)
|
frappe.rename_doc("Account", old, new, merge=1, force=1)
|
||||||
|
@ -1,289 +0,0 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
"""
|
|
||||||
Import chart of accounts from OpenERP sources
|
|
||||||
"""
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from xml.etree import ElementTree as ET
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.utils.csvutils import read_csv_content
|
|
||||||
|
|
||||||
path = "/Users/nabinhait/projects/odoo/addons"
|
|
||||||
|
|
||||||
accounts = {}
|
|
||||||
charts = {}
|
|
||||||
all_account_types = []
|
|
||||||
all_roots = {}
|
|
||||||
|
|
||||||
|
|
||||||
def go():
|
|
||||||
global accounts, charts
|
|
||||||
default_account_types = get_default_account_types()
|
|
||||||
|
|
||||||
country_dirs = []
|
|
||||||
for basepath, folders, files in os.walk(path):
|
|
||||||
basename = os.path.basename(basepath)
|
|
||||||
if basename.startswith("l10n_"):
|
|
||||||
country_dirs.append(basename)
|
|
||||||
|
|
||||||
for country_dir in country_dirs:
|
|
||||||
accounts, charts = {}, {}
|
|
||||||
country_path = os.path.join(path, country_dir)
|
|
||||||
manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read())
|
|
||||||
data_files = (
|
|
||||||
manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", [])
|
|
||||||
)
|
|
||||||
files_path = [os.path.join(country_path, d) for d in data_files]
|
|
||||||
xml_roots = get_xml_roots(files_path)
|
|
||||||
csv_content = get_csv_contents(files_path)
|
|
||||||
prefix = country_dir if csv_content else None
|
|
||||||
account_types = get_account_types(
|
|
||||||
xml_roots.get("account.account.type", []), csv_content.get("account.account.type", []), prefix
|
|
||||||
)
|
|
||||||
account_types.update(default_account_types)
|
|
||||||
|
|
||||||
if xml_roots:
|
|
||||||
make_maps_for_xml(xml_roots, account_types, country_dir)
|
|
||||||
|
|
||||||
if csv_content:
|
|
||||||
make_maps_for_csv(csv_content, account_types, country_dir)
|
|
||||||
make_account_trees()
|
|
||||||
make_charts()
|
|
||||||
|
|
||||||
create_all_roots_file()
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_account_types():
|
|
||||||
default_types_root = []
|
|
||||||
default_types_root.append(
|
|
||||||
ET.parse(os.path.join(path, "account", "data", "data_account_type.xml")).getroot()
|
|
||||||
)
|
|
||||||
return get_account_types(default_types_root, None, prefix="account")
|
|
||||||
|
|
||||||
|
|
||||||
def get_xml_roots(files_path):
|
|
||||||
xml_roots = frappe._dict()
|
|
||||||
for filepath in files_path:
|
|
||||||
fname = os.path.basename(filepath)
|
|
||||||
if fname.endswith(".xml"):
|
|
||||||
tree = ET.parse(filepath)
|
|
||||||
root = tree.getroot()
|
|
||||||
for node in root[0].findall("record"):
|
|
||||||
if node.get("model") in [
|
|
||||||
"account.account.template",
|
|
||||||
"account.chart.template",
|
|
||||||
"account.account.type",
|
|
||||||
]:
|
|
||||||
xml_roots.setdefault(node.get("model"), []).append(root)
|
|
||||||
break
|
|
||||||
return xml_roots
|
|
||||||
|
|
||||||
|
|
||||||
def get_csv_contents(files_path):
|
|
||||||
csv_content = {}
|
|
||||||
for filepath in files_path:
|
|
||||||
fname = os.path.basename(filepath)
|
|
||||||
for file_type in ["account.account.template", "account.account.type", "account.chart.template"]:
|
|
||||||
if fname.startswith(file_type) and fname.endswith(".csv"):
|
|
||||||
with open(filepath, "r") as csvfile:
|
|
||||||
try:
|
|
||||||
csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read()))
|
|
||||||
except Exception as e:
|
|
||||||
continue
|
|
||||||
return csv_content
|
|
||||||
|
|
||||||
|
|
||||||
def get_account_types(root_list, csv_content, prefix=None):
|
|
||||||
types = {}
|
|
||||||
account_type_map = {
|
|
||||||
"cash": "Cash",
|
|
||||||
"bank": "Bank",
|
|
||||||
"tr_cash": "Cash",
|
|
||||||
"tr_bank": "Bank",
|
|
||||||
"receivable": "Receivable",
|
|
||||||
"tr_receivable": "Receivable",
|
|
||||||
"account rec": "Receivable",
|
|
||||||
"payable": "Payable",
|
|
||||||
"tr_payable": "Payable",
|
|
||||||
"equity": "Equity",
|
|
||||||
"stocks": "Stock",
|
|
||||||
"stock": "Stock",
|
|
||||||
"tax": "Tax",
|
|
||||||
"tr_tax": "Tax",
|
|
||||||
"tax-out": "Tax",
|
|
||||||
"tax-in": "Tax",
|
|
||||||
"charges_personnel": "Chargeable",
|
|
||||||
"fixed asset": "Fixed Asset",
|
|
||||||
"cogs": "Cost of Goods Sold",
|
|
||||||
}
|
|
||||||
for root in root_list:
|
|
||||||
for node in root[0].findall("record"):
|
|
||||||
if node.get("model") == "account.account.type":
|
|
||||||
data = {}
|
|
||||||
for field in node.findall("field"):
|
|
||||||
if (
|
|
||||||
field.get("name") == "code"
|
|
||||||
and field.text.lower() != "none"
|
|
||||||
and account_type_map.get(field.text)
|
|
||||||
):
|
|
||||||
data["account_type"] = account_type_map[field.text]
|
|
||||||
|
|
||||||
node_id = prefix + "." + node.get("id") if prefix else node.get("id")
|
|
||||||
types[node_id] = data
|
|
||||||
|
|
||||||
if csv_content and csv_content[0][0] == "id":
|
|
||||||
for row in csv_content[1:]:
|
|
||||||
row_dict = dict(zip(csv_content[0], row))
|
|
||||||
data = {}
|
|
||||||
if row_dict.get("code") and account_type_map.get(row_dict["code"]):
|
|
||||||
data["account_type"] = account_type_map[row_dict["code"]]
|
|
||||||
if data and data.get("id"):
|
|
||||||
node_id = prefix + "." + data.get("id") if prefix else data.get("id")
|
|
||||||
types[node_id] = data
|
|
||||||
return types
|
|
||||||
|
|
||||||
|
|
||||||
def make_maps_for_xml(xml_roots, account_types, country_dir):
|
|
||||||
"""make maps for `charts` and `accounts`"""
|
|
||||||
for model, root_list in xml_roots.items():
|
|
||||||
for root in root_list:
|
|
||||||
for node in root[0].findall("record"):
|
|
||||||
if node.get("model") == "account.account.template":
|
|
||||||
data = {}
|
|
||||||
for field in node.findall("field"):
|
|
||||||
if field.get("name") == "name":
|
|
||||||
data["name"] = field.text
|
|
||||||
if field.get("name") == "parent_id":
|
|
||||||
parent_id = field.get("ref") or field.get("eval")
|
|
||||||
data["parent_id"] = parent_id
|
|
||||||
|
|
||||||
if field.get("name") == "user_type":
|
|
||||||
value = field.get("ref")
|
|
||||||
if account_types.get(value, {}).get("account_type"):
|
|
||||||
data["account_type"] = account_types[value]["account_type"]
|
|
||||||
if data["account_type"] not in all_account_types:
|
|
||||||
all_account_types.append(data["account_type"])
|
|
||||||
|
|
||||||
data["children"] = []
|
|
||||||
accounts[node.get("id")] = data
|
|
||||||
|
|
||||||
if node.get("model") == "account.chart.template":
|
|
||||||
data = {}
|
|
||||||
for field in node.findall("field"):
|
|
||||||
if field.get("name") == "name":
|
|
||||||
data["name"] = field.text
|
|
||||||
if field.get("name") == "account_root_id":
|
|
||||||
data["account_root_id"] = field.get("ref")
|
|
||||||
data["id"] = country_dir
|
|
||||||
charts.setdefault(node.get("id"), {}).update(data)
|
|
||||||
|
|
||||||
|
|
||||||
def make_maps_for_csv(csv_content, account_types, country_dir):
|
|
||||||
for content in csv_content.get("account.account.template", []):
|
|
||||||
for row in content[1:]:
|
|
||||||
data = dict(zip(content[0], row))
|
|
||||||
account = {
|
|
||||||
"name": data.get("name"),
|
|
||||||
"parent_id": data.get("parent_id:id") or data.get("parent_id/id"),
|
|
||||||
"children": [],
|
|
||||||
}
|
|
||||||
user_type = data.get("user_type/id") or data.get("user_type:id")
|
|
||||||
if account_types.get(user_type, {}).get("account_type"):
|
|
||||||
account["account_type"] = account_types[user_type]["account_type"]
|
|
||||||
if account["account_type"] not in all_account_types:
|
|
||||||
all_account_types.append(account["account_type"])
|
|
||||||
|
|
||||||
accounts[data.get("id")] = account
|
|
||||||
if not account.get("parent_id") and data.get("chart_template_id:id"):
|
|
||||||
chart_id = data.get("chart_template_id:id")
|
|
||||||
charts.setdefault(chart_id, {}).update({"account_root_id": data.get("id")})
|
|
||||||
|
|
||||||
for content in csv_content.get("account.chart.template", []):
|
|
||||||
for row in content[1:]:
|
|
||||||
if row:
|
|
||||||
data = dict(zip(content[0], row))
|
|
||||||
charts.setdefault(data.get("id"), {}).update(
|
|
||||||
{
|
|
||||||
"account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"),
|
|
||||||
"name": data.get("name"),
|
|
||||||
"id": country_dir,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def make_account_trees():
|
|
||||||
"""build tree hierarchy"""
|
|
||||||
for id in accounts.keys():
|
|
||||||
account = accounts[id]
|
|
||||||
|
|
||||||
if account.get("parent_id"):
|
|
||||||
if accounts.get(account["parent_id"]):
|
|
||||||
# accounts[account["parent_id"]]["children"].append(account)
|
|
||||||
accounts[account["parent_id"]][account["name"]] = account
|
|
||||||
del account["parent_id"]
|
|
||||||
del account["name"]
|
|
||||||
|
|
||||||
# remove empty children
|
|
||||||
for id in accounts.keys():
|
|
||||||
if "children" in accounts[id] and not accounts[id].get("children"):
|
|
||||||
del accounts[id]["children"]
|
|
||||||
|
|
||||||
|
|
||||||
def make_charts():
|
|
||||||
"""write chart files in app/setup/doctype/company/charts"""
|
|
||||||
for chart_id in charts:
|
|
||||||
src = charts[chart_id]
|
|
||||||
if not src.get("name") or not src.get("account_root_id"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not src["account_root_id"] in accounts:
|
|
||||||
continue
|
|
||||||
|
|
||||||
filename = src["id"][5:] + "_" + chart_id
|
|
||||||
|
|
||||||
print("building " + filename)
|
|
||||||
chart = {}
|
|
||||||
chart["name"] = src["name"]
|
|
||||||
chart["country_code"] = src["id"][5:]
|
|
||||||
chart["tree"] = accounts[src["account_root_id"]]
|
|
||||||
|
|
||||||
for key, val in chart["tree"].items():
|
|
||||||
if key in ["name", "parent_id"]:
|
|
||||||
chart["tree"].pop(key)
|
|
||||||
if type(val) == dict:
|
|
||||||
val["root_type"] = ""
|
|
||||||
if chart:
|
|
||||||
fpath = os.path.join(
|
|
||||||
"erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json"
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(fpath, "r") as chartfile:
|
|
||||||
old_content = chartfile.read()
|
|
||||||
if not old_content or (
|
|
||||||
json.loads(old_content).get("is_active", "No") == "No"
|
|
||||||
and json.loads(old_content).get("disabled", "No") == "No"
|
|
||||||
):
|
|
||||||
with open(fpath, "w") as chartfile:
|
|
||||||
chartfile.write(json.dumps(chart, indent=4, sort_keys=True))
|
|
||||||
|
|
||||||
all_roots.setdefault(filename, chart["tree"].keys())
|
|
||||||
|
|
||||||
|
|
||||||
def create_all_roots_file():
|
|
||||||
with open("all_roots.txt", "w") as f:
|
|
||||||
for filename, roots in sorted(all_roots.items()):
|
|
||||||
f.write(filename)
|
|
||||||
f.write("\n----------------------\n")
|
|
||||||
for r in sorted(roots):
|
|
||||||
f.write(r.encode("utf-8"))
|
|
||||||
f.write("\n")
|
|
||||||
f.write("\n\n\n")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
go()
|
|
@ -437,12 +437,20 @@
|
|||||||
},
|
},
|
||||||
"Sales": {
|
"Sales": {
|
||||||
"Sales from Other Regions": {
|
"Sales from Other Regions": {
|
||||||
"Sales from Other Region": {}
|
"Sales from Other Region": {
|
||||||
|
"account_type": "Income Account"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Sales of same region": {
|
"Sales of same region": {
|
||||||
"Management Consultancy Fees 1": {},
|
"Management Consultancy Fees 1": {
|
||||||
"Sales Account": {},
|
"account_type": "Income Account"
|
||||||
"Sales of I/C": {}
|
},
|
||||||
|
"Sales Account": {
|
||||||
|
"account_type": "Income Account"
|
||||||
|
},
|
||||||
|
"Sales of I/C": {
|
||||||
|
"account_type": "Income Account"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root_type": "Income"
|
"root_type": "Income"
|
||||||
|
@ -69,8 +69,7 @@
|
|||||||
"Persediaan Barang": {
|
"Persediaan Barang": {
|
||||||
"Persediaan Barang": {
|
"Persediaan Barang": {
|
||||||
"account_number": "1141.000",
|
"account_number": "1141.000",
|
||||||
"account_type": "Stock",
|
"account_type": "Stock"
|
||||||
"is_group": 1
|
|
||||||
},
|
},
|
||||||
"Uang Muka Pembelian": {
|
"Uang Muka Pembelian": {
|
||||||
"Uang Muka Pembelian": {
|
"Uang Muka Pembelian": {
|
||||||
@ -670,7 +669,8 @@
|
|||||||
},
|
},
|
||||||
"Penjualan Barang Dagangan": {
|
"Penjualan Barang Dagangan": {
|
||||||
"Penjualan": {
|
"Penjualan": {
|
||||||
"account_number": "4110.000"
|
"account_number": "4110.000",
|
||||||
|
"account_type": "Income Account"
|
||||||
},
|
},
|
||||||
"Potongan Penjualan": {
|
"Potongan Penjualan": {
|
||||||
"account_number": "4130.000"
|
"account_number": "4130.000"
|
||||||
|
@ -109,8 +109,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"INVENTARIOS": {
|
"INVENTARIOS": {
|
||||||
"account_type": "Stock",
|
"account_type": "Stock"
|
||||||
"is_group": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ACTIVO LARGO PLAZO": {
|
"ACTIVO LARGO PLAZO": {
|
||||||
@ -398,10 +397,18 @@
|
|||||||
"INGRESOS POR SERVICIOS 1": {}
|
"INGRESOS POR SERVICIOS 1": {}
|
||||||
},
|
},
|
||||||
"VENTAS": {
|
"VENTAS": {
|
||||||
"VENTAS EXPORTACION": {},
|
"VENTAS EXPORTACION": {
|
||||||
"VENTAS INMUEBLES": {},
|
"account_type": "Income Account"
|
||||||
"VENTAS NACIONALES": {},
|
},
|
||||||
"VENTAS NACIONALES AL DETAL": {}
|
"VENTAS INMUEBLES": {
|
||||||
|
"account_type": "Income Account"
|
||||||
|
},
|
||||||
|
"VENTAS NACIONALES": {
|
||||||
|
"account_type": "Income Account"
|
||||||
|
},
|
||||||
|
"VENTAS NACIONALES AL DETAL": {
|
||||||
|
"account_type": "Income Account"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,11 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
|
|
||||||
from erpnext.accounts.doctype.account.account import merge_account, update_account_number
|
from erpnext.accounts.doctype.account.account import (
|
||||||
|
InvalidAccountMergeError,
|
||||||
|
merge_account,
|
||||||
|
update_account_number,
|
||||||
|
)
|
||||||
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
|
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
|
||||||
|
|
||||||
test_dependencies = ["Company"]
|
test_dependencies = ["Company"]
|
||||||
@ -47,49 +51,53 @@ class TestAccount(unittest.TestCase):
|
|||||||
frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC")
|
frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC")
|
||||||
|
|
||||||
def test_merge_account(self):
|
def test_merge_account(self):
|
||||||
if not frappe.db.exists("Account", "Current Assets - _TC"):
|
create_account(
|
||||||
acc = frappe.new_doc("Account")
|
account_name="Current Assets",
|
||||||
acc.account_name = "Current Assets"
|
is_group=1,
|
||||||
acc.is_group = 1
|
parent_account="Application of Funds (Assets) - _TC",
|
||||||
acc.parent_account = "Application of Funds (Assets) - _TC"
|
company="_Test Company",
|
||||||
acc.company = "_Test Company"
|
)
|
||||||
acc.insert()
|
|
||||||
if not frappe.db.exists("Account", "Securities and Deposits - _TC"):
|
create_account(
|
||||||
acc = frappe.new_doc("Account")
|
account_name="Securities and Deposits",
|
||||||
acc.account_name = "Securities and Deposits"
|
is_group=1,
|
||||||
acc.parent_account = "Current Assets - _TC"
|
parent_account="Current Assets - _TC",
|
||||||
acc.is_group = 1
|
company="_Test Company",
|
||||||
acc.company = "_Test Company"
|
)
|
||||||
acc.insert()
|
|
||||||
if not frappe.db.exists("Account", "Earnest Money - _TC"):
|
create_account(
|
||||||
acc = frappe.new_doc("Account")
|
account_name="Earnest Money",
|
||||||
acc.account_name = "Earnest Money"
|
parent_account="Securities and Deposits - _TC",
|
||||||
acc.parent_account = "Securities and Deposits - _TC"
|
company="_Test Company",
|
||||||
acc.company = "_Test Company"
|
)
|
||||||
acc.insert()
|
|
||||||
if not frappe.db.exists("Account", "Cash In Hand - _TC"):
|
create_account(
|
||||||
acc = frappe.new_doc("Account")
|
account_name="Cash In Hand",
|
||||||
acc.account_name = "Cash In Hand"
|
is_group=1,
|
||||||
acc.is_group = 1
|
parent_account="Current Assets - _TC",
|
||||||
acc.parent_account = "Current Assets - _TC"
|
company="_Test Company",
|
||||||
acc.company = "_Test Company"
|
)
|
||||||
acc.insert()
|
|
||||||
if not frappe.db.exists("Account", "Accumulated Depreciation - _TC"):
|
create_account(
|
||||||
acc = frappe.new_doc("Account")
|
account_name="Receivable INR",
|
||||||
acc.account_name = "Accumulated Depreciation"
|
parent_account="Current Assets - _TC",
|
||||||
acc.parent_account = "Fixed Assets - _TC"
|
company="_Test Company",
|
||||||
acc.company = "_Test Company"
|
account_currency="INR",
|
||||||
acc.account_type = "Accumulated Depreciation"
|
)
|
||||||
acc.insert()
|
|
||||||
|
create_account(
|
||||||
|
account_name="Receivable USD",
|
||||||
|
parent_account="Current Assets - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
account_currency="USD",
|
||||||
|
)
|
||||||
|
|
||||||
doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
|
|
||||||
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
|
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
|
||||||
|
|
||||||
self.assertEqual(parent, "Securities and Deposits - _TC")
|
self.assertEqual(parent, "Securities and Deposits - _TC")
|
||||||
|
|
||||||
merge_account(
|
merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC")
|
||||||
"Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
|
|
||||||
)
|
|
||||||
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
|
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
|
||||||
|
|
||||||
# Parent account of the child account changes after merging
|
# Parent account of the child account changes after merging
|
||||||
@ -98,30 +106,28 @@ class TestAccount(unittest.TestCase):
|
|||||||
# Old account doesn't exist after merging
|
# Old account doesn't exist after merging
|
||||||
self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC"))
|
self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC"))
|
||||||
|
|
||||||
doc = frappe.get_doc("Account", "Current Assets - _TC")
|
|
||||||
|
|
||||||
# Raise error as is_group property doesn't match
|
# Raise error as is_group property doesn't match
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
frappe.ValidationError,
|
InvalidAccountMergeError,
|
||||||
merge_account,
|
merge_account,
|
||||||
"Current Assets - _TC",
|
"Current Assets - _TC",
|
||||||
"Accumulated Depreciation - _TC",
|
"Accumulated Depreciation - _TC",
|
||||||
doc.is_group,
|
|
||||||
doc.root_type,
|
|
||||||
doc.company,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
doc = frappe.get_doc("Account", "Capital Stock - _TC")
|
|
||||||
|
|
||||||
# Raise error as root_type property doesn't match
|
# Raise error as root_type property doesn't match
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
frappe.ValidationError,
|
InvalidAccountMergeError,
|
||||||
merge_account,
|
merge_account,
|
||||||
"Capital Stock - _TC",
|
"Capital Stock - _TC",
|
||||||
"Softwares - _TC",
|
"Softwares - _TC",
|
||||||
doc.is_group,
|
)
|
||||||
doc.root_type,
|
|
||||||
doc.company,
|
# Raise error as currency doesn't match
|
||||||
|
self.assertRaises(
|
||||||
|
InvalidAccountMergeError,
|
||||||
|
merge_account,
|
||||||
|
"Receivable INR - _TC",
|
||||||
|
"Receivable USD - _TC",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_account_sync(self):
|
def test_account_sync(self):
|
||||||
@ -400,11 +406,20 @@ def create_account(**kwargs):
|
|||||||
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
|
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
|
||||||
)
|
)
|
||||||
if account:
|
if account:
|
||||||
return account
|
account = frappe.get_doc("Account", account)
|
||||||
|
account.update(
|
||||||
|
dict(
|
||||||
|
is_group=kwargs.get("is_group", 0),
|
||||||
|
parent_account=kwargs.get("parent_account"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
account.save()
|
||||||
|
return account.name
|
||||||
else:
|
else:
|
||||||
account = frappe.get_doc(
|
account = frappe.get_doc(
|
||||||
dict(
|
dict(
|
||||||
doctype="Account",
|
doctype="Account",
|
||||||
|
is_group=kwargs.get("is_group", 0),
|
||||||
account_name=kwargs.get("account_name"),
|
account_name=kwargs.get("account_name"),
|
||||||
account_type=kwargs.get("account_type"),
|
account_type=kwargs.get("account_type"),
|
||||||
parent_account=kwargs.get("parent_account"),
|
parent_account=kwargs.get("parent_account"),
|
||||||
|
@ -265,20 +265,21 @@ def get_dimension_with_children(doctype, dimensions):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_dimensions(with_cost_center_and_project=False):
|
def get_dimensions(with_cost_center_and_project=False):
|
||||||
dimension_filters = frappe.db.sql(
|
|
||||||
"""
|
|
||||||
SELECT label, fieldname, document_type
|
|
||||||
FROM `tabAccounting Dimension`
|
|
||||||
WHERE disabled = 0
|
|
||||||
""",
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
default_dimensions = frappe.db.sql(
|
c = frappe.qb.DocType("Accounting Dimension Detail")
|
||||||
"""SELECT p.fieldname, c.company, c.default_dimension
|
p = frappe.qb.DocType("Accounting Dimension")
|
||||||
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
|
dimension_filters = (
|
||||||
WHERE c.parent = p.name""",
|
frappe.qb.from_(p)
|
||||||
as_dict=1,
|
.select(p.label, p.fieldname, p.document_type)
|
||||||
|
.where(p.disabled == 0)
|
||||||
|
.run(as_dict=1)
|
||||||
|
)
|
||||||
|
default_dimensions = (
|
||||||
|
frappe.qb.from_(c)
|
||||||
|
.inner_join(p)
|
||||||
|
.on(c.parent == p.name)
|
||||||
|
.select(p.fieldname, c.company, c.default_dimension)
|
||||||
|
.run(as_dict=1)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(with_cost_center_and_project, str):
|
if isinstance(with_cost_center_and_project, str):
|
||||||
|
@ -84,12 +84,22 @@ def create_dimension():
|
|||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
|
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
|
||||||
frappe.get_doc(
|
dimension = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Accounting Dimension",
|
"doctype": "Accounting Dimension",
|
||||||
"document_type": "Department",
|
"document_type": "Department",
|
||||||
}
|
}
|
||||||
).insert()
|
)
|
||||||
|
dimension.append(
|
||||||
|
"dimension_defaults",
|
||||||
|
{
|
||||||
|
"company": "_Test Company",
|
||||||
|
"reference_document": "Department",
|
||||||
|
"default_dimension": "_Test Department - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
dimension.insert()
|
||||||
|
dimension.save()
|
||||||
else:
|
else:
|
||||||
dimension = frappe.get_doc("Accounting Dimension", "Department")
|
dimension = frappe.get_doc("Accounting Dimension", "Department")
|
||||||
dimension.disabled = 0
|
dimension.disabled = 0
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"account_type",
|
"account_type",
|
||||||
"account_subtype",
|
"account_subtype",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
|
"disabled",
|
||||||
"is_default",
|
"is_default",
|
||||||
"is_company_account",
|
"is_company_account",
|
||||||
"company",
|
"company",
|
||||||
@ -199,10 +200,16 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"label": "Branch Code"
|
"label": "Branch Code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "disabled",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Disabled"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-05-04 15:49:42.620630",
|
"modified": "2023-09-22 21:31:34.763977",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Account",
|
"name": "Bank Account",
|
||||||
|
@ -35,13 +35,14 @@ class TestBankClearance(unittest.TestCase):
|
|||||||
from lending.loan_management.doctype.loan.test_loan import (
|
from lending.loan_management.doctype.loan.test_loan import (
|
||||||
create_loan,
|
create_loan,
|
||||||
create_loan_accounts,
|
create_loan_accounts,
|
||||||
create_loan_type,
|
create_loan_product,
|
||||||
create_repayment_entry,
|
create_repayment_entry,
|
||||||
make_loan_disbursement_entry,
|
make_loan_disbursement_entry,
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_loan_masters():
|
def create_loan_masters():
|
||||||
create_loan_type(
|
create_loan_product(
|
||||||
|
"Clearance Loan",
|
||||||
"Clearance Loan",
|
"Clearance Loan",
|
||||||
2000000,
|
2000000,
|
||||||
13.5,
|
13.5,
|
||||||
|
@ -7,7 +7,9 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
from pypika.terms import Parameter
|
||||||
|
|
||||||
from erpnext import get_default_cost_center
|
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_total_allocated_amount
|
||||||
@ -15,7 +17,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s
|
|||||||
get_amounts_not_reflected_in_system,
|
get_amounts_not_reflected_in_system,
|
||||||
get_entries,
|
get_entries,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import get_balance_on
|
from erpnext.accounts.utils import get_account_currency, get_balance_on
|
||||||
|
|
||||||
|
|
||||||
class BankReconciliationTool(Document):
|
class BankReconciliationTool(Document):
|
||||||
@ -283,68 +285,68 @@ def auto_reconcile_vouchers(
|
|||||||
to_reference_date=None,
|
to_reference_date=None,
|
||||||
):
|
):
|
||||||
frappe.flags.auto_reconcile_vouchers = True
|
frappe.flags.auto_reconcile_vouchers = True
|
||||||
document_types = ["payment_entry", "journal_entry"]
|
reconciled, partially_reconciled = set(), set()
|
||||||
|
|
||||||
bank_transactions = get_bank_transactions(bank_account)
|
bank_transactions = get_bank_transactions(bank_account)
|
||||||
matched_transaction = []
|
|
||||||
for transaction in bank_transactions:
|
for transaction in bank_transactions:
|
||||||
linked_payments = get_linked_payments(
|
linked_payments = get_linked_payments(
|
||||||
transaction.name,
|
transaction.name,
|
||||||
document_types,
|
["payment_entry", "journal_entry"],
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
filter_by_reference_date,
|
filter_by_reference_date,
|
||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
)
|
)
|
||||||
vouchers = []
|
|
||||||
for r in linked_payments:
|
if not linked_payments:
|
||||||
vouchers.append(
|
continue
|
||||||
{
|
|
||||||
"payment_doctype": r[1],
|
vouchers = list(
|
||||||
"payment_name": r[2],
|
map(
|
||||||
"amount": r[4],
|
lambda entry: {
|
||||||
}
|
"payment_doctype": entry.get("doctype"),
|
||||||
)
|
"payment_name": entry.get("name"),
|
||||||
transaction = frappe.get_doc("Bank Transaction", transaction.name)
|
"amount": entry.get("paid_amount"),
|
||||||
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
|
||||||
matched_trans = 0
|
|
||||||
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", "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_doctype"],
|
|
||||||
"payment_entry": voucher["payment_name"],
|
|
||||||
"allocated_amount": allocated_amount,
|
|
||||||
},
|
},
|
||||||
|
linked_payments,
|
||||||
)
|
)
|
||||||
matched_transaction.append(str(transaction.name))
|
)
|
||||||
transaction.save()
|
|
||||||
transaction.update_allocations()
|
updated_transaction = reconcile_vouchers(transaction.name, json.dumps(vouchers))
|
||||||
matched_transaction_len = len(set(matched_transaction))
|
|
||||||
if matched_transaction_len == 0:
|
if updated_transaction.status == "Reconciled":
|
||||||
frappe.msgprint(_("No matching references found for auto reconciliation"))
|
reconciled.add(updated_transaction.name)
|
||||||
elif matched_transaction_len == 1:
|
elif flt(transaction.unallocated_amount) != flt(updated_transaction.unallocated_amount):
|
||||||
frappe.msgprint(_("{0} transaction is reconcilied").format(matched_transaction_len))
|
# Partially reconciled (status = Unreconciled & unallocated amount changed)
|
||||||
else:
|
partially_reconciled.add(updated_transaction.name)
|
||||||
frappe.msgprint(_("{0} transactions are reconcilied").format(matched_transaction_len))
|
|
||||||
|
alert_message, indicator = get_auto_reconcile_message(partially_reconciled, reconciled)
|
||||||
|
frappe.msgprint(title=_("Auto Reconciliation"), msg=alert_message, indicator=indicator)
|
||||||
|
|
||||||
frappe.flags.auto_reconcile_vouchers = False
|
frappe.flags.auto_reconcile_vouchers = False
|
||||||
|
return reconciled, partially_reconciled
|
||||||
|
|
||||||
return frappe.get_doc("Bank Transaction", transaction.name)
|
|
||||||
|
def get_auto_reconcile_message(partially_reconciled, reconciled):
|
||||||
|
"""Returns alert message and indicator for auto reconciliation depending on result state."""
|
||||||
|
alert_message, indicator = "", "blue"
|
||||||
|
if not partially_reconciled and not reconciled:
|
||||||
|
alert_message = _("No matches occurred via auto reconciliation")
|
||||||
|
return alert_message, indicator
|
||||||
|
|
||||||
|
indicator = "green"
|
||||||
|
if reconciled:
|
||||||
|
alert_message += _("{0} Transaction(s) Reconciled").format(len(reconciled))
|
||||||
|
alert_message += "<br>"
|
||||||
|
|
||||||
|
if partially_reconciled:
|
||||||
|
alert_message += _("{0} {1} Partially Reconciled").format(
|
||||||
|
len(partially_reconciled),
|
||||||
|
_("Transactions") if len(partially_reconciled) > 1 else _("Transaction"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return alert_message, indicator
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -390,19 +392,13 @@ def subtract_allocations(gl_account, vouchers):
|
|||||||
"Look up & subtract any existing Bank Transaction allocations"
|
"Look up & subtract any existing Bank Transaction allocations"
|
||||||
copied = []
|
copied = []
|
||||||
for voucher in vouchers:
|
for voucher in vouchers:
|
||||||
rows = get_total_allocated_amount(voucher[1], voucher[2])
|
rows = get_total_allocated_amount(voucher.get("doctype"), voucher.get("name"))
|
||||||
amount = None
|
filtered_row = list(filter(lambda row: row.get("gl_account") == gl_account, rows))
|
||||||
for row in rows:
|
|
||||||
if row["gl_account"] == gl_account:
|
|
||||||
amount = row["total"]
|
|
||||||
break
|
|
||||||
|
|
||||||
if amount:
|
if amount := None if not filtered_row else filtered_row[0]["total"]:
|
||||||
l = list(voucher)
|
voucher["paid_amount"] -= amount
|
||||||
l[3] -= amount
|
|
||||||
copied.append(tuple(l))
|
copied.append(voucher)
|
||||||
else:
|
|
||||||
copied.append(voucher)
|
|
||||||
return copied
|
return copied
|
||||||
|
|
||||||
|
|
||||||
@ -418,6 +414,18 @@ def check_matching(
|
|||||||
to_reference_date,
|
to_reference_date,
|
||||||
):
|
):
|
||||||
exact_match = True if "exact_match" in document_types else False
|
exact_match = True if "exact_match" in document_types else False
|
||||||
|
queries = get_queries(
|
||||||
|
bank_account,
|
||||||
|
company,
|
||||||
|
transaction,
|
||||||
|
document_types,
|
||||||
|
from_date,
|
||||||
|
to_date,
|
||||||
|
filter_by_reference_date,
|
||||||
|
from_reference_date,
|
||||||
|
to_reference_date,
|
||||||
|
exact_match,
|
||||||
|
)
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
"amount": transaction.unallocated_amount,
|
"amount": transaction.unallocated_amount,
|
||||||
@ -429,30 +437,15 @@ def check_matching(
|
|||||||
}
|
}
|
||||||
|
|
||||||
matching_vouchers = []
|
matching_vouchers = []
|
||||||
|
for query in queries:
|
||||||
|
matching_vouchers.extend(frappe.db.sql(query, filters, as_dict=True))
|
||||||
|
|
||||||
# get matching vouchers from all the apps
|
return (
|
||||||
for method_name in frappe.get_hooks("get_matching_vouchers_for_bank_reconciliation"):
|
sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True) if matching_vouchers else []
|
||||||
matching_vouchers.extend(
|
)
|
||||||
frappe.get_attr(method_name)(
|
|
||||||
bank_account,
|
|
||||||
company,
|
|
||||||
transaction,
|
|
||||||
document_types,
|
|
||||||
from_date,
|
|
||||||
to_date,
|
|
||||||
filter_by_reference_date,
|
|
||||||
from_reference_date,
|
|
||||||
to_reference_date,
|
|
||||||
exact_match,
|
|
||||||
filters,
|
|
||||||
)
|
|
||||||
or []
|
|
||||||
)
|
|
||||||
|
|
||||||
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
|
|
||||||
|
|
||||||
|
|
||||||
def get_matching_vouchers_for_bank_reconciliation(
|
def get_queries(
|
||||||
bank_account,
|
bank_account,
|
||||||
company,
|
company,
|
||||||
transaction,
|
transaction,
|
||||||
@ -463,7 +456,6 @@ def get_matching_vouchers_for_bank_reconciliation(
|
|||||||
from_reference_date,
|
from_reference_date,
|
||||||
to_reference_date,
|
to_reference_date,
|
||||||
exact_match,
|
exact_match,
|
||||||
filters,
|
|
||||||
):
|
):
|
||||||
# get queries to get matching vouchers
|
# get queries to get matching vouchers
|
||||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||||
@ -488,17 +480,7 @@ def get_matching_vouchers_for_bank_reconciliation(
|
|||||||
or []
|
or []
|
||||||
)
|
)
|
||||||
|
|
||||||
vouchers = []
|
return queries
|
||||||
|
|
||||||
for query in queries:
|
|
||||||
vouchers.extend(
|
|
||||||
frappe.db.sql(
|
|
||||||
query,
|
|
||||||
filters,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return vouchers
|
|
||||||
|
|
||||||
|
|
||||||
def get_matching_queries(
|
def get_matching_queries(
|
||||||
@ -515,6 +497,8 @@ def get_matching_queries(
|
|||||||
to_reference_date,
|
to_reference_date,
|
||||||
):
|
):
|
||||||
queries = []
|
queries = []
|
||||||
|
currency = get_account_currency(bank_account)
|
||||||
|
|
||||||
if "payment_entry" in document_types:
|
if "payment_entry" in document_types:
|
||||||
query = get_pe_matching_query(
|
query = get_pe_matching_query(
|
||||||
exact_match,
|
exact_match,
|
||||||
@ -541,12 +525,12 @@ def get_matching_queries(
|
|||||||
queries.append(query)
|
queries.append(query)
|
||||||
|
|
||||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||||
query = get_si_matching_query(exact_match)
|
query = get_si_matching_query(exact_match, currency)
|
||||||
queries.append(query)
|
queries.append(query)
|
||||||
|
|
||||||
if transaction.withdrawal > 0.0:
|
if transaction.withdrawal > 0.0:
|
||||||
if "purchase_invoice" in document_types:
|
if "purchase_invoice" in document_types:
|
||||||
query = get_pi_matching_query(exact_match)
|
query = get_pi_matching_query(exact_match, currency)
|
||||||
queries.append(query)
|
queries.append(query)
|
||||||
|
|
||||||
if "bank_transaction" in document_types:
|
if "bank_transaction" in document_types:
|
||||||
@ -560,33 +544,48 @@ def get_bt_matching_query(exact_match, transaction):
|
|||||||
# get matching bank transaction query
|
# get matching bank transaction query
|
||||||
# find bank transactions in the same bank account with opposite sign
|
# find bank transactions in the same bank account with opposite sign
|
||||||
# same bank account must have same company and currency
|
# same bank account must have same company and currency
|
||||||
|
bt = frappe.qb.DocType("Bank Transaction")
|
||||||
|
|
||||||
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
|
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
|
||||||
|
amount_equality = getattr(bt, field) == transaction.unallocated_amount
|
||||||
|
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||||
|
amount_condition = amount_equality if exact_match else getattr(bt, field) > 0.0
|
||||||
|
|
||||||
return f"""
|
ref_rank = (
|
||||||
|
frappe.qb.terms.Case().when(bt.reference_number == transaction.reference_number, 1).else_(0)
|
||||||
|
)
|
||||||
|
unallocated_rank = (
|
||||||
|
frappe.qb.terms.Case().when(bt.unallocated_amount == transaction.unallocated_amount, 1).else_(0)
|
||||||
|
)
|
||||||
|
|
||||||
SELECT
|
party_condition = (
|
||||||
(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
|
(bt.party_type == transaction.party_type)
|
||||||
+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
|
& (bt.party == transaction.party)
|
||||||
+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
|
& bt.party.isnotnull()
|
||||||
+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
|
)
|
||||||
+ 1) AS rank,
|
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||||
'Bank Transaction' AS doctype,
|
|
||||||
name,
|
query = (
|
||||||
unallocated_amount AS paid_amount,
|
frappe.qb.from_(bt)
|
||||||
reference_number AS reference_no,
|
.select(
|
||||||
date AS reference_date,
|
(ref_rank + amount_rank + party_rank + unallocated_rank + 1).as_("rank"),
|
||||||
party,
|
ConstantColumn("Bank Transaction").as_("doctype"),
|
||||||
party_type,
|
bt.name,
|
||||||
date AS posting_date,
|
bt.unallocated_amount.as_("paid_amount"),
|
||||||
currency
|
bt.reference_number.as_("reference_no"),
|
||||||
FROM
|
bt.date.as_("reference_date"),
|
||||||
`tabBank Transaction`
|
bt.party,
|
||||||
WHERE
|
bt.party_type,
|
||||||
status != 'Reconciled'
|
bt.date.as_("posting_date"),
|
||||||
AND name != '{transaction.name}'
|
bt.currency,
|
||||||
AND bank_account = '{transaction.bank_account}'
|
)
|
||||||
AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
|
.where(bt.status != "Reconciled")
|
||||||
"""
|
.where(bt.name != transaction.name)
|
||||||
|
.where(bt.bank_account == transaction.bank_account)
|
||||||
|
.where(amount_condition)
|
||||||
|
.where(bt.docstatus == 1)
|
||||||
|
)
|
||||||
|
return str(query)
|
||||||
|
|
||||||
|
|
||||||
def get_pe_matching_query(
|
def get_pe_matching_query(
|
||||||
@ -600,45 +599,56 @@ def get_pe_matching_query(
|
|||||||
to_reference_date,
|
to_reference_date,
|
||||||
):
|
):
|
||||||
# get matching payment entries query
|
# get matching payment entries query
|
||||||
if transaction.deposit > 0.0:
|
to_from = "to" if transaction.deposit > 0.0 else "from"
|
||||||
currency_field = "paid_to_account_currency as currency"
|
currency_field = f"paid_{to_from}_account_currency"
|
||||||
else:
|
payment_type = "Receive" if transaction.deposit > 0.0 else "Pay"
|
||||||
currency_field = "paid_from_account_currency as currency"
|
pe = frappe.qb.DocType("Payment Entry")
|
||||||
filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'"
|
|
||||||
order_by = " posting_date"
|
ref_condition = pe.reference_no == transaction.reference_number
|
||||||
filter_by_reference_no = ""
|
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||||
|
|
||||||
|
amount_equality = pe.paid_amount == transaction.unallocated_amount
|
||||||
|
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||||
|
amount_condition = amount_equality if exact_match else pe.paid_amount > 0.0
|
||||||
|
|
||||||
|
party_condition = (
|
||||||
|
(pe.party_type == transaction.party_type)
|
||||||
|
& (pe.party == transaction.party)
|
||||||
|
& pe.party.isnotnull()
|
||||||
|
)
|
||||||
|
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||||
|
|
||||||
|
filter_by_date = pe.posting_date.between(from_date, to_date)
|
||||||
if cint(filter_by_reference_date):
|
if cint(filter_by_reference_date):
|
||||||
filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
|
filter_by_date = pe.reference_date.between(from_reference_date, to_reference_date)
|
||||||
order_by = " reference_date"
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(pe)
|
||||||
|
.select(
|
||||||
|
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||||
|
ConstantColumn("Payment Entry").as_("doctype"),
|
||||||
|
pe.name,
|
||||||
|
pe.paid_amount,
|
||||||
|
pe.reference_no,
|
||||||
|
pe.reference_date,
|
||||||
|
pe.party,
|
||||||
|
pe.party_type,
|
||||||
|
pe.posting_date,
|
||||||
|
getattr(pe, currency_field).as_("currency"),
|
||||||
|
)
|
||||||
|
.where(pe.docstatus == 1)
|
||||||
|
.where(pe.payment_type.isin([payment_type, "Internal Transfer"]))
|
||||||
|
.where(pe.clearance_date.isnull())
|
||||||
|
.where(getattr(pe, account_from_to) == Parameter("%(bank_account)s"))
|
||||||
|
.where(amount_condition)
|
||||||
|
.where(filter_by_date)
|
||||||
|
.orderby(pe.reference_date if cint(filter_by_reference_date) else pe.posting_date)
|
||||||
|
)
|
||||||
|
|
||||||
if frappe.flags.auto_reconcile_vouchers == True:
|
if frappe.flags.auto_reconcile_vouchers == True:
|
||||||
filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
|
query = query.where(ref_condition)
|
||||||
return f"""
|
|
||||||
SELECT
|
return str(query)
|
||||||
(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
|
|
||||||
+ 1 ) AS rank,
|
|
||||||
'Payment Entry' as doctype,
|
|
||||||
name,
|
|
||||||
paid_amount,
|
|
||||||
reference_no,
|
|
||||||
reference_date,
|
|
||||||
party,
|
|
||||||
party_type,
|
|
||||||
posting_date,
|
|
||||||
{currency_field}
|
|
||||||
FROM
|
|
||||||
`tabPayment Entry`
|
|
||||||
WHERE
|
|
||||||
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(
|
def get_je_matching_query(
|
||||||
@ -655,100 +665,121 @@ def get_je_matching_query(
|
|||||||
# So one bank could have both types of bank accounts like asset and liability
|
# 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
|
# 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.0 else "debit"
|
||||||
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
|
je = frappe.qb.DocType("Journal Entry")
|
||||||
order_by = " je.posting_date"
|
jea = frappe.qb.DocType("Journal Entry Account")
|
||||||
filter_by_reference_no = ""
|
|
||||||
|
ref_condition = je.cheque_no == transaction.reference_number
|
||||||
|
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||||
|
|
||||||
|
amount_field = f"{cr_or_dr}_in_account_currency"
|
||||||
|
amount_equality = getattr(jea, amount_field) == transaction.unallocated_amount
|
||||||
|
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||||
|
|
||||||
|
filter_by_date = je.posting_date.between(from_date, to_date)
|
||||||
if cint(filter_by_reference_date):
|
if cint(filter_by_reference_date):
|
||||||
filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
|
filter_by_date = je.cheque_date.between(from_reference_date, to_reference_date)
|
||||||
order_by = " je.cheque_date"
|
|
||||||
if frappe.flags.auto_reconcile_vouchers == True:
|
query = (
|
||||||
filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
|
frappe.qb.from_(jea)
|
||||||
return f"""
|
.join(je)
|
||||||
SELECT
|
.on(jea.parent == je.name)
|
||||||
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
|
.select(
|
||||||
+ CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
|
(ref_rank + amount_rank + 1).as_("rank"),
|
||||||
+ 1) AS rank ,
|
ConstantColumn("Journal Entry").as_("doctype"),
|
||||||
'Journal Entry' AS doctype,
|
|
||||||
je.name,
|
je.name,
|
||||||
jea.{cr_or_dr}_in_account_currency AS paid_amount,
|
getattr(jea, amount_field).as_("paid_amount"),
|
||||||
je.cheque_no AS reference_no,
|
je.cheque_no.as_("reference_no"),
|
||||||
je.cheque_date AS reference_date,
|
je.cheque_date.as_("reference_date"),
|
||||||
je.pay_to_recd_from AS party,
|
je.pay_to_recd_from.as_("party"),
|
||||||
jea.party_type,
|
jea.party_type,
|
||||||
je.posting_date,
|
je.posting_date,
|
||||||
jea.account_currency AS currency
|
jea.account_currency.as_("currency"),
|
||||||
FROM
|
)
|
||||||
`tabJournal Entry Account` AS jea
|
.where(je.docstatus == 1)
|
||||||
JOIN
|
.where(je.voucher_type != "Opening Entry")
|
||||||
`tabJournal Entry` AS je
|
.where(je.clearance_date.isnull())
|
||||||
ON
|
.where(jea.account == Parameter("%(bank_account)s"))
|
||||||
jea.parent = je.name
|
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
||||||
WHERE
|
.where(je.docstatus == 1)
|
||||||
je.docstatus = 1
|
.where(filter_by_date)
|
||||||
AND je.voucher_type NOT IN ('Opening Entry')
|
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||||
AND (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'}
|
if frappe.flags.auto_reconcile_vouchers == True:
|
||||||
AND je.docstatus = 1
|
query = query.where(ref_condition)
|
||||||
{filter_by_date}
|
|
||||||
{filter_by_reference_no}
|
return str(query)
|
||||||
order by {order_by}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def get_si_matching_query(exact_match):
|
def get_si_matching_query(exact_match, currency):
|
||||||
# get matching sales invoice query
|
# get matching sales invoice query
|
||||||
return f"""
|
si = frappe.qb.DocType("Sales Invoice")
|
||||||
SELECT
|
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||||
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
|
|
||||||
+ CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
|
amount_equality = sip.amount == Parameter("%(amount)s")
|
||||||
+ 1 ) AS rank,
|
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||||
'Sales Invoice' as doctype,
|
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||||
|
|
||||||
|
party_condition = si.customer == Parameter("%(party)s")
|
||||||
|
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sip)
|
||||||
|
.join(si)
|
||||||
|
.on(sip.parent == si.name)
|
||||||
|
.select(
|
||||||
|
(party_rank + amount_rank + 1).as_("rank"),
|
||||||
|
ConstantColumn("Sales Invoice").as_("doctype"),
|
||||||
si.name,
|
si.name,
|
||||||
sip.amount as paid_amount,
|
sip.amount.as_("paid_amount"),
|
||||||
'' as reference_no,
|
ConstantColumn("").as_("reference_no"),
|
||||||
'' as reference_date,
|
ConstantColumn("").as_("reference_date"),
|
||||||
si.customer as party,
|
si.customer.as_("party"),
|
||||||
'Customer' as party_type,
|
ConstantColumn("Customer").as_("party_type"),
|
||||||
si.posting_date,
|
si.posting_date,
|
||||||
si.currency
|
si.currency,
|
||||||
|
)
|
||||||
|
.where(si.docstatus == 1)
|
||||||
|
.where(sip.clearance_date.isnull())
|
||||||
|
.where(sip.account == Parameter("%(bank_account)s"))
|
||||||
|
.where(amount_condition)
|
||||||
|
.where(si.currency == currency)
|
||||||
|
)
|
||||||
|
|
||||||
FROM
|
return str(query)
|
||||||
`tabSales Invoice Payment` as sip
|
|
||||||
JOIN
|
|
||||||
`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')
|
|
||||||
AND sip.account = %(bank_account)s
|
|
||||||
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def get_pi_matching_query(exact_match):
|
def get_pi_matching_query(exact_match, currency):
|
||||||
# get matching purchase invoice query when they are also used as payment entries (is_paid)
|
# get matching purchase invoice query when they are also used as payment entries (is_paid)
|
||||||
return f"""
|
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||||
SELECT
|
|
||||||
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
|
amount_equality = purchase_invoice.paid_amount == Parameter("%(amount)s")
|
||||||
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
|
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||||
+ 1 ) AS rank,
|
amount_condition = amount_equality if exact_match else purchase_invoice.paid_amount > 0.0
|
||||||
'Purchase Invoice' as doctype,
|
|
||||||
name,
|
party_condition = purchase_invoice.supplier == Parameter("%(party)s")
|
||||||
paid_amount,
|
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||||
'' as reference_no,
|
|
||||||
'' as reference_date,
|
query = (
|
||||||
supplier as party,
|
frappe.qb.from_(purchase_invoice)
|
||||||
'Supplier' as party_type,
|
.select(
|
||||||
posting_date,
|
(party_rank + amount_rank + 1).as_("rank"),
|
||||||
currency
|
ConstantColumn("Purchase Invoice").as_("doctype"),
|
||||||
FROM
|
purchase_invoice.name,
|
||||||
`tabPurchase Invoice`
|
purchase_invoice.paid_amount,
|
||||||
WHERE
|
ConstantColumn("").as_("reference_no"),
|
||||||
docstatus = 1
|
ConstantColumn("").as_("reference_date"),
|
||||||
AND is_paid = 1
|
purchase_invoice.supplier.as_("party"),
|
||||||
AND ifnull(clearance_date, '') = ""
|
ConstantColumn("Supplier").as_("party_type"),
|
||||||
AND cash_bank_account = %(bank_account)s
|
purchase_invoice.posting_date,
|
||||||
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
|
purchase_invoice.currency,
|
||||||
"""
|
)
|
||||||
|
.where(purchase_invoice.docstatus == 1)
|
||||||
|
.where(purchase_invoice.is_paid == 1)
|
||||||
|
.where(purchase_invoice.clearance_date.isnull())
|
||||||
|
.where(purchase_invoice.cash_bank_account == Parameter("%(bank_account)s"))
|
||||||
|
.where(amount_condition)
|
||||||
|
.where(purchase_invoice.currency == currency)
|
||||||
|
)
|
||||||
|
|
||||||
|
return str(query)
|
||||||
|
@ -1,9 +1,100 @@
|
|||||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import qb
|
||||||
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
from frappe.utils import add_days, flt, getdate, today
|
||||||
|
|
||||||
class TestBankReconciliationTool(unittest.TestCase):
|
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||||
pass
|
auto_reconcile_vouchers,
|
||||||
|
get_bank_transactions,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.create_company()
|
||||||
|
self.create_customer()
|
||||||
|
self.clear_old_entries()
|
||||||
|
bank_dt = qb.DocType("Bank")
|
||||||
|
q = qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||||
|
self.create_bank_account()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def create_bank_account(self):
|
||||||
|
bank = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Bank",
|
||||||
|
"bank_name": "HDFC",
|
||||||
|
}
|
||||||
|
).save()
|
||||||
|
|
||||||
|
self.bank_account = (
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Bank Account",
|
||||||
|
"account_name": "HDFC _current_",
|
||||||
|
"bank": bank,
|
||||||
|
"is_company_account": True,
|
||||||
|
"account": self.bank, # account from Chart of Accounts
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.insert()
|
||||||
|
.name
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_auto_reconcile(self):
|
||||||
|
# make payment
|
||||||
|
from_date = add_days(today(), -1)
|
||||||
|
to_date = today()
|
||||||
|
payment = create_payment_entry(
|
||||||
|
company=self.company,
|
||||||
|
posting_date=from_date,
|
||||||
|
payment_type="Receive",
|
||||||
|
party_type="Customer",
|
||||||
|
party=self.customer,
|
||||||
|
paid_from=self.debit_to,
|
||||||
|
paid_to=self.bank,
|
||||||
|
paid_amount=100,
|
||||||
|
).save()
|
||||||
|
payment.reference_no = "123"
|
||||||
|
payment = payment.save().submit()
|
||||||
|
|
||||||
|
# make bank transaction
|
||||||
|
bank_transaction = (
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Bank Transaction",
|
||||||
|
"date": to_date,
|
||||||
|
"deposit": 100,
|
||||||
|
"bank_account": self.bank_account,
|
||||||
|
"reference_number": "123",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.save()
|
||||||
|
.submit()
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert API output pre reconciliation
|
||||||
|
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
|
||||||
|
self.assertEqual(len(transactions), 1)
|
||||||
|
self.assertEqual(transactions[0].name, bank_transaction.name)
|
||||||
|
|
||||||
|
# auto reconcile
|
||||||
|
auto_reconcile_vouchers(
|
||||||
|
bank_account=self.bank_account,
|
||||||
|
from_date=from_date,
|
||||||
|
to_date=to_date,
|
||||||
|
filter_by_reference_date=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert API output post reconciliation
|
||||||
|
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
|
||||||
|
self.assertEqual(len(transactions), 0)
|
||||||
|
@ -352,10 +352,11 @@ frappe.ui.form.on("Bank Statement Import", {
|
|||||||
|
|
||||||
export_errored_rows(frm) {
|
export_errored_rows(frm) {
|
||||||
open_url_post(
|
open_url_post(
|
||||||
"/api/method/frappe.core.doctype.data_import.data_import.download_errored_template",
|
"/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_errored_template",
|
||||||
{
|
{
|
||||||
data_import_name: frm.doc.name,
|
data_import_name: frm.doc.name,
|
||||||
}
|
},
|
||||||
|
true
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -13,10 +13,11 @@ frappe.ui.form.on("Bank Transaction", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
frm.add_custom_button(__('Unreconcile Transaction'), () => {
|
if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) {
|
||||||
frm.call('remove_payment_entries')
|
frm.add_custom_button(__("Unreconcile Transaction"), () => {
|
||||||
.then( () => frm.refresh() );
|
frm.call("remove_payment_entries").then(() => frm.refresh());
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
bank_account: function (frm) {
|
bank_account: function (frm) {
|
||||||
set_bank_statement_filter(frm);
|
set_bank_statement_filter(frm);
|
||||||
|
@ -47,7 +47,7 @@ class TestBankTransaction(FrappeTestCase):
|
|||||||
from_date=bank_transaction.date,
|
from_date=bank_transaction.date,
|
||||||
to_date=utils.today(),
|
to_date=utils.today(),
|
||||||
)
|
)
|
||||||
self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
|
self.assertTrue(linked_payments[0]["party"] == "Conrad Electronic")
|
||||||
|
|
||||||
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
|
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
|
||||||
def test_reconcile(self):
|
def test_reconcile(self):
|
||||||
@ -93,7 +93,7 @@ class TestBankTransaction(FrappeTestCase):
|
|||||||
from_date=bank_transaction.date,
|
from_date=bank_transaction.date,
|
||||||
to_date=utils.today(),
|
to_date=utils.today(),
|
||||||
)
|
)
|
||||||
self.assertTrue(linked_payments[0][3])
|
self.assertTrue(linked_payments[0]["paid_amount"])
|
||||||
|
|
||||||
# Check error if already reconciled
|
# Check error if already reconciled
|
||||||
def test_already_reconciled(self):
|
def test_already_reconciled(self):
|
||||||
@ -188,7 +188,7 @@ class TestBankTransaction(FrappeTestCase):
|
|||||||
repayment_entry = create_loan_and_repayment()
|
repayment_entry = create_loan_and_repayment()
|
||||||
|
|
||||||
linked_payments = get_linked_payments(bank_transaction.name, ["loan_repayment", "exact_match"])
|
linked_payments = get_linked_payments(bank_transaction.name, ["loan_repayment", "exact_match"])
|
||||||
self.assertEqual(linked_payments[0][2], repayment_entry.name)
|
self.assertEqual(linked_payments[0]["name"], repayment_entry.name)
|
||||||
|
|
||||||
|
|
||||||
@if_lending_app_installed
|
@if_lending_app_installed
|
||||||
@ -410,7 +410,7 @@ def add_vouchers():
|
|||||||
def create_loan_and_repayment():
|
def create_loan_and_repayment():
|
||||||
from lending.loan_management.doctype.loan.test_loan import (
|
from lending.loan_management.doctype.loan.test_loan import (
|
||||||
create_loan,
|
create_loan,
|
||||||
create_loan_type,
|
create_loan_product,
|
||||||
create_repayment_entry,
|
create_repayment_entry,
|
||||||
make_loan_disbursement_entry,
|
make_loan_disbursement_entry,
|
||||||
)
|
)
|
||||||
@ -420,7 +420,8 @@ def create_loan_and_repayment():
|
|||||||
|
|
||||||
from erpnext.setup.doctype.employee.test_employee import make_employee
|
from erpnext.setup.doctype.employee.test_employee import make_employee
|
||||||
|
|
||||||
create_loan_type(
|
create_loan_product(
|
||||||
|
"Personal Loan",
|
||||||
"Personal Loan",
|
"Personal Loan",
|
||||||
500000,
|
500000,
|
||||||
8.4,
|
8.4,
|
||||||
@ -441,7 +442,7 @@ def create_loan_and_repayment():
|
|||||||
"applicant_type": "Employee",
|
"applicant_type": "Employee",
|
||||||
"company": "_Test Company",
|
"company": "_Test Company",
|
||||||
"applicant": applicant,
|
"applicant": applicant,
|
||||||
"loan_type": "Personal Loan",
|
"loan_product": "Personal Loan",
|
||||||
"loan_amount": 5000,
|
"loan_amount": 5000,
|
||||||
"repayment_method": "Repay Fixed Amount per Period",
|
"repayment_method": "Repay Fixed Amount per Period",
|
||||||
"monthly_repayment_amount": 500,
|
"monthly_repayment_amount": 500,
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"disabled",
|
"disabled",
|
||||||
"service_provider",
|
"service_provider",
|
||||||
"api_endpoint",
|
"api_endpoint",
|
||||||
|
"access_key",
|
||||||
"url",
|
"url",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"help",
|
"help",
|
||||||
@ -84,12 +85,18 @@
|
|||||||
"fieldname": "disabled",
|
"fieldname": "disabled",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Disabled"
|
"label": "Disabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.service_provider == 'exchangerate.host';",
|
||||||
|
"fieldname": "access_key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Access Key"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-01-09 12:19:03.955906",
|
"modified": "2023-10-04 15:30:25.333860",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Currency Exchange Settings",
|
"name": "Currency Exchange Settings",
|
||||||
|
@ -18,11 +18,21 @@ class CurrencyExchangeSettings(Document):
|
|||||||
|
|
||||||
def set_parameters_and_result(self):
|
def set_parameters_and_result(self):
|
||||||
if self.service_provider == "exchangerate.host":
|
if self.service_provider == "exchangerate.host":
|
||||||
|
|
||||||
|
if not self.access_key:
|
||||||
|
frappe.throw(
|
||||||
|
_("Access Key is required for Service Provider: {0}").format(
|
||||||
|
frappe.bold(self.service_provider)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.set("result_key", [])
|
self.set("result_key", [])
|
||||||
self.set("req_params", [])
|
self.set("req_params", [])
|
||||||
|
|
||||||
self.api_endpoint = "https://api.exchangerate.host/convert"
|
self.api_endpoint = "https://api.exchangerate.host/convert"
|
||||||
self.append("result_key", {"key": "result"})
|
self.append("result_key", {"key": "result"})
|
||||||
|
self.append("req_params", {"key": "access_key", "value": self.access_key})
|
||||||
|
self.append("req_params", {"key": "amount", "value": "1"})
|
||||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||||
|
@ -3,6 +3,296 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import qb
|
||||||
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
from frappe.utils import add_days, flt, today
|
||||||
|
|
||||||
class TestExchangeRateRevaluation(unittest.TestCase):
|
from erpnext import get_default_cost_center
|
||||||
pass
|
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.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
|
|
||||||
|
class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.create_company()
|
||||||
|
self.create_usd_receivable_account()
|
||||||
|
self.create_item()
|
||||||
|
self.create_customer()
|
||||||
|
self.clear_old_entries()
|
||||||
|
self.set_system_and_company_settings()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def set_system_and_company_settings(self):
|
||||||
|
# set number and currency precision
|
||||||
|
system_settings = frappe.get_doc("System Settings")
|
||||||
|
system_settings.float_precision = 2
|
||||||
|
system_settings.currency_precision = 2
|
||||||
|
system_settings.save()
|
||||||
|
|
||||||
|
# Using Exchange Gain/Loss account for unrealized as well.
|
||||||
|
company_doc = frappe.get_doc("Company", self.company)
|
||||||
|
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
|
||||||
|
company_doc.save()
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||||
|
)
|
||||||
|
def test_01_revaluation_of_forex_balance(self):
|
||||||
|
"""
|
||||||
|
Test Forex account balance and Journal creation post Revaluation
|
||||||
|
"""
|
||||||
|
si = create_sales_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
debit_to=self.debtors_usd,
|
||||||
|
posting_date=today(),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
rate=100,
|
||||||
|
price_list_rate=100,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
si.currency = "USD"
|
||||||
|
si.conversion_rate = 80
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||||
|
err.company = self.company
|
||||||
|
err.posting_date = today()
|
||||||
|
accounts = err.get_accounts_data()
|
||||||
|
err.extend("accounts", accounts)
|
||||||
|
row = err.accounts[0]
|
||||||
|
row.new_exchange_rate = 85
|
||||||
|
row.new_balance_in_base_currency = flt(
|
||||||
|
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||||
|
)
|
||||||
|
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
|
||||||
|
err.set_total_gain_loss()
|
||||||
|
err = err.save().submit()
|
||||||
|
|
||||||
|
# Create JV for ERR
|
||||||
|
err_journals = err.make_jv_entries()
|
||||||
|
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
|
||||||
|
je = je.submit()
|
||||||
|
|
||||||
|
je.reload()
|
||||||
|
self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
|
||||||
|
self.assertEqual(je.total_debit, 8500.0)
|
||||||
|
self.assertEqual(je.total_credit, 8500.0)
|
||||||
|
|
||||||
|
acc_balance = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||||
|
fields=["sum(debit)-sum(credit) as balance"],
|
||||||
|
)[0]
|
||||||
|
self.assertEqual(acc_balance.balance, 8500.0)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||||
|
)
|
||||||
|
def test_02_accounts_only_with_base_currency_balance(self):
|
||||||
|
"""
|
||||||
|
Test Revaluation on Forex account with balance only in base currency
|
||||||
|
"""
|
||||||
|
si = create_sales_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
debit_to=self.debtors_usd,
|
||||||
|
posting_date=today(),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
rate=100,
|
||||||
|
price_list_rate=100,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
si.currency = "USD"
|
||||||
|
si.conversion_rate = 80
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
pe = get_payment_entry(si.doctype, si.name)
|
||||||
|
pe.source_exchange_rate = 85
|
||||||
|
pe.received_amount = 8500
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
# Cancel the auto created gain/loss JE to simulate balance only in base currency
|
||||||
|
je = frappe.db.get_all(
|
||||||
|
"Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
|
||||||
|
)[0]
|
||||||
|
frappe.get_doc("Journal Entry", je).cancel()
|
||||||
|
|
||||||
|
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||||
|
err.company = self.company
|
||||||
|
err.posting_date = today()
|
||||||
|
err.fetch_and_calculate_accounts_data()
|
||||||
|
err = err.save().submit()
|
||||||
|
|
||||||
|
# Create JV for ERR
|
||||||
|
self.assertTrue(err.check_journal_entry_condition())
|
||||||
|
err_journals = err.make_jv_entries()
|
||||||
|
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
|
||||||
|
je = je.submit()
|
||||||
|
|
||||||
|
je.reload()
|
||||||
|
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
|
||||||
|
self.assertEqual(len(je.accounts), 2)
|
||||||
|
# Only base currency fields will be posted to
|
||||||
|
for acc in je.accounts:
|
||||||
|
self.assertEqual(acc.debit_in_account_currency, 0)
|
||||||
|
self.assertEqual(acc.credit_in_account_currency, 0)
|
||||||
|
|
||||||
|
self.assertEqual(je.total_debit, 500.0)
|
||||||
|
self.assertEqual(je.total_credit, 500.0)
|
||||||
|
|
||||||
|
acc_balance = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||||
|
fields=[
|
||||||
|
"sum(debit)-sum(credit) as balance",
|
||||||
|
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||||
|
],
|
||||||
|
)[0]
|
||||||
|
# account shouldn't have balance in base and account currency
|
||||||
|
self.assertEqual(acc_balance.balance, 0.0)
|
||||||
|
self.assertEqual(acc_balance.balance_in_account_currency, 0.0)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||||
|
)
|
||||||
|
def test_03_accounts_only_with_account_currency_balance(self):
|
||||||
|
"""
|
||||||
|
Test Revaluation on Forex account with balance only in account currency
|
||||||
|
"""
|
||||||
|
precision = frappe.db.get_single_value("System Settings", "currency_precision")
|
||||||
|
|
||||||
|
# posting on previous date to make sure that ERR picks up the Payment entry's exchange
|
||||||
|
# rate while calculating gain/loss for account currency balance
|
||||||
|
si = create_sales_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
debit_to=self.debtors_usd,
|
||||||
|
posting_date=add_days(today(), -1),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
rate=100,
|
||||||
|
price_list_rate=100,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
si.currency = "USD"
|
||||||
|
si.conversion_rate = 80
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
pe = get_payment_entry(si.doctype, si.name)
|
||||||
|
pe.paid_amount = 95
|
||||||
|
pe.source_exchange_rate = 84.211
|
||||||
|
pe.received_amount = 8000
|
||||||
|
pe.references = []
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
acc_balance = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||||
|
fields=[
|
||||||
|
"sum(debit)-sum(credit) as balance",
|
||||||
|
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||||
|
],
|
||||||
|
)[0]
|
||||||
|
# account should have balance only in account currency
|
||||||
|
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
|
||||||
|
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD
|
||||||
|
|
||||||
|
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||||
|
err.company = self.company
|
||||||
|
err.posting_date = today()
|
||||||
|
err.fetch_and_calculate_accounts_data()
|
||||||
|
err.set_total_gain_loss()
|
||||||
|
err = err.save().submit()
|
||||||
|
|
||||||
|
# Create JV for ERR
|
||||||
|
self.assertTrue(err.check_journal_entry_condition())
|
||||||
|
err_journals = err.make_jv_entries()
|
||||||
|
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
|
||||||
|
je = je.submit()
|
||||||
|
|
||||||
|
je.reload()
|
||||||
|
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
|
||||||
|
self.assertEqual(len(je.accounts), 2)
|
||||||
|
# Only account currency fields will be posted to
|
||||||
|
for acc in je.accounts:
|
||||||
|
self.assertEqual(flt(acc.debit, precision), 0.0)
|
||||||
|
self.assertEqual(flt(acc.credit, precision), 0.0)
|
||||||
|
|
||||||
|
row = [x for x in je.accounts if x.account == self.debtors_usd][0]
|
||||||
|
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||||
|
row = [x for x in je.accounts if x.account != self.debtors_usd][0]
|
||||||
|
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
|
||||||
|
|
||||||
|
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||||
|
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||||
|
self.assertEqual(flt(je.total_credit, precision), 0.0)
|
||||||
|
|
||||||
|
acc_balance = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"account": self.debtors_usd, "is_cancelled": 0},
|
||||||
|
fields=[
|
||||||
|
"sum(debit)-sum(credit) as balance",
|
||||||
|
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
|
||||||
|
],
|
||||||
|
)[0]
|
||||||
|
# account shouldn't have balance in base and account currency post revaluation
|
||||||
|
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
|
||||||
|
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||||
|
)
|
||||||
|
def test_04_get_account_details_function(self):
|
||||||
|
si = create_sales_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
debit_to=self.debtors_usd,
|
||||||
|
posting_date=today(),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
rate=100,
|
||||||
|
price_list_rate=100,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
si.currency = "USD"
|
||||||
|
si.conversion_rate = 80
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import (
|
||||||
|
get_account_details,
|
||||||
|
)
|
||||||
|
|
||||||
|
account_details = get_account_details(
|
||||||
|
self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05
|
||||||
|
)
|
||||||
|
# not checking for new exchange rate and balances as it is dependent on live exchange rates
|
||||||
|
expected_data = {
|
||||||
|
"account_currency": "USD",
|
||||||
|
"balance_in_base_currency": 8000.0,
|
||||||
|
"balance_in_account_currency": 100.0,
|
||||||
|
"current_exchange_rate": 80.0,
|
||||||
|
"zero_balance": False,
|
||||||
|
"new_balance_in_account_currency": 100.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val in expected_data.items():
|
||||||
|
self.assertEqual(expected_data.get(key), account_details.get(key))
|
||||||
|
@ -50,8 +50,18 @@ frappe.ui.form.on("Journal Entry", {
|
|||||||
frm.trigger("make_inter_company_journal_entry");
|
frm.trigger("make_inter_company_journal_entry");
|
||||||
}, __('Make'));
|
}, __('Make'));
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
|
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||||
|
},
|
||||||
|
before_save: function(frm) {
|
||||||
|
if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
|
||||||
|
let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry"));
|
||||||
|
if (payment_entry_references.length > 0) {
|
||||||
|
let rows = payment_entry_references.map(x => "#"+x.idx);
|
||||||
|
frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
make_inter_company_journal_entry: function(frm) {
|
make_inter_company_journal_entry: function(frm) {
|
||||||
var d = new frappe.ui.Dialog({
|
var d = new frappe.ui.Dialog({
|
||||||
title: __("Select Company"),
|
title: __("Select Company"),
|
||||||
|
@ -48,9 +48,6 @@ def start_merge(docname):
|
|||||||
merge_account(
|
merge_account(
|
||||||
row.account,
|
row.account,
|
||||||
ledger_merge.account,
|
ledger_merge.account,
|
||||||
ledger_merge.is_group,
|
|
||||||
ledger_merge.root_type,
|
|
||||||
ledger_merge.company,
|
|
||||||
)
|
)
|
||||||
row.db_set("merged", 1)
|
row.db_set("merged", 1)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
@ -218,6 +218,7 @@ def make_customer(customer=None):
|
|||||||
"territory": "All Territories",
|
"territory": "All Territories",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
customer.insert(ignore_permissions=True)
|
customer.insert(ignore_permissions=True)
|
||||||
return customer.name
|
return customer.name
|
||||||
|
@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
|
|||||||
|
|
||||||
frappe.ui.form.on('Payment Entry', {
|
frappe.ui.form.on('Payment Entry', {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
|
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
|
||||||
|
|
||||||
if(frm.doc.__islocal) {
|
if(frm.doc.__islocal) {
|
||||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||||
@ -154,6 +154,13 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
frm.events.set_dynamic_labels(frm);
|
frm.events.set_dynamic_labels(frm);
|
||||||
frm.events.show_general_ledger(frm);
|
frm.events.show_general_ledger(frm);
|
||||||
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
|
erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm);
|
||||||
|
if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
|
||||||
|
frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
|
||||||
|
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
|
||||||
|
}, __('Actions'));
|
||||||
|
|
||||||
|
}
|
||||||
|
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
validate_company: (frm) => {
|
validate_company: (frm) => {
|
||||||
@ -535,15 +542,21 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
source_exchange_rate: function(frm) {
|
source_exchange_rate: function(frm) {
|
||||||
|
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||||
if (frm.doc.paid_amount) {
|
if (frm.doc.paid_amount) {
|
||||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||||
// target exchange rate should always be same as source if both account currencies is same
|
// target exchange rate should always be same as source if both account currencies is same
|
||||||
if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||||
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate);
|
||||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||||
|
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||||
|
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||||
|
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.events.set_unallocated_amount(frm);
|
// set_unallocated_amount is called by below method,
|
||||||
|
// no need trigger separately
|
||||||
|
frm.events.set_total_allocated_amount(frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make read only if Accounts Settings doesn't allow stale rates
|
// Make read only if Accounts Settings doesn't allow stale rates
|
||||||
@ -552,6 +565,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
|
|
||||||
target_exchange_rate: function(frm) {
|
target_exchange_rate: function(frm) {
|
||||||
frm.set_paid_amount_based_on_received_amount = true;
|
frm.set_paid_amount_based_on_received_amount = true;
|
||||||
|
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||||
|
|
||||||
if (frm.doc.received_amount) {
|
if (frm.doc.received_amount) {
|
||||||
frm.set_value("base_received_amount",
|
frm.set_value("base_received_amount",
|
||||||
@ -561,9 +575,14 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) {
|
(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) {
|
||||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||||
|
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||||
|
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||||
|
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.events.set_unallocated_amount(frm);
|
// set_unallocated_amount is called by below method,
|
||||||
|
// no need trigger separately
|
||||||
|
frm.events.set_total_allocated_amount(frm);
|
||||||
}
|
}
|
||||||
frm.set_paid_amount_based_on_received_amount = false;
|
frm.set_paid_amount_based_on_received_amount = false;
|
||||||
|
|
||||||
@ -879,12 +898,18 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
set_total_allocated_amount: function(frm) {
|
set_total_allocated_amount: function(frm) {
|
||||||
|
let exchange_rate = 1;
|
||||||
|
if (frm.doc.payment_type == "Receive") {
|
||||||
|
exchange_rate = frm.doc.source_exchange_rate;
|
||||||
|
} else if (frm.doc.payment_type == "Pay") {
|
||||||
|
exchange_rate = frm.doc.target_exchange_rate;
|
||||||
|
}
|
||||||
var total_allocated_amount = 0.0;
|
var total_allocated_amount = 0.0;
|
||||||
var base_total_allocated_amount = 0.0;
|
var base_total_allocated_amount = 0.0;
|
||||||
$.each(frm.doc.references || [], function(i, row) {
|
$.each(frm.doc.references || [], function(i, row) {
|
||||||
if (row.allocated_amount) {
|
if (row.allocated_amount) {
|
||||||
total_allocated_amount += flt(row.allocated_amount);
|
total_allocated_amount += flt(row.allocated_amount);
|
||||||
base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(row.exchange_rate),
|
base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(exchange_rate),
|
||||||
precision("base_paid_amount"));
|
precision("base_paid_amount"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -98,7 +98,6 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.difference_amount:
|
if self.difference_amount:
|
||||||
frappe.throw(_("Difference Amount must be zero"))
|
frappe.throw(_("Difference Amount must be zero"))
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.make_advance_gl_entries()
|
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.update_advance_paid()
|
self.update_advance_paid()
|
||||||
self.update_payment_schedule()
|
self.update_payment_schedule()
|
||||||
@ -149,10 +148,11 @@ class PaymentEntry(AccountsController):
|
|||||||
"Repost Payment Ledger Items",
|
"Repost Payment Ledger Items",
|
||||||
"Repost Accounting Ledger",
|
"Repost Accounting Ledger",
|
||||||
"Repost Accounting Ledger Items",
|
"Repost Accounting Ledger Items",
|
||||||
|
"Unreconcile Payments",
|
||||||
|
"Unreconcile Payment Entries",
|
||||||
)
|
)
|
||||||
super(PaymentEntry, self).on_cancel()
|
super(PaymentEntry, self).on_cancel()
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.make_advance_gl_entries(cancel=1)
|
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.update_advance_paid()
|
self.update_advance_paid()
|
||||||
self.delink_advance_entry_references()
|
self.delink_advance_entry_references()
|
||||||
@ -271,16 +271,18 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||||
latest = latest.get(d.payment_term) or latest.get(None)
|
latest = latest.get(d.payment_term) or latest.get(None)
|
||||||
|
|
||||||
# The reference has already been fully paid
|
# The reference has already been fully paid
|
||||||
if not latest:
|
if not latest:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||||
)
|
)
|
||||||
# The reference has already been partly paid
|
# The reference has already been partly paid
|
||||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
elif (
|
||||||
d.outstanding_amount, d.precision("outstanding_amount")
|
latest.outstanding_amount < latest.invoice_amount
|
||||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
|
||||||
|
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
|
||||||
|
and d.payment_term == ""
|
||||||
|
):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||||
@ -856,6 +858,11 @@ class PaymentEntry(AccountsController):
|
|||||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# on rare case, when `exchange_rate` is unset, gain/loss amount is incorrectly calculated
|
||||||
|
# for base currency transactions
|
||||||
|
if d.exchange_rate is None:
|
||||||
|
d.exchange_rate = 1
|
||||||
|
|
||||||
allocated_amount_in_pe_exchange_rate = flt(
|
allocated_amount_in_pe_exchange_rate = flt(
|
||||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||||
)
|
)
|
||||||
@ -999,14 +1006,14 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.payment_type == "Internal Transfer":
|
if self.payment_type == "Internal Transfer":
|
||||||
remarks = [
|
remarks = [
|
||||||
_("Amount {0} {1} transferred from {2} to {3}").format(
|
_("Amount {0} {1} transferred from {2} to {3}").format(
|
||||||
self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to
|
_(self.paid_from_account_currency), self.paid_amount, self.paid_from, self.paid_to
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
|
|
||||||
remarks = [
|
remarks = [
|
||||||
_("Amount {0} {1} {2} {3}").format(
|
_("Amount {0} {1} {2} {3}").format(
|
||||||
self.party_account_currency,
|
_(self.party_account_currency),
|
||||||
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
|
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
|
||||||
_("received from") if self.payment_type == "Receive" else _("to"),
|
_("received from") if self.payment_type == "Receive" else _("to"),
|
||||||
self.party,
|
self.party,
|
||||||
@ -1023,14 +1030,14 @@ class PaymentEntry(AccountsController):
|
|||||||
if d.allocated_amount:
|
if d.allocated_amount:
|
||||||
remarks.append(
|
remarks.append(
|
||||||
_("Amount {0} {1} against {2} {3}").format(
|
_("Amount {0} {1} against {2} {3}").format(
|
||||||
self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name
|
_(self.party_account_currency), d.allocated_amount, d.reference_doctype, d.reference_name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for d in self.get("deductions"):
|
for d in self.get("deductions"):
|
||||||
if d.amount:
|
if d.amount:
|
||||||
remarks.append(
|
remarks.append(
|
||||||
_("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account)
|
_("Amount {0} {1} deducted against {2}").format(_(self.company_currency), d.amount, d.account)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.set("remarks", "\n".join(remarks))
|
self.set("remarks", "\n".join(remarks))
|
||||||
@ -1055,6 +1062,8 @@ class PaymentEntry(AccountsController):
|
|||||||
else:
|
else:
|
||||||
self.make_exchange_gain_loss_journal()
|
self.make_exchange_gain_loss_journal()
|
||||||
|
|
||||||
|
self.make_advance_gl_entries(cancel=cancel)
|
||||||
|
|
||||||
def add_party_gl_entries(self, gl_entries):
|
def add_party_gl_entries(self, gl_entries):
|
||||||
if self.party_account:
|
if self.party_account:
|
||||||
if self.payment_type == "Receive":
|
if self.payment_type == "Receive":
|
||||||
@ -1123,7 +1132,7 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.book_advance_payments_in_separate_party_account:
|
if self.book_advance_payments_in_separate_party_account:
|
||||||
gl_entries = []
|
gl_entries = []
|
||||||
for d in self.get("references"):
|
for d in self.get("references"):
|
||||||
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
|
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Journal Entry"):
|
||||||
if not (against_voucher_type and against_voucher) or (
|
if not (against_voucher_type and against_voucher) or (
|
||||||
d.reference_doctype == against_voucher_type and d.reference_name == against_voucher
|
d.reference_doctype == against_voucher_type and d.reference_name == against_voucher
|
||||||
):
|
):
|
||||||
@ -1145,8 +1154,25 @@ class PaymentEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
make_reverse_gl_entries(gl_entries=gl_entries, partial_cancel=True)
|
make_reverse_gl_entries(gl_entries=gl_entries, partial_cancel=True)
|
||||||
else:
|
return
|
||||||
make_gl_entries(gl_entries)
|
|
||||||
|
# same reference added to payment entry
|
||||||
|
for gl_entry in gl_entries.copy():
|
||||||
|
if frappe.db.exists(
|
||||||
|
"GL Entry",
|
||||||
|
{
|
||||||
|
"account": gl_entry.account,
|
||||||
|
"voucher_type": gl_entry.voucher_type,
|
||||||
|
"voucher_no": gl_entry.voucher_no,
|
||||||
|
"voucher_detail_no": gl_entry.voucher_detail_no,
|
||||||
|
"debit": gl_entry.debit,
|
||||||
|
"credit": gl_entry.credit,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
gl_entries.remove(gl_entry)
|
||||||
|
|
||||||
|
make_gl_entries(gl_entries)
|
||||||
|
|
||||||
def make_invoice_liability_entry(self, gl_entries, invoice):
|
def make_invoice_liability_entry(self, gl_entries, invoice):
|
||||||
args_dict = {
|
args_dict = {
|
||||||
@ -1159,6 +1185,13 @@ class PaymentEntry(AccountsController):
|
|||||||
"voucher_detail_no": invoice.name,
|
"voucher_detail_no": invoice.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
posting_date = frappe.db.get_value(
|
||||||
|
invoice.reference_doctype, invoice.reference_name, "posting_date"
|
||||||
|
)
|
||||||
|
|
||||||
|
if getdate(posting_date) < getdate(self.posting_date):
|
||||||
|
posting_date = self.posting_date
|
||||||
|
|
||||||
dr_or_cr = "credit" if invoice.reference_doctype == "Sales Invoice" else "debit"
|
dr_or_cr = "credit" if invoice.reference_doctype == "Sales Invoice" else "debit"
|
||||||
args_dict["account"] = invoice.account
|
args_dict["account"] = invoice.account
|
||||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||||
@ -1167,6 +1200,7 @@ class PaymentEntry(AccountsController):
|
|||||||
{
|
{
|
||||||
"against_voucher_type": invoice.reference_doctype,
|
"against_voucher_type": invoice.reference_doctype,
|
||||||
"against_voucher": invoice.reference_name,
|
"against_voucher": invoice.reference_name,
|
||||||
|
"posting_date": posting_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
gle = self.get_gl_dict(
|
gle = self.get_gl_dict(
|
||||||
@ -1573,6 +1607,14 @@ def get_outstanding_reference_documents(args, validate=False):
|
|||||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||||
)
|
)
|
||||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||||
|
elif args.get(date_fields[0]):
|
||||||
|
# if only from date is supplied
|
||||||
|
condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0]))
|
||||||
|
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
|
||||||
|
elif args.get(date_fields[1]):
|
||||||
|
# if only to date is supplied
|
||||||
|
condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1]))
|
||||||
|
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||||
|
|
||||||
if args.get("company"):
|
if args.get("company"):
|
||||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||||
@ -1711,11 +1753,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
|||||||
"voucher_type": d.voucher_type,
|
"voucher_type": d.voucher_type,
|
||||||
"posting_date": d.posting_date,
|
"posting_date": d.posting_date,
|
||||||
"invoice_amount": flt(d.invoice_amount),
|
"invoice_amount": flt(d.invoice_amount),
|
||||||
"outstanding_amount": flt(d.outstanding_amount),
|
"outstanding_amount": payment_term_outstanding
|
||||||
"payment_term_outstanding": payment_term_outstanding,
|
|
||||||
"allocated_amount": payment_term_outstanding
|
|
||||||
if payment_term_outstanding
|
if payment_term_outstanding
|
||||||
else d.outstanding_amount,
|
else d.outstanding_amount,
|
||||||
|
"payment_term_outstanding": payment_term_outstanding,
|
||||||
"payment_amount": payment_term.payment_amount,
|
"payment_amount": payment_term.payment_amount,
|
||||||
"payment_term": payment_term.payment_term,
|
"payment_term": payment_term.payment_term,
|
||||||
"account": d.account,
|
"account": d.account,
|
||||||
@ -1993,10 +2034,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
if not total_amount:
|
if not total_amount:
|
||||||
if party_account_currency == company_currency:
|
if party_account_currency == company_currency:
|
||||||
# for handling cases that don't have multi-currency (base field)
|
# for handling cases that don't have multi-currency (base field)
|
||||||
total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
|
total_amount = (
|
||||||
|
ref_doc.get("base_rounded_total")
|
||||||
|
or ref_doc.get("rounded_total")
|
||||||
|
or ref_doc.get("base_grand_total")
|
||||||
|
or ref_doc.get("grand_total")
|
||||||
|
)
|
||||||
exchange_rate = 1
|
exchange_rate = 1
|
||||||
else:
|
else:
|
||||||
total_amount = ref_doc.get("grand_total")
|
total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total")
|
||||||
if not exchange_rate:
|
if not exchange_rate:
|
||||||
# Get the exchange rate from the original ref doc
|
# Get the exchange rate from the original ref doc
|
||||||
# or get it based on the posting date of the ref doc.
|
# or get it based on the posting date of the ref doc.
|
||||||
@ -2295,7 +2341,7 @@ def set_paid_amount_and_received_amount(
|
|||||||
if bank_amount:
|
if bank_amount:
|
||||||
received_amount = bank_amount
|
received_amount = bank_amount
|
||||||
else:
|
else:
|
||||||
if company_currency != bank.account_currency:
|
if bank and company_currency != bank.account_currency:
|
||||||
received_amount = paid_amount / doc.get("conversion_rate", 1)
|
received_amount = paid_amount / doc.get("conversion_rate", 1)
|
||||||
else:
|
else:
|
||||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||||
@ -2304,7 +2350,7 @@ def set_paid_amount_and_received_amount(
|
|||||||
if bank_amount:
|
if bank_amount:
|
||||||
paid_amount = bank_amount
|
paid_amount = bank_amount
|
||||||
else:
|
else:
|
||||||
if company_currency != bank.account_currency:
|
if bank and company_currency != bank.account_currency:
|
||||||
paid_amount = received_amount / doc.get("conversion_rate", 1)
|
paid_amount = received_amount / doc.get("conversion_rate", 1)
|
||||||
else:
|
else:
|
||||||
# if party account currency and bank currency is different then populate paid amount as well
|
# if party account currency and bank currency is different then populate paid amount as well
|
||||||
|
@ -702,7 +702,50 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
pe2.submit()
|
pe2.submit()
|
||||||
|
|
||||||
# create return entry against si1
|
# create return entry against si1
|
||||||
create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
|
cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
|
||||||
|
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
||||||
|
|
||||||
|
# create JE(credit note) manually against si1 and cr_note
|
||||||
|
je = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Journal Entry",
|
||||||
|
"company": si1.company,
|
||||||
|
"voucher_type": "Credit Note",
|
||||||
|
"posting_date": nowdate(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
je.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": si1.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": si1.customer,
|
||||||
|
"debit": 0,
|
||||||
|
"credit": 100,
|
||||||
|
"debit_in_account_currency": 0,
|
||||||
|
"credit_in_account_currency": 100,
|
||||||
|
"reference_type": si1.doctype,
|
||||||
|
"reference_name": si1.name,
|
||||||
|
"cost_center": si1.items[0].cost_center,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
je.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": cr_note.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": cr_note.customer,
|
||||||
|
"debit": 100,
|
||||||
|
"credit": 0,
|
||||||
|
"debit_in_account_currency": 100,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
"reference_type": cr_note.doctype,
|
||||||
|
"reference_name": cr_note.name,
|
||||||
|
"cost_center": cr_note.items[0].cost_center,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
je.save().submit()
|
||||||
|
|
||||||
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
||||||
self.assertEqual(si1_outstanding, -100)
|
self.assertEqual(si1_outstanding, -100)
|
||||||
|
|
||||||
@ -1201,6 +1244,24 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
template.allocate_payment_based_on_payment_terms = 1
|
template.allocate_payment_based_on_payment_terms = 1
|
||||||
template.save()
|
template.save()
|
||||||
|
|
||||||
|
def test_allocation_validation_for_sales_order(self):
|
||||||
|
so = make_sales_order(do_not_save=True)
|
||||||
|
so.items[0].rate = 99.55
|
||||||
|
so.save().submit()
|
||||||
|
self.assertGreater(so.rounded_total, 0.0)
|
||||||
|
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||||
|
pe.paid_from = "Debtors - _TC"
|
||||||
|
pe.paid_amount = 45.55
|
||||||
|
pe.references[0].allocated_amount = 45.55
|
||||||
|
pe.save().submit()
|
||||||
|
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||||
|
pe.paid_from = "Debtors - _TC"
|
||||||
|
# No validation error should be thrown here.
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.advance_paid, so.rounded_total)
|
||||||
|
|
||||||
|
|
||||||
def create_payment_entry(**args):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
@ -294,7 +294,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
|||||||
cr_note1.return_against = si3.name
|
cr_note1.return_against = si3.name
|
||||||
cr_note1 = cr_note1.save().submit()
|
cr_note1 = cr_note1.save().submit()
|
||||||
|
|
||||||
pl_entries = (
|
pl_entries_si3 = (
|
||||||
qb.from_(ple)
|
qb.from_(ple)
|
||||||
.select(
|
.select(
|
||||||
ple.voucher_type,
|
ple.voucher_type,
|
||||||
@ -309,7 +309,24 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
|||||||
.run(as_dict=True)
|
.run(as_dict=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_values = [
|
pl_entries_cr_note1 = (
|
||||||
|
qb.from_(ple)
|
||||||
|
.select(
|
||||||
|
ple.voucher_type,
|
||||||
|
ple.voucher_no,
|
||||||
|
ple.against_voucher_type,
|
||||||
|
ple.against_voucher_no,
|
||||||
|
ple.amount,
|
||||||
|
ple.delinked,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)
|
||||||
|
)
|
||||||
|
.orderby(ple.creation)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_values_for_si3 = [
|
||||||
{
|
{
|
||||||
"voucher_type": si3.doctype,
|
"voucher_type": si3.doctype,
|
||||||
"voucher_no": si3.name,
|
"voucher_no": si3.name,
|
||||||
@ -317,18 +334,21 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
|||||||
"against_voucher_no": si3.name,
|
"against_voucher_no": si3.name,
|
||||||
"amount": amount,
|
"amount": amount,
|
||||||
"delinked": 0,
|
"delinked": 0,
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
# credit/debit notes post ledger entries against itself
|
||||||
|
expected_values_for_cr_note1 = [
|
||||||
{
|
{
|
||||||
"voucher_type": cr_note1.doctype,
|
"voucher_type": cr_note1.doctype,
|
||||||
"voucher_no": cr_note1.name,
|
"voucher_no": cr_note1.name,
|
||||||
"against_voucher_type": si3.doctype,
|
"against_voucher_type": cr_note1.doctype,
|
||||||
"against_voucher_no": si3.name,
|
"against_voucher_no": cr_note1.name,
|
||||||
"amount": -amount,
|
"amount": -amount,
|
||||||
"delinked": 0,
|
"delinked": 0,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
self.assertEqual(pl_entries[0], expected_values[0])
|
self.assertEqual(pl_entries_si3, expected_values_for_si3)
|
||||||
self.assertEqual(pl_entries[1], expected_values[1])
|
self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1)
|
||||||
|
|
||||||
def test_je_against_inv_and_note(self):
|
def test_je_against_inv_and_note(self):
|
||||||
ple = self.ple
|
ple = self.ple
|
||||||
|
@ -24,7 +24,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
filters: {
|
filters: {
|
||||||
"company": this.frm.doc.company,
|
"company": this.frm.doc.company,
|
||||||
"is_group": 0,
|
"is_group": 0,
|
||||||
"account_type": frappe.boot.party_account_types[this.frm.doc.party_type]
|
"account_type": frappe.boot.party_account_types[this.frm.doc.party_type],
|
||||||
|
"root_type": this.frm.doc.party_type == 'Customer' ? "Asset" : "Liability"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -163,6 +164,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
this.frm.refresh();
|
this.frm.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invoice_name() {
|
||||||
|
this.frm.trigger("get_unreconciled_entries");
|
||||||
|
}
|
||||||
|
|
||||||
|
payment_name() {
|
||||||
|
this.frm.trigger("get_unreconciled_entries");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
clear_child_tables() {
|
clear_child_tables() {
|
||||||
this.frm.clear_table("invoices");
|
this.frm.clear_table("invoices");
|
||||||
this.frm.clear_table("payments");
|
this.frm.clear_table("payments");
|
||||||
|
@ -27,8 +27,10 @@
|
|||||||
"bank_cash_account",
|
"bank_cash_account",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"sec_break1",
|
"sec_break1",
|
||||||
|
"invoice_name",
|
||||||
"invoices",
|
"invoices",
|
||||||
"column_break_15",
|
"column_break_15",
|
||||||
|
"payment_name",
|
||||||
"payments",
|
"payments",
|
||||||
"sec_break2",
|
"sec_break2",
|
||||||
"allocation"
|
"allocation"
|
||||||
@ -137,6 +139,7 @@
|
|||||||
"label": "Minimum Invoice Amount"
|
"label": "Minimum Invoice Amount"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "50",
|
||||||
"description": "System will fetch all the entries if limit value is zero.",
|
"description": "System will fetch all the entries if limit value is zero.",
|
||||||
"fieldname": "invoice_limit",
|
"fieldname": "invoice_limit",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
@ -167,6 +170,7 @@
|
|||||||
"label": "Maximum Payment Amount"
|
"label": "Maximum Payment Amount"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "50",
|
||||||
"description": "System will fetch all the entries if limit value is zero.",
|
"description": "System will fetch all the entries if limit value is zero.",
|
||||||
"fieldname": "payment_limit",
|
"fieldname": "payment_limit",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
@ -194,13 +198,23 @@
|
|||||||
"label": "Default Advance Account",
|
"label": "Default Advance Account",
|
||||||
"mandatory_depends_on": "doc.party_type",
|
"mandatory_depends_on": "doc.party_type",
|
||||||
"options": "Account"
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "invoice_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Filter on Invoice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Filter on Payment"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"icon": "icon-resize-horizontal",
|
"icon": "icon-resize-horizontal",
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-09 13:02:48.718362",
|
"modified": "2023-08-15 05:35:50.109290",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation",
|
"name": "Payment Reconciliation",
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint, qb
|
from frappe import _, msgprint, qb
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder import Criterion
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ from erpnext.accounts.utils import (
|
|||||||
get_outstanding_invoices,
|
get_outstanding_invoices,
|
||||||
reconcile_against_document,
|
reconcile_against_document,
|
||||||
)
|
)
|
||||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional
|
||||||
|
|
||||||
|
|
||||||
class PaymentReconciliation(Document):
|
class PaymentReconciliation(Document):
|
||||||
@ -74,7 +75,10 @@ class PaymentReconciliation(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_entries = get_advance_payment_entries(
|
if self.payment_name:
|
||||||
|
condition.update({"name": self.payment_name})
|
||||||
|
|
||||||
|
payment_entries = get_advance_payment_entries_for_regional(
|
||||||
self.party_type,
|
self.party_type,
|
||||||
self.party,
|
self.party,
|
||||||
party_account,
|
party_account,
|
||||||
@ -89,6 +93,9 @@ class PaymentReconciliation(Document):
|
|||||||
def get_jv_entries(self):
|
def get_jv_entries(self):
|
||||||
condition = self.get_conditions()
|
condition = self.get_conditions()
|
||||||
|
|
||||||
|
if self.payment_name:
|
||||||
|
condition += f" and t1.name like '%%{self.payment_name}%%'"
|
||||||
|
|
||||||
if self.get("cost_center"):
|
if self.get("cost_center"):
|
||||||
condition += f" and t2.cost_center = '{self.cost_center}' "
|
condition += f" and t2.cost_center = '{self.cost_center}' "
|
||||||
|
|
||||||
@ -109,7 +116,7 @@ class PaymentReconciliation(Document):
|
|||||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||||
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
||||||
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
|
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
|
||||||
t2.account_currency as currency
|
t2.account_currency as currency, t2.cost_center as cost_center
|
||||||
from
|
from
|
||||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||||
where
|
where
|
||||||
@ -146,6 +153,15 @@ class PaymentReconciliation(Document):
|
|||||||
def get_return_invoices(self):
|
def get_return_invoices(self):
|
||||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||||
doc = qb.DocType(voucher_type)
|
doc = qb.DocType(voucher_type)
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
conditions.append(doc.docstatus == 1)
|
||||||
|
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||||
|
conditions.append(doc.is_return == 1)
|
||||||
|
|
||||||
|
if self.payment_name:
|
||||||
|
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||||
|
|
||||||
self.return_invoices = (
|
self.return_invoices = (
|
||||||
qb.from_(doc)
|
qb.from_(doc)
|
||||||
.select(
|
.select(
|
||||||
@ -153,11 +169,7 @@ class PaymentReconciliation(Document):
|
|||||||
doc.name.as_("voucher_no"),
|
doc.name.as_("voucher_no"),
|
||||||
doc.return_against,
|
doc.return_against,
|
||||||
)
|
)
|
||||||
.where(
|
.where(Criterion.all(conditions))
|
||||||
(doc.docstatus == 1)
|
|
||||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
|
||||||
& (doc.is_return == 1)
|
|
||||||
)
|
|
||||||
.run(as_dict=True)
|
.run(as_dict=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -174,15 +186,12 @@ class PaymentReconciliation(Document):
|
|||||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||||
|
|
||||||
self.get_return_invoices()
|
self.get_return_invoices()
|
||||||
return_invoices = [
|
|
||||||
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
|
||||||
]
|
|
||||||
|
|
||||||
outstanding_dr_or_cr = []
|
outstanding_dr_or_cr = []
|
||||||
if return_invoices:
|
if self.return_invoices:
|
||||||
ple_query = QueryPaymentLedger()
|
ple_query = QueryPaymentLedger()
|
||||||
return_outstanding = ple_query.get_voucher_outstandings(
|
return_outstanding = ple_query.get_voucher_outstandings(
|
||||||
vouchers=return_invoices,
|
vouchers=self.return_invoices,
|
||||||
common_filter=self.common_filter_conditions,
|
common_filter=self.common_filter_conditions,
|
||||||
posting_date=self.ple_posting_date_filter,
|
posting_date=self.ple_posting_date_filter,
|
||||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||||
@ -200,6 +209,7 @@ class PaymentReconciliation(Document):
|
|||||||
"amount": -(inv.outstanding_in_account_currency),
|
"amount": -(inv.outstanding_in_account_currency),
|
||||||
"posting_date": inv.posting_date,
|
"posting_date": inv.posting_date,
|
||||||
"currency": inv.currency,
|
"currency": inv.currency,
|
||||||
|
"cost_center": inv.cost_center,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -226,6 +236,8 @@ class PaymentReconciliation(Document):
|
|||||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||||
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
||||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||||
|
limit=self.invoice_limit,
|
||||||
|
voucher_no=self.invoice_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
cr_dr_notes = (
|
cr_dr_notes = (
|
||||||
@ -346,10 +358,12 @@ class PaymentReconciliation(Document):
|
|||||||
"allocated_amount": allocated_amount,
|
"allocated_amount": allocated_amount,
|
||||||
"difference_amount": pay.get("difference_amount"),
|
"difference_amount": pay.get("difference_amount"),
|
||||||
"currency": inv.get("currency"),
|
"currency": inv.get("currency"),
|
||||||
|
"cost_center": pay.get("cost_center"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
||||||
|
adjust_allocations_for_taxes(self)
|
||||||
dr_or_cr = (
|
dr_or_cr = (
|
||||||
"credit_in_account_currency"
|
"credit_in_account_currency"
|
||||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||||
@ -420,6 +434,7 @@ class PaymentReconciliation(Document):
|
|||||||
"allocated_amount": flt(row.get("allocated_amount")),
|
"allocated_amount": flt(row.get("allocated_amount")),
|
||||||
"difference_amount": flt(row.get("difference_amount")),
|
"difference_amount": flt(row.get("difference_amount")),
|
||||||
"difference_account": row.get("difference_account"),
|
"difference_account": row.get("difference_account"),
|
||||||
|
"cost_center": row.get("cost_center"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -592,7 +607,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
inv.dr_or_cr: abs(inv.allocated_amount),
|
inv.dr_or_cr: abs(inv.allocated_amount),
|
||||||
"reference_type": inv.against_voucher_type,
|
"reference_type": inv.against_voucher_type,
|
||||||
"reference_name": inv.against_voucher,
|
"reference_name": inv.against_voucher,
|
||||||
"cost_center": erpnext.get_default_cost_center(company),
|
"cost_center": inv.cost_center or erpnext.get_default_cost_center(company),
|
||||||
"exchange_rate": inv.exchange_rate,
|
"exchange_rate": inv.exchange_rate,
|
||||||
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}",
|
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}",
|
||||||
},
|
},
|
||||||
@ -607,7 +622,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
),
|
),
|
||||||
"reference_type": inv.voucher_type,
|
"reference_type": inv.voucher_type,
|
||||||
"reference_name": inv.voucher_no,
|
"reference_name": inv.voucher_no,
|
||||||
"cost_center": erpnext.get_default_cost_center(company),
|
"cost_center": inv.cost_center or erpnext.get_default_cost_center(company),
|
||||||
"exchange_rate": inv.exchange_rate,
|
"exchange_rate": inv.exchange_rate,
|
||||||
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}",
|
"user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}",
|
||||||
},
|
},
|
||||||
@ -633,6 +648,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
|
|
||||||
create_gain_loss_journal(
|
create_gain_loss_journal(
|
||||||
company,
|
company,
|
||||||
|
today(),
|
||||||
inv.party_type,
|
inv.party_type,
|
||||||
inv.party,
|
inv.party,
|
||||||
inv.account,
|
inv.account,
|
||||||
@ -646,4 +662,10 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
inv.against_voucher_type,
|
inv.against_voucher_type,
|
||||||
inv.against_voucher,
|
inv.against_voucher,
|
||||||
None,
|
None,
|
||||||
|
inv.cost_center,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@erpnext.allow_regional
|
||||||
|
def adjust_allocations_for_taxes(doc):
|
||||||
|
pass
|
||||||
|
@ -22,7 +22,8 @@
|
|||||||
"column_break_7",
|
"column_break_7",
|
||||||
"difference_account",
|
"difference_account",
|
||||||
"exchange_rate",
|
"exchange_rate",
|
||||||
"currency"
|
"currency",
|
||||||
|
"cost_center"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -144,11 +145,17 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Exchange Rate",
|
"label": "Exchange Rate",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "cost_center",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cost Center",
|
||||||
|
"options": "Cost Center"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-12-24 21:01:14.882747",
|
"modified": "2023-09-03 07:52:33.684217",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Allocation",
|
"name": "Payment Reconciliation Allocation",
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
"sec_break1",
|
"sec_break1",
|
||||||
"remark",
|
"remark",
|
||||||
"currency",
|
"currency",
|
||||||
"exchange_rate"
|
"exchange_rate",
|
||||||
|
"cost_center"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -98,11 +99,17 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Exchange Rate"
|
"label": "Exchange Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "cost_center",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cost Center",
|
||||||
|
"options": "Cost Center"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-08 18:18:36.268760",
|
"modified": "2023-09-03 07:43:29.965353",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Payment",
|
"name": "Payment Reconciliation Payment",
|
||||||
|
@ -230,6 +230,28 @@
|
|||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"label": "SWIFT Number"
|
"label": "SWIFT Number"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "accounting_dimensions_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounting Dimensions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "cost_center",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cost Center",
|
||||||
|
"options": "Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dimension_col_break",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.payment_request_type == 'Inward'",
|
"depends_on": "eval: doc.payment_request_type == 'Inward'",
|
||||||
"fieldname": "recipient_and_message",
|
"fieldname": "recipient_and_message",
|
||||||
@ -317,9 +339,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "payment_url",
|
"fieldname": "payment_url",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "payment_url",
|
"length": 500,
|
||||||
|
"options": "URL",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -343,6 +366,14 @@
|
|||||||
"label": "Payment Account",
|
"label": "Payment Account",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "payment_gateway_account.payment_channel",
|
||||||
|
"fieldname": "payment_channel",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Payment Channel",
|
||||||
|
"options": "\nEmail\nPhone",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "payment_order",
|
"fieldname": "payment_order",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -358,43 +389,13 @@
|
|||||||
"options": "Payment Request",
|
"options": "Payment Request",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "payment_gateway_account.payment_channel",
|
|
||||||
"fieldname": "payment_channel",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Payment Channel",
|
|
||||||
"options": "\nEmail\nPhone",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "accounting_dimensions_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Accounting Dimensions"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "cost_center",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Cost Center",
|
|
||||||
"options": "Cost Center"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "dimension_col_break",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "project",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Project",
|
|
||||||
"options": "Project"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-12-21 16:56:40.115737",
|
"modified": "2023-09-27 09:51:42.277638",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Request",
|
"name": "Payment Request",
|
||||||
|
@ -249,7 +249,7 @@ class PaymentRequest(Document):
|
|||||||
if (
|
if (
|
||||||
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
|
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
|
||||||
):
|
):
|
||||||
party_amount = ref_doc.base_grand_total
|
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
||||||
else:
|
else:
|
||||||
party_amount = self.grand_total
|
party_amount = self.grand_total
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"transaction_date",
|
"transaction_date",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"fiscal_year",
|
"fiscal_year",
|
||||||
|
"year_start_date",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"company",
|
"company",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
@ -100,16 +101,22 @@
|
|||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Error Message",
|
"label": "Error Message",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "year_start_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Year Start Date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-07-20 14:51:04.714154",
|
"modified": "2023-09-11 20:19:11.810533",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Period Closing Voucher",
|
"name": "Period Closing Voucher",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -144,5 +151,6 @@
|
|||||||
"search_fields": "posting_date, fiscal_year",
|
"search_fields": "posting_date, fiscal_year",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"title_field": "closing_account_head"
|
"title_field": "closing_account_head"
|
||||||
}
|
}
|
@ -33,7 +33,7 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.validate_future_closing_vouchers()
|
self.validate_future_closing_vouchers()
|
||||||
self.db_set("gle_processing_status", "In Progress")
|
self.db_set("gle_processing_status", "In Progress")
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||||
gle_count = frappe.db.count(
|
gle_count = frappe.db.count(
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||||
@ -95,15 +95,23 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
|
|
||||||
self.check_if_previous_year_closed()
|
self.check_if_previous_year_closed()
|
||||||
|
|
||||||
pce = frappe.db.sql(
|
pcv = frappe.qb.DocType("Period Closing Voucher")
|
||||||
"""select name from `tabPeriod Closing Voucher`
|
existing_entry = (
|
||||||
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
|
frappe.qb.from_(pcv)
|
||||||
(self.posting_date, self.fiscal_year, self.company),
|
.select(pcv.name)
|
||||||
|
.where(
|
||||||
|
(pcv.posting_date >= self.posting_date)
|
||||||
|
& (pcv.fiscal_year == self.fiscal_year)
|
||||||
|
& (pcv.docstatus == 1)
|
||||||
|
& (pcv.company == self.company)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
)
|
)
|
||||||
if pce and pce[0][0]:
|
|
||||||
|
if existing_entry and existing_entry[0][0]:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Another Period Closing Entry {0} has been made after {1}").format(
|
_("Another Period Closing Entry {0} has been made after {1}").format(
|
||||||
pce[0][0], self.posting_date
|
existing_entry[0][0], self.posting_date
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -126,22 +134,31 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
def make_gl_entries(self, get_opening_entries=False):
|
def make_gl_entries(self, get_opening_entries=False):
|
||||||
gl_entries = self.get_gl_entries()
|
gl_entries = self.get_gl_entries()
|
||||||
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
||||||
if len(gl_entries) > 5000:
|
if len(gl_entries + closing_entries) > 3000:
|
||||||
frappe.enqueue(
|
frappe.enqueue(
|
||||||
process_gl_entries,
|
process_gl_entries,
|
||||||
gl_entries=gl_entries,
|
gl_entries=gl_entries,
|
||||||
|
voucher_name=self.name,
|
||||||
|
timeout=3000,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.enqueue(
|
||||||
|
process_closing_entries,
|
||||||
|
gl_entries=gl_entries,
|
||||||
closing_entries=closing_entries,
|
closing_entries=closing_entries,
|
||||||
voucher_name=self.name,
|
voucher_name=self.name,
|
||||||
company=self.company,
|
company=self.company,
|
||||||
closing_date=self.posting_date,
|
closing_date=self.posting_date,
|
||||||
queue="long",
|
timeout=3000,
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
||||||
alert=True,
|
alert=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
process_gl_entries(gl_entries, self.name)
|
||||||
|
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
||||||
|
|
||||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
def get_grouped_gl_entries(self, get_opening_entries=False):
|
||||||
closing_entries = []
|
closing_entries = []
|
||||||
@ -322,17 +339,12 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
return query.run(as_dict=1)
|
return query.run(as_dict=1)
|
||||||
|
|
||||||
|
|
||||||
def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
def process_gl_entries(gl_entries, voucher_name):
|
||||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
|
||||||
make_closing_entries,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.general_ledger import make_gl_entries
|
from erpnext.accounts.general_ledger import make_gl_entries
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if gl_entries:
|
if gl_entries:
|
||||||
make_gl_entries(gl_entries, merge_entries=False)
|
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")
|
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
@ -340,6 +352,19 @@ def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closi
|
|||||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
||||||
|
|
||||||
|
|
||||||
|
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||||
|
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||||
|
make_closing_entries,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if gl_entries + closing_entries:
|
||||||
|
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.db.rollback()
|
||||||
|
frappe.log_error(e)
|
||||||
|
|
||||||
|
|
||||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
def make_reverse_gl_entries(voucher_type, voucher_no):
|
||||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from frappe.utils import today
|
|||||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
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
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.utils import get_fiscal_year, now
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
|
|
||||||
class TestPeriodClosingVoucher(unittest.TestCase):
|
class TestPeriodClosingVoucher(unittest.TestCase):
|
||||||
|
@ -5,6 +5,10 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||||
|
create_dimension,
|
||||||
|
disable_dimension,
|
||||||
|
)
|
||||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
||||||
make_closing_entry_from_opening,
|
make_closing_entry_from_opening,
|
||||||
)
|
)
|
||||||
@ -140,6 +144,43 @@ class TestPOSClosingEntry(unittest.TestCase):
|
|||||||
pos_inv1.load_from_db()
|
pos_inv1.load_from_db()
|
||||||
self.assertEqual(pos_inv1.status, "Paid")
|
self.assertEqual(pos_inv1.status, "Paid")
|
||||||
|
|
||||||
|
def test_pos_closing_for_required_accounting_dimension_in_pos_profile(self):
|
||||||
|
"""
|
||||||
|
test case to check whether we can create POS Closing Entry without mandatory accounting dimension
|
||||||
|
"""
|
||||||
|
|
||||||
|
create_dimension()
|
||||||
|
pos_profile = make_pos_profile(do_not_insert=1, do_not_set_accounting_dimension=1)
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, pos_profile.insert)
|
||||||
|
|
||||||
|
pos_profile.location = "Block 1"
|
||||||
|
pos_profile.insert()
|
||||||
|
self.assertTrue(frappe.db.exists("POS Profile", pos_profile.name))
|
||||||
|
|
||||||
|
test_user = init_user_and_profile(do_not_create_pos_profile=1)
|
||||||
|
|
||||||
|
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||||
|
pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
|
||||||
|
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||||
|
pos_inv1.submit()
|
||||||
|
|
||||||
|
# if in between a mandatory accounting dimension is added to the POS Profile then
|
||||||
|
accounting_dimension_department = frappe.get_doc("Accounting Dimension", {"name": "Department"})
|
||||||
|
accounting_dimension_department.dimension_defaults[0].mandatory_for_bs = 1
|
||||||
|
accounting_dimension_department.save()
|
||||||
|
|
||||||
|
pcv_doc = make_closing_entry_from_opening(opening_entry)
|
||||||
|
# will assert coz the new mandatory accounting dimension bank is not set in POS Profile
|
||||||
|
self.assertRaises(frappe.ValidationError, pcv_doc.submit)
|
||||||
|
|
||||||
|
accounting_dimension_department = frappe.get_doc(
|
||||||
|
"Accounting Dimension Detail", {"parent": "Department"}
|
||||||
|
)
|
||||||
|
accounting_dimension_department.mandatory_for_bs = 0
|
||||||
|
accounting_dimension_department.save()
|
||||||
|
disable_dimension()
|
||||||
|
|
||||||
|
|
||||||
def init_user_and_profile(**args):
|
def init_user_and_profile(**args):
|
||||||
user = "test@example.com"
|
user = "test@example.com"
|
||||||
@ -149,6 +190,9 @@ def init_user_and_profile(**args):
|
|||||||
test_user.add_roles(*roles)
|
test_user.add_roles(*roles)
|
||||||
frappe.set_user(user)
|
frappe.set_user(user)
|
||||||
|
|
||||||
|
if args.get("do_not_create_pos_profile"):
|
||||||
|
return test_user
|
||||||
|
|
||||||
pos_profile = make_pos_profile(**args)
|
pos_profile = make_pos_profile(**args)
|
||||||
pos_profile.append("applicable_for_users", {"default": 1, "user": user})
|
pos_profile.append("applicable_for_users", {"default": 1, "user": user})
|
||||||
|
|
||||||
|
@ -414,7 +414,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
selling_price_list = (
|
selling_price_list = (
|
||||||
customer_price_list or customer_group_price_list or profile.get("selling_price_list")
|
customer_price_list or customer_group_price_list or profile.get("selling_price_list")
|
||||||
)
|
)
|
||||||
if customer_currency != profile.get("currency"):
|
if customer_currency and customer_currency != profile.get("currency"):
|
||||||
self.set("currency", customer_currency)
|
self.set("currency", customer_currency)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -12,6 +12,8 @@ from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
|||||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||||
from frappe.utils.scheduler import is_scheduler_inactive
|
from frappe.utils.scheduler import is_scheduler_inactive
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.pos_profile.pos_profile import required_accounting_dimensions
|
||||||
|
|
||||||
|
|
||||||
class POSInvoiceMergeLog(Document):
|
class POSInvoiceMergeLog(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -163,7 +165,8 @@ class POSInvoiceMergeLog(Document):
|
|||||||
for i in items:
|
for i in items:
|
||||||
if (
|
if (
|
||||||
i.item_code == item.item_code
|
i.item_code == item.item_code
|
||||||
and not i.serial_and_batch_bundle
|
and not i.serial_no
|
||||||
|
and not i.batch_no
|
||||||
and i.uom == item.uom
|
and i.uom == item.uom
|
||||||
and i.net_rate == item.net_rate
|
and i.net_rate == item.net_rate
|
||||||
and i.warehouse == item.warehouse
|
and i.warehouse == item.warehouse
|
||||||
@ -238,6 +241,22 @@ class POSInvoiceMergeLog(Document):
|
|||||||
invoice.disable_rounded_total = cint(
|
invoice.disable_rounded_total = cint(
|
||||||
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
|
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
|
||||||
)
|
)
|
||||||
|
accounting_dimensions = required_accounting_dimensions()
|
||||||
|
dimension_values = frappe.db.get_value(
|
||||||
|
"POS Profile", {"name": invoice.pos_profile}, accounting_dimensions, as_dict=1
|
||||||
|
)
|
||||||
|
for dimension in accounting_dimensions:
|
||||||
|
dimension_value = dimension_values.get(dimension)
|
||||||
|
|
||||||
|
if not dimension_value:
|
||||||
|
frappe.throw(
|
||||||
|
_("Please set Accounting Dimension {} in {}").format(
|
||||||
|
frappe.bold(frappe.unscrub(dimension)),
|
||||||
|
frappe.get_desk_link("POS Profile", invoice.pos_profile),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
invoice.set(dimension, dimension_value)
|
||||||
|
|
||||||
if self.merge_invoices_based_on == "Customer Group":
|
if self.merge_invoices_based_on == "Customer Group":
|
||||||
invoice.flags.ignore_pos_profile = True
|
invoice.flags.ignore_pos_profile = True
|
||||||
@ -424,11 +443,9 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
|||||||
)
|
)
|
||||||
merge_log.customer = customer
|
merge_log.customer = customer
|
||||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||||
|
|
||||||
merge_log.set("pos_invoices", _invoices)
|
merge_log.set("pos_invoices", _invoices)
|
||||||
merge_log.save(ignore_permissions=True)
|
merge_log.save(ignore_permissions=True)
|
||||||
merge_log.submit()
|
merge_log.submit()
|
||||||
|
|
||||||
if closing_entry:
|
if closing_entry:
|
||||||
closing_entry.set_status(update=True, status="Submitted")
|
closing_entry.set_status(update=True, status="Submitted")
|
||||||
closing_entry.db_set("error_message", "")
|
closing_entry.db_set("error_message", "")
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('POS Profile', {
|
frappe.ui.form.on('POS Profile', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.set_query("selling_price_list", function() {
|
frm.set_query("selling_price_list", function() {
|
||||||
@ -140,6 +139,7 @@ frappe.ui.form.on('POS Profile', {
|
|||||||
company: function(frm) {
|
company: function(frm) {
|
||||||
frm.trigger("toggle_display_account_head");
|
frm.trigger("toggle_display_account_head");
|
||||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
toggle_display_account_head: function(frm) {
|
toggle_display_account_head: function(frm) {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, msgprint, scrub, unscrub
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import get_link_to_form, now
|
from frappe.utils import get_link_to_form, now
|
||||||
|
|
||||||
@ -14,6 +14,21 @@ class POSProfile(Document):
|
|||||||
self.validate_all_link_fields()
|
self.validate_all_link_fields()
|
||||||
self.validate_duplicate_groups()
|
self.validate_duplicate_groups()
|
||||||
self.validate_payment_methods()
|
self.validate_payment_methods()
|
||||||
|
self.validate_accounting_dimensions()
|
||||||
|
|
||||||
|
def validate_accounting_dimensions(self):
|
||||||
|
acc_dim_names = required_accounting_dimensions()
|
||||||
|
for acc_dim in acc_dim_names:
|
||||||
|
if not self.get(acc_dim):
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"{0} is a mandatory Accounting Dimension. <br>"
|
||||||
|
"Please set a value for {0} in Accounting Dimensions section."
|
||||||
|
).format(
|
||||||
|
unscrub(frappe.bold(acc_dim)),
|
||||||
|
),
|
||||||
|
title=_("Mandatory Accounting Dimension"),
|
||||||
|
)
|
||||||
|
|
||||||
def validate_default_profile(self):
|
def validate_default_profile(self):
|
||||||
for row in self.applicable_for_users:
|
for row in self.applicable_for_users:
|
||||||
@ -152,6 +167,24 @@ def get_child_nodes(group_type, root):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def required_accounting_dimensions():
|
||||||
|
|
||||||
|
p = frappe.qb.DocType("Accounting Dimension")
|
||||||
|
c = frappe.qb.DocType("Accounting Dimension Detail")
|
||||||
|
|
||||||
|
acc_dim_doc = (
|
||||||
|
frappe.qb.from_(p)
|
||||||
|
.inner_join(c)
|
||||||
|
.on(p.name == c.parent)
|
||||||
|
.select(c.parent)
|
||||||
|
.where((c.mandatory_for_bs == 1) | (c.mandatory_for_pl == 1))
|
||||||
|
.where(p.disabled == 0)
|
||||||
|
).run(as_dict=1)
|
||||||
|
|
||||||
|
acc_dim_names = [scrub(d.parent) for d in acc_dim_doc]
|
||||||
|
return acc_dim_names
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
@ -5,7 +5,10 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes
|
from erpnext.accounts.doctype.pos_profile.pos_profile import (
|
||||||
|
get_child_nodes,
|
||||||
|
required_accounting_dimensions,
|
||||||
|
)
|
||||||
from erpnext.stock.get_item_details import get_pos_profile
|
from erpnext.stock.get_item_details import get_pos_profile
|
||||||
|
|
||||||
test_dependencies = ["Item"]
|
test_dependencies = ["Item"]
|
||||||
@ -118,6 +121,7 @@ def make_pos_profile(**args):
|
|||||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
|
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
|
||||||
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
|
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
|
||||||
|
"location": "Block 1" if not args.do_not_set_accounting_dimension else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -132,6 +136,7 @@ def make_pos_profile(**args):
|
|||||||
pos_profile.append("payments", {"mode_of_payment": "Cash", "default": 1})
|
pos_profile.append("payments", {"mode_of_payment": "Cash", "default": 1})
|
||||||
|
|
||||||
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
|
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
|
||||||
pos_profile.insert()
|
if not args.get("do_not_insert"):
|
||||||
|
pos_profile.insert()
|
||||||
|
|
||||||
return pos_profile
|
return pos_profile
|
||||||
|
@ -129,7 +129,7 @@ def trigger_job_for_doc(docname: str | None = None):
|
|||||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
|
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
|
||||||
job_name = f"start_processing_{docname}"
|
job_name = f"start_processing_{docname}"
|
||||||
if not is_job_running(job_name):
|
if not is_job_running(job_name):
|
||||||
job = frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
|
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
|
||||||
queue="long",
|
queue="long",
|
||||||
is_async=True,
|
is_async=True,
|
||||||
@ -147,7 +147,7 @@ def trigger_job_for_doc(docname: str | None = None):
|
|||||||
# Resume tasks for running doc
|
# Resume tasks for running doc
|
||||||
job_name = f"start_processing_{docname}"
|
job_name = f"start_processing_{docname}"
|
||||||
if not is_job_running(job_name):
|
if not is_job_running(job_name):
|
||||||
job = frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
|
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
|
||||||
queue="long",
|
queue="long",
|
||||||
is_async=True,
|
is_async=True,
|
||||||
@ -224,7 +224,7 @@ def reconcile_based_on_filters(doc: None | str = None) -> None:
|
|||||||
|
|
||||||
job_name = f"process_{doc}_fetch_and_allocate"
|
job_name = f"process_{doc}_fetch_and_allocate"
|
||||||
if not is_job_running(job_name):
|
if not is_job_running(job_name):
|
||||||
job = frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
|
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
|
||||||
queue="long",
|
queue="long",
|
||||||
timeout="3600",
|
timeout="3600",
|
||||||
@ -245,7 +245,7 @@ def reconcile_based_on_filters(doc: None | str = None) -> None:
|
|||||||
if not allocated:
|
if not allocated:
|
||||||
job_name = f"process__{doc}_fetch_and_allocate"
|
job_name = f"process__{doc}_fetch_and_allocate"
|
||||||
if not is_job_running(job_name):
|
if not is_job_running(job_name):
|
||||||
job = frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
|
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
|
||||||
queue="long",
|
queue="long",
|
||||||
timeout="3600",
|
timeout="3600",
|
||||||
@ -263,7 +263,7 @@ def reconcile_based_on_filters(doc: None | str = None) -> None:
|
|||||||
else:
|
else:
|
||||||
reconcile_job_name = f"process_{doc}_reconcile"
|
reconcile_job_name = f"process_{doc}_reconcile"
|
||||||
if not is_job_running(reconcile_job_name):
|
if not is_job_running(reconcile_job_name):
|
||||||
job = frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||||
queue="long",
|
queue="long",
|
||||||
timeout="3600",
|
timeout="3600",
|
||||||
@ -350,7 +350,7 @@ def fetch_and_allocate(doc: str) -> None:
|
|||||||
reconcile_job_name = f"process_{doc}_reconcile"
|
reconcile_job_name = f"process_{doc}_reconcile"
|
||||||
|
|
||||||
if not is_job_running(reconcile_job_name):
|
if not is_job_running(reconcile_job_name):
|
||||||
job = frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||||
queue="long",
|
queue="long",
|
||||||
timeout="3600",
|
timeout="3600",
|
||||||
@ -462,7 +462,7 @@ def reconcile(doc: None | str = None) -> None:
|
|||||||
reconcile_job_name = f"process_{doc}_reconcile"
|
reconcile_job_name = f"process_{doc}_reconcile"
|
||||||
|
|
||||||
if not is_job_running(reconcile_job_name):
|
if not is_job_running(reconcile_job_name):
|
||||||
job = frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||||
queue="long",
|
queue="long",
|
||||||
timeout="3600",
|
timeout="3600",
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"column_break_21",
|
"column_break_21",
|
||||||
"start_date",
|
"start_date",
|
||||||
"section_break_33",
|
"section_break_33",
|
||||||
|
"pdf_name",
|
||||||
"subject",
|
"subject",
|
||||||
"column_break_28",
|
"column_break_28",
|
||||||
"cc_to",
|
"cc_to",
|
||||||
@ -275,7 +276,7 @@
|
|||||||
"fieldname": "help_text",
|
"fieldname": "help_text",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"label": "Help Text",
|
"label": "Help Text",
|
||||||
"options": "<br>\n<h4>Note</h4>\n<ul>\n<li>\nYou can use <a href=\"https://jinja.palletsprojects.com/en/2.11.x/\" target=\"_blank\">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.\n</li><li>\n All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.\n</li></ul>\n<h4> Examples</h4>\n<!-- {% raw %} -->\n<ul>\n <li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.name }}</code></pre><br></li>\n <li><b>Body</b>: <br><br>\n<pre><code>Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>\n</ul>\n<!-- {% endraw %} -->"
|
"options": "<br>\n<h4>Note</h4>\n<ul>\n<li>\nYou can use <a href=\"https://jinja.palletsprojects.com/en/2.11.x/\" target=\"_blank\">Jinja tags</a> in <b>Subject</b> and <b>Body</b> fields for dynamic values.\n</li><li>\n All fields in this doctype are available under the <b>doc</b> object and all fields for the customer to whom the mail will go to is available under the <b>customer</b> object.\n</li></ul>\n<h4> Examples</h4>\n<!-- {% raw %} -->\n<ul>\n <li><b>Subject</b>:<br><br><pre><code>Statement Of Accounts for {{ customer.customer_name }}</code></pre><br></li>\n <li><b>Body</b>: <br><br>\n<pre><code>Hello {{ customer.customer_name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.</code> </pre></li>\n</ul>\n<!-- {% endraw %} -->"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "subject",
|
"fieldname": "subject",
|
||||||
@ -370,10 +371,15 @@
|
|||||||
"fieldname": "based_on_payment_terms",
|
"fieldname": "based_on_payment_terms",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Based On Payment Terms"
|
"label": "Based On Payment Terms"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pdf_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "PDF Name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-23 10:13:15.051950",
|
"modified": "2023-08-28 12:59:53.071334",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts",
|
"name": "Process Statement Of Accounts",
|
||||||
|
@ -27,7 +27,13 @@ class ProcessStatementOfAccounts(Document):
|
|||||||
if not self.subject:
|
if not self.subject:
|
||||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||||
if not self.body:
|
if not self.body:
|
||||||
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
|
if self.report == "General Ledger":
|
||||||
|
body_str = " from {{ doc.from_date }} to {{ doc.to_date }}."
|
||||||
|
else:
|
||||||
|
body_str = " until {{ doc.posting_date }}."
|
||||||
|
self.body = "Hello {{ customer.customer_name }},<br>PFA your Statement Of Accounts" + body_str
|
||||||
|
if not self.pdf_name:
|
||||||
|
self.pdf_name = "{{ customer.customer_name }}"
|
||||||
|
|
||||||
validate_template(self.subject)
|
validate_template(self.subject)
|
||||||
validate_template(self.body)
|
validate_template(self.body)
|
||||||
@ -42,6 +48,20 @@ class ProcessStatementOfAccounts(Document):
|
|||||||
|
|
||||||
|
|
||||||
def get_report_pdf(doc, consolidated=True):
|
def get_report_pdf(doc, consolidated=True):
|
||||||
|
statement_dict = get_statement_dict(doc)
|
||||||
|
if not bool(statement_dict):
|
||||||
|
return False
|
||||||
|
elif consolidated:
|
||||||
|
delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
|
||||||
|
result = delimiter.join(list(statement_dict.values()))
|
||||||
|
return get_pdf(result, {"orientation": doc.orientation})
|
||||||
|
else:
|
||||||
|
for customer, statement_html in statement_dict.items():
|
||||||
|
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
|
||||||
|
return statement_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_statement_dict(doc, get_statement_dict=False):
|
||||||
statement_dict = {}
|
statement_dict = {}
|
||||||
ageing = ""
|
ageing = ""
|
||||||
|
|
||||||
@ -60,31 +80,23 @@ def get_report_pdf(doc, consolidated=True):
|
|||||||
|
|
||||||
if doc.report == "General Ledger":
|
if doc.report == "General Ledger":
|
||||||
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
|
||||||
else:
|
|
||||||
filters.update(get_ar_filters(doc, entry))
|
|
||||||
|
|
||||||
if doc.report == "General Ledger":
|
|
||||||
col, res = get_soa(filters)
|
col, res = get_soa(filters)
|
||||||
for x in [0, -2, -1]:
|
for x in [0, -2, -1]:
|
||||||
res[x]["account"] = res[x]["account"].replace("'", "")
|
res[x]["account"] = res[x]["account"].replace("'", "")
|
||||||
if len(res) == 3:
|
if len(res) == 3:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
filters.update(get_ar_filters(doc, entry))
|
||||||
ar_res = get_ar_soa(filters)
|
ar_res = get_ar_soa(filters)
|
||||||
col, res = ar_res[0], ar_res[1]
|
col, res = ar_res[0], ar_res[1]
|
||||||
|
if not res:
|
||||||
|
continue
|
||||||
|
|
||||||
statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
|
statement_dict[entry.customer] = (
|
||||||
|
[res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing)
|
||||||
|
)
|
||||||
|
|
||||||
if not bool(statement_dict):
|
return statement_dict
|
||||||
return False
|
|
||||||
elif consolidated:
|
|
||||||
delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
|
|
||||||
result = delimiter.join(list(statement_dict.values()))
|
|
||||||
return get_pdf(result, {"orientation": doc.orientation})
|
|
||||||
else:
|
|
||||||
for customer, statement_html in statement_dict.items():
|
|
||||||
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
|
|
||||||
return statement_dict
|
|
||||||
|
|
||||||
|
|
||||||
def set_ageing(doc, entry):
|
def set_ageing(doc, entry):
|
||||||
@ -97,7 +109,8 @@ def set_ageing(doc, entry):
|
|||||||
"range2": 60,
|
"range2": 60,
|
||||||
"range3": 90,
|
"range3": 90,
|
||||||
"range4": 120,
|
"range4": 120,
|
||||||
"customer": entry.customer,
|
"party_type": "Customer",
|
||||||
|
"party": [entry.customer],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
col1, ageing = get_ageing(ageing_filters)
|
col1, ageing = get_ageing(ageing_filters)
|
||||||
@ -140,7 +153,9 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
|
|||||||
def get_ar_filters(doc, entry):
|
def get_ar_filters(doc, entry):
|
||||||
return {
|
return {
|
||||||
"report_date": doc.posting_date if doc.posting_date else None,
|
"report_date": doc.posting_date if doc.posting_date else None,
|
||||||
"customer": entry.customer,
|
"party_type": "Customer",
|
||||||
|
"party": [entry.customer],
|
||||||
|
"customer_name": entry.customer_name if entry.customer_name else None,
|
||||||
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
|
"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_partner": doc.sales_partner if doc.sales_partner else None,
|
||||||
"sales_person": doc.sales_person if doc.sales_person else None,
|
"sales_person": doc.sales_person if doc.sales_person else None,
|
||||||
@ -366,18 +381,20 @@ def download_statements(document_name):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def send_emails(document_name, from_scheduler=False):
|
def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||||
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
|
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
|
||||||
report = get_report_pdf(doc, consolidated=False)
|
report = get_report_pdf(doc, consolidated=False)
|
||||||
|
|
||||||
if report:
|
if report:
|
||||||
for customer, report_pdf in report.items():
|
for customer, report_pdf in report.items():
|
||||||
attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}]
|
context = get_context(customer, doc)
|
||||||
|
filename = frappe.render_template(doc.pdf_name, context)
|
||||||
|
attachments = [{"fname": filename + ".pdf", "fcontent": report_pdf}]
|
||||||
|
|
||||||
recipients, cc = get_recipients_and_cc(customer, doc)
|
recipients, cc = get_recipients_and_cc(customer, doc)
|
||||||
if not recipients:
|
if not recipients:
|
||||||
continue
|
continue
|
||||||
context = get_context(customer, doc)
|
|
||||||
subject = frappe.render_template(doc.subject, context)
|
subject = frappe.render_template(doc.subject, context)
|
||||||
message = frappe.render_template(doc.body, context)
|
message = frappe.render_template(doc.body, context)
|
||||||
|
|
||||||
@ -396,7 +413,7 @@ def send_emails(document_name, from_scheduler=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if doc.enable_auto_email and from_scheduler:
|
if doc.enable_auto_email and from_scheduler:
|
||||||
new_to_date = getdate(today())
|
new_to_date = getdate(posting_date or today())
|
||||||
if doc.frequency == "Weekly":
|
if doc.frequency == "Weekly":
|
||||||
new_to_date = add_days(new_to_date, 7)
|
new_to_date = add_days(new_to_date, 7)
|
||||||
else:
|
else:
|
||||||
@ -405,8 +422,11 @@ def send_emails(document_name, from_scheduler=False):
|
|||||||
doc.add_comment(
|
doc.add_comment(
|
||||||
"Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())
|
"Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())
|
||||||
)
|
)
|
||||||
doc.db_set("to_date", new_to_date, commit=True)
|
if doc.report == "General Ledger":
|
||||||
doc.db_set("from_date", new_from_date, commit=True)
|
doc.db_set("to_date", new_to_date, commit=True)
|
||||||
|
doc.db_set("from_date", new_from_date, commit=True)
|
||||||
|
else:
|
||||||
|
doc.db_set("posting_date", new_to_date, commit=True)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@ -416,7 +436,8 @@ def send_emails(document_name, from_scheduler=False):
|
|||||||
def send_auto_email():
|
def send_auto_email():
|
||||||
selected = frappe.get_list(
|
selected = frappe.get_list(
|
||||||
"Process Statement Of Accounts",
|
"Process Statement Of Accounts",
|
||||||
filters={"to_date": format_date(today()), "enable_auto_email": 1},
|
filters={"enable_auto_email": 1},
|
||||||
|
or_filters={"to_date": format_date(today()), "posting_date": format_date(today())},
|
||||||
)
|
)
|
||||||
for entry in selected:
|
for entry in selected:
|
||||||
send_emails(entry.name, from_scheduler=True)
|
send_emails(entry.name, from_scheduler=True)
|
||||||
|
@ -8,9 +8,24 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<div id="header-html" class="hidden-pdf">
|
||||||
|
{% if letter_head.content %}
|
||||||
|
<div class="letter-head text-center">{{ letter_head.content }}</div>
|
||||||
|
<hr style="height:2px;border-width:0;color:black;background-color:black;">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div id="footer-html" class="visible-pdf">
|
||||||
|
{% if letter_head.footer %}
|
||||||
|
<div class="letter-head-footer">
|
||||||
|
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
|
||||||
|
{{ letter_head.footer }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
|
||||||
<h4 class="text-center">
|
<h4 class="text-center">
|
||||||
{{ filters.customer }}
|
{{ filters.customer_name }}
|
||||||
</h4>
|
</h4>
|
||||||
<h6 class="text-center">
|
<h6 class="text-center">
|
||||||
{% if (filters.tax_id) %}
|
{% if (filters.tax_id) %}
|
||||||
@ -341,4 +356,9 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if terms_and_conditions %}
|
||||||
|
<div>
|
||||||
|
{{ terms_and_conditions }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>
|
<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>
|
||||||
|
@ -1,9 +1,110 @@
|
|||||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import add_days, getdate, today
|
||||||
|
|
||||||
class TestProcessStatementOfAccounts(unittest.TestCase):
|
from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import (
|
||||||
pass
|
get_statement_dict,
|
||||||
|
send_emails,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.create_company()
|
||||||
|
self.create_customer()
|
||||||
|
self.create_customer(customer_name="Other Customer")
|
||||||
|
self.clear_old_entries()
|
||||||
|
self.si = create_sales_invoice()
|
||||||
|
create_sales_invoice(customer="Other Customer")
|
||||||
|
|
||||||
|
def test_process_soa_for_gl(self):
|
||||||
|
"""Tests the utils for Statement of Accounts(General Ledger)"""
|
||||||
|
process_soa = create_process_soa(
|
||||||
|
name="_Test Process SOA for GL",
|
||||||
|
customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}],
|
||||||
|
)
|
||||||
|
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
|
||||||
|
|
||||||
|
# Checks if the statements are filtered based on the Customer
|
||||||
|
self.assertIn("Other Customer", statement_dict)
|
||||||
|
self.assertIn("_Test Customer", statement_dict)
|
||||||
|
|
||||||
|
# Checks if the correct number of receivable entries exist
|
||||||
|
# 3 rows for opening and closing and 1 row for SI
|
||||||
|
receivable_entries = statement_dict["_Test Customer"][0]
|
||||||
|
self.assertEqual(len(receivable_entries), 4)
|
||||||
|
|
||||||
|
# Checks the amount for the receivable entry
|
||||||
|
self.assertEqual(receivable_entries[1].voucher_no, self.si.name)
|
||||||
|
self.assertEqual(receivable_entries[1].balance, 100)
|
||||||
|
|
||||||
|
def test_process_soa_for_ar(self):
|
||||||
|
"""Tests the utils for Statement of Accounts(Accounts Receivable)"""
|
||||||
|
process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable")
|
||||||
|
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
|
||||||
|
|
||||||
|
# Checks if the statements are filtered based on the Customer
|
||||||
|
self.assertNotIn("Other Customer", statement_dict)
|
||||||
|
self.assertIn("_Test Customer", statement_dict)
|
||||||
|
|
||||||
|
# Checks if the correct number of receivable entries exist
|
||||||
|
receivable_entries = statement_dict["_Test Customer"][0]
|
||||||
|
self.assertEqual(len(receivable_entries), 1)
|
||||||
|
|
||||||
|
# Checks the amount for the receivable entry
|
||||||
|
self.assertEqual(receivable_entries[0].voucher_no, self.si.name)
|
||||||
|
self.assertEqual(receivable_entries[0].total_due, 100)
|
||||||
|
|
||||||
|
# Checks the ageing summary for AR
|
||||||
|
ageing_summary = statement_dict["_Test Customer"][1][0]
|
||||||
|
expected_summary = frappe._dict(
|
||||||
|
range1=100,
|
||||||
|
range2=0,
|
||||||
|
range3=0,
|
||||||
|
range4=0,
|
||||||
|
range5=0,
|
||||||
|
)
|
||||||
|
self.check_ageing_summary(ageing_summary, expected_summary)
|
||||||
|
|
||||||
|
def test_auto_email_for_process_soa_ar(self):
|
||||||
|
process_soa = create_process_soa(
|
||||||
|
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
|
||||||
|
)
|
||||||
|
send_emails(process_soa.name, from_scheduler=True)
|
||||||
|
process_soa.load_from_db()
|
||||||
|
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
|
||||||
|
|
||||||
|
def check_ageing_summary(self, ageing, expected_ageing):
|
||||||
|
for age_range in expected_ageing:
|
||||||
|
self.assertEqual(expected_ageing[age_range], ageing.get(age_range))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
def create_process_soa(**args):
|
||||||
|
args = frappe._dict(args)
|
||||||
|
frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name)
|
||||||
|
process_soa = frappe.new_doc("Process Statement Of Accounts")
|
||||||
|
soa_dict = frappe._dict(
|
||||||
|
name=args.name,
|
||||||
|
company=args.company or "_Test Company",
|
||||||
|
customers=args.customers or [{"customer": "_Test Customer"}],
|
||||||
|
enable_auto_email=1 if args.enable_auto_email else 0,
|
||||||
|
frequency=args.frequency or "Weekly",
|
||||||
|
report=args.report or "General Ledger",
|
||||||
|
from_date=args.from_date or getdate(today()),
|
||||||
|
to_date=args.to_date or getdate(today()),
|
||||||
|
posting_date=args.posting_date or getdate(today()),
|
||||||
|
include_ageing=1,
|
||||||
|
)
|
||||||
|
process_soa.update(soa_dict)
|
||||||
|
process_soa.save()
|
||||||
|
return process_soa
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("Process Subscription", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2023-09-17 15:40:59.724177",
|
||||||
|
"default_view": "List",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"posting_date",
|
||||||
|
"subscription",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Process Subscription",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Posting Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "subscription",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Subscription",
|
||||||
|
"options": "Subscription"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-09-17 17:33:37.974166",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Process Subscription",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import getdate
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.subscription.subscription import process_all
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessSubscription(Document):
|
||||||
|
def on_submit(self):
|
||||||
|
process_all(subscription=self.subscription, posting_date=self.posting_date)
|
||||||
|
|
||||||
|
|
||||||
|
def create_subscription_process(
|
||||||
|
subscription: str | None, posting_date: Union[str, datetime.date] | None
|
||||||
|
):
|
||||||
|
"""Create a new Process Subscription document"""
|
||||||
|
doc = frappe.new_doc("Process Subscription")
|
||||||
|
doc.subscription = subscription
|
||||||
|
doc.posting_date = getdate(posting_date)
|
||||||
|
doc.insert(ignore_permissions=True)
|
||||||
|
doc.submit()
|
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestProcessSubscription(FrappeTestCase):
|
||||||
|
pass
|
@ -65,6 +65,25 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||||
|
this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."));
|
||||||
|
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||||
|
() => {
|
||||||
|
this.frm.call({
|
||||||
|
doc: this.frm.doc,
|
||||||
|
method: 'repost_accounting_entries',
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __('Reposting...'),
|
||||||
|
callback: (r) => {
|
||||||
|
if (!r.exc) {
|
||||||
|
frappe.msgprint(__('Accounting Entries are reposted.'));
|
||||||
|
me.frm.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).removeClass('btn-default').addClass('btn-warning');
|
||||||
|
}
|
||||||
|
|
||||||
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
|
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
|
||||||
if(doc.on_hold) {
|
if(doc.on_hold) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
@ -86,8 +105,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
|
||||||
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__('Payment'),
|
__('Payment'),
|
||||||
() => this.make_payment_entry(),
|
() => this.make_payment_entry(),
|
||||||
@ -162,6 +180,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
|
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
|
||||||
|
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
unblock_invoice() {
|
unblock_invoice() {
|
||||||
@ -460,6 +479,12 @@ cur_frm.set_query("expense_account", "items", function(doc) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cur_frm.set_query("wip_composite_asset", "items", function() {
|
||||||
|
return {
|
||||||
|
filters: {'is_composite_asset': 1, 'docstatus': 0 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
cur_frm.cscript.expense_account = function(doc, cdt, cdn){
|
cur_frm.cscript.expense_account = function(doc, cdt, cdn){
|
||||||
var d = locals[cdt][cdn];
|
var d = locals[cdt][cdn];
|
||||||
if(d.idx == 1 && d.expense_account){
|
if(d.idx == 1 && d.expense_account){
|
||||||
|
@ -166,6 +166,7 @@
|
|||||||
"against_expense_account",
|
"against_expense_account",
|
||||||
"column_break_63",
|
"column_break_63",
|
||||||
"unrealized_profit_loss_account",
|
"unrealized_profit_loss_account",
|
||||||
|
"repost_required",
|
||||||
"subscription_section",
|
"subscription_section",
|
||||||
"subscription",
|
"subscription",
|
||||||
"auto_repeat",
|
"auto_repeat",
|
||||||
@ -191,8 +192,7 @@
|
|||||||
"inter_company_invoice_reference",
|
"inter_company_invoice_reference",
|
||||||
"is_old_subcontracting_flow",
|
"is_old_subcontracting_flow",
|
||||||
"remarks",
|
"remarks",
|
||||||
"connections_tab",
|
"connections_tab"
|
||||||
"column_break_38"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -990,6 +990,7 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "cash_bank_account",
|
"fieldname": "cash_bank_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Cash/Bank Account",
|
"label": "Cash/Bank Account",
|
||||||
@ -1053,6 +1054,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"depends_on": "eval:flt(doc.write_off_amount)!=0",
|
"depends_on": "eval:flt(doc.write_off_amount)!=0",
|
||||||
"fieldname": "write_off_account",
|
"fieldname": "write_off_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -1217,6 +1219,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"default": "No",
|
"default": "No",
|
||||||
"fieldname": "is_opening",
|
"fieldname": "is_opening",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@ -1349,6 +1352,7 @@
|
|||||||
"options": "Project"
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"depends_on": "eval:doc.is_internal_supplier",
|
"depends_on": "eval:doc.is_internal_supplier",
|
||||||
"description": "Unrealized Profit/Loss account for intra-company transfers",
|
"description": "Unrealized Profit/Loss account for intra-company transfers",
|
||||||
"fieldname": "unrealized_profit_loss_account",
|
"fieldname": "unrealized_profit_loss_account",
|
||||||
@ -1381,6 +1385,7 @@
|
|||||||
"depends_on": "eval:doc.is_subcontracted",
|
"depends_on": "eval:doc.is_subcontracted",
|
||||||
"fieldname": "supplier_warehouse",
|
"fieldname": "supplier_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
"label": "Supplier Warehouse",
|
"label": "Supplier Warehouse",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Warehouse",
|
"options": "Warehouse",
|
||||||
@ -1504,10 +1509,6 @@
|
|||||||
"fieldname": "column_break_6",
|
"fieldname": "column_break_6",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_38",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_50",
|
"fieldname": "column_break_50",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
@ -1578,13 +1579,22 @@
|
|||||||
"fieldname": "use_company_roundoff_cost_center",
|
"fieldname": "use_company_roundoff_cost_center",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Company Default Round Off Cost Center"
|
"label": "Use Company Default Round Off Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "repost_required",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Repost Required",
|
||||||
|
"options": "Account",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-25 17:22:59.145031",
|
"modified": "2023-10-01 21:01:47.282533",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
@ -11,6 +11,9 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
|
|||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||||
|
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||||
|
validate_docs_for_deferred_accounting,
|
||||||
|
)
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||||
check_if_return_invoice_linked_with_payment_entry,
|
check_if_return_invoice_linked_with_payment_entry,
|
||||||
get_total_in_party_account_currency,
|
get_total_in_party_account_currency,
|
||||||
@ -266,9 +269,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||||
stock_items = self.get_stock_items()
|
stock_items = self.get_stock_items()
|
||||||
|
|
||||||
asset_items = [d.is_fixed_asset for d in self.items if d.is_fixed_asset]
|
asset_received_but_not_billed = None
|
||||||
if len(asset_items) > 0:
|
|
||||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
|
||||||
|
|
||||||
if self.update_stock:
|
if self.update_stock:
|
||||||
self.validate_item_code()
|
self.validate_item_code()
|
||||||
@ -362,6 +363,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
item.expense_account = asset_category_account
|
item.expense_account = asset_category_account
|
||||||
elif item.is_fixed_asset and item.pr_detail:
|
elif item.is_fixed_asset and item.pr_detail:
|
||||||
|
if not asset_received_but_not_billed:
|
||||||
|
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||||
item.expense_account = asset_received_but_not_billed
|
item.expense_account = asset_received_but_not_billed
|
||||||
elif not item.expense_account and for_validate:
|
elif not item.expense_account and for_validate:
|
||||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||||
@ -484,6 +487,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
_("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt)
|
_("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_for_repost(self):
|
||||||
|
self.validate_write_off_account()
|
||||||
|
self.validate_expense_account()
|
||||||
|
validate_docs_for_deferred_accounting([], [self.name])
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
super(PurchaseInvoice, self).on_submit()
|
super(PurchaseInvoice, self).on_submit()
|
||||||
|
|
||||||
@ -522,6 +530,18 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
self.process_common_party_accounting()
|
self.process_common_party_accounting()
|
||||||
|
|
||||||
|
def on_update_after_submit(self):
|
||||||
|
if hasattr(self, "repost_required"):
|
||||||
|
fields_to_check = [
|
||||||
|
"cash_bank_account",
|
||||||
|
"write_off_account",
|
||||||
|
"unrealized_profit_loss_account",
|
||||||
|
]
|
||||||
|
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||||
|
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||||
|
self.validate_for_repost()
|
||||||
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
if not gl_entries:
|
if not gl_entries:
|
||||||
gl_entries = self.get_gl_entries()
|
gl_entries = self.get_gl_entries()
|
||||||
@ -628,9 +648,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"credit_in_account_currency": base_grand_total
|
"credit_in_account_currency": base_grand_total
|
||||||
if self.party_account_currency == self.company_currency
|
if self.party_account_currency == self.company_currency
|
||||||
else grand_total,
|
else grand_total,
|
||||||
"against_voucher": self.return_against
|
"against_voucher": self.name,
|
||||||
if cint(self.is_return) and self.return_against
|
|
||||||
else self.name,
|
|
||||||
"against_voucher_type": self.doctype,
|
"against_voucher_type": self.doctype,
|
||||||
"project": self.project,
|
"project": self.project,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
@ -761,21 +779,22 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
# Amount added through landed-cost-voucher
|
# Amount added through landed-cost-voucher
|
||||||
if landed_cost_entries:
|
if landed_cost_entries:
|
||||||
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
|
if (item.item_code, item.name) in landed_cost_entries:
|
||||||
gl_entries.append(
|
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
|
||||||
self.get_gl_dict(
|
gl_entries.append(
|
||||||
{
|
self.get_gl_dict(
|
||||||
"account": account,
|
{
|
||||||
"against": item.expense_account,
|
"account": account,
|
||||||
"cost_center": item.cost_center,
|
"against": item.expense_account,
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
"cost_center": item.cost_center,
|
||||||
"credit": flt(amount["base_amount"]),
|
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||||
"credit_in_account_currency": flt(amount["amount"]),
|
"credit": flt(amount["base_amount"]),
|
||||||
"project": item.project or self.project,
|
"credit_in_account_currency": flt(amount["amount"]),
|
||||||
},
|
"project": item.project or self.project,
|
||||||
item=item,
|
},
|
||||||
|
item=item,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
# sub-contracting warehouse
|
# sub-contracting warehouse
|
||||||
if flt(item.rm_supp_cost):
|
if flt(item.rm_supp_cost):
|
||||||
@ -970,8 +989,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_asset_gl_entry(self, gl_entries):
|
def get_asset_gl_entry(self, gl_entries):
|
||||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
arbnb_account = None
|
||||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
eiiav_account = None
|
||||||
|
asset_eiiav_currency = None
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.is_fixed_asset:
|
if item.is_fixed_asset:
|
||||||
@ -983,6 +1003,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"Asset Received But Not Billed",
|
"Asset Received But Not Billed",
|
||||||
"Fixed Asset",
|
"Fixed Asset",
|
||||||
]:
|
]:
|
||||||
|
if not arbnb_account:
|
||||||
|
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||||
item.expense_account = arbnb_account
|
item.expense_account = arbnb_account
|
||||||
|
|
||||||
if not self.update_stock:
|
if not self.update_stock:
|
||||||
@ -1005,7 +1027,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if item.item_tax_amount:
|
if item.item_tax_amount:
|
||||||
asset_eiiav_currency = get_account_currency(eiiav_account)
|
if not eiiav_account or not asset_eiiav_currency:
|
||||||
|
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||||
|
asset_eiiav_currency = get_account_currency(eiiav_account)
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
@ -1048,7 +1073,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
||||||
asset_eiiav_currency = get_account_currency(eiiav_account)
|
if not eiiav_account or not asset_eiiav_currency:
|
||||||
|
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||||
|
asset_eiiav_currency = get_account_currency(eiiav_account)
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
@ -1068,47 +1096,46 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# When update stock is checked
|
|
||||||
# Assets are bought through this document then it will be linked to this document
|
# Assets are bought through this document then it will be linked to this document
|
||||||
if self.update_stock:
|
if flt(item.landed_cost_voucher_amount):
|
||||||
if flt(item.landed_cost_voucher_amount):
|
if not eiiav_account:
|
||||||
gl_entries.append(
|
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||||
self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"account": eiiav_account,
|
|
||||||
"against": cwip_account,
|
|
||||||
"cost_center": item.cost_center,
|
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
|
||||||
"credit": flt(item.landed_cost_voucher_amount),
|
|
||||||
"project": item.project or self.project,
|
|
||||||
},
|
|
||||||
item=item,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
"account": cwip_account,
|
"account": eiiav_account,
|
||||||
"against": eiiav_account,
|
"against": cwip_account,
|
||||||
"cost_center": item.cost_center,
|
"cost_center": item.cost_center,
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||||
"debit": flt(item.landed_cost_voucher_amount),
|
"credit": flt(item.landed_cost_voucher_amount),
|
||||||
"project": item.project or self.project,
|
"project": item.project or self.project,
|
||||||
},
|
},
|
||||||
item=item,
|
item=item,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# update gross amount of assets bought through this document
|
|
||||||
assets = frappe.db.get_all(
|
|
||||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
|
||||||
)
|
)
|
||||||
for asset in assets:
|
|
||||||
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
gl_entries.append(
|
||||||
frappe.db.set_value(
|
self.get_gl_dict(
|
||||||
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
|
{
|
||||||
|
"account": cwip_account,
|
||||||
|
"against": eiiav_account,
|
||||||
|
"cost_center": item.cost_center,
|
||||||
|
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||||
|
"debit": flt(item.landed_cost_voucher_amount),
|
||||||
|
"project": item.project or self.project,
|
||||||
|
},
|
||||||
|
item=item,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# update gross amount of assets bought through this document
|
||||||
|
assets = frappe.db.get_all(
|
||||||
|
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||||
|
)
|
||||||
|
for asset in assets:
|
||||||
|
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
||||||
|
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
|
||||||
|
|
||||||
return gl_entries
|
return gl_entries
|
||||||
|
|
||||||
@ -1644,12 +1671,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||||
self.status = "Unpaid"
|
self.status = "Unpaid"
|
||||||
# Check if outstanding amount is 0 due to debit note issued against invoice
|
# Check if outstanding amount is 0 due to debit note issued against invoice
|
||||||
elif (
|
elif self.is_return == 0 and frappe.db.get_value(
|
||||||
outstanding_amount <= 0
|
"Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||||
and self.is_return == 0
|
|
||||||
and frappe.db.get_value(
|
|
||||||
"Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
self.status = "Debit Note Issued"
|
self.status = "Debit Note Issued"
|
||||||
elif self.is_return == 1:
|
elif self.is_return == 1:
|
||||||
|
@ -1164,7 +1164,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
|
|
||||||
item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
|
item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
|
||||||
item.enable_deferred_expense = 1
|
item.enable_deferred_expense = 1
|
||||||
item.deferred_expense_account = deferred_account
|
item.item_defaults[0].deferred_expense_account = deferred_account
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
|
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
|
||||||
@ -1744,7 +1744,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
|
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
company="_Test Company",
|
company="_Test Company",
|
||||||
customer="_Test Supplier",
|
|
||||||
do_not_save=True,
|
do_not_save=True,
|
||||||
do_not_submit=True,
|
do_not_submit=True,
|
||||||
rate=1000,
|
rate=1000,
|
||||||
@ -1862,7 +1861,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
|
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
company="_Test Company",
|
company="_Test Company",
|
||||||
customer="_Test Supplier",
|
|
||||||
do_not_save=True,
|
do_not_save=True,
|
||||||
do_not_submit=True,
|
do_not_submit=True,
|
||||||
rate=1000,
|
rate=1000,
|
||||||
@ -1892,6 +1890,58 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
clear_dimension_defaults("Branch")
|
clear_dimension_defaults("Branch")
|
||||||
disable_dimension()
|
disable_dimension()
|
||||||
|
|
||||||
|
def test_repost_accounting_entries(self):
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
rate=1000,
|
||||||
|
price_list_rate=1000,
|
||||||
|
qty=1,
|
||||||
|
)
|
||||||
|
expected_gle = [
|
||||||
|
["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate()],
|
||||||
|
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||||
|
]
|
||||||
|
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||||
|
|
||||||
|
pi.items[0].expense_account = "Service - _TC"
|
||||||
|
pi.save()
|
||||||
|
pi.load_from_db()
|
||||||
|
self.assertTrue(pi.repost_required)
|
||||||
|
pi.repost_accounting_entries()
|
||||||
|
|
||||||
|
expected_gle = [
|
||||||
|
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||||
|
["Service - _TC", 1000, 0.0, nowdate()],
|
||||||
|
]
|
||||||
|
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||||
|
pi.load_from_db()
|
||||||
|
self.assertFalse(pi.repost_required)
|
||||||
|
|
||||||
|
@change_settings("Buying Settings", {"supplier_group": None})
|
||||||
|
def test_purchase_invoice_without_supplier_group(self):
|
||||||
|
# Create a Supplier
|
||||||
|
test_supplier_name = "_Test Supplier Without Supplier Group"
|
||||||
|
if not frappe.db.exists("Supplier", test_supplier_name):
|
||||||
|
supplier = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Supplier",
|
||||||
|
"supplier_name": test_supplier_name,
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
self.assertEqual(supplier.supplier_group, None)
|
||||||
|
|
||||||
|
po = create_purchase_order(
|
||||||
|
supplier=test_supplier_name,
|
||||||
|
rate=3000,
|
||||||
|
item="_Test Non Stock Item",
|
||||||
|
posting_date="2021-09-15",
|
||||||
|
)
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(supplier=test_supplier_name)
|
||||||
|
|
||||||
|
self.assertEqual(po.docstatus, 1)
|
||||||
|
self.assertEqual(pi.docstatus, 1)
|
||||||
|
|
||||||
|
|
||||||
def set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
@ -77,6 +77,7 @@
|
|||||||
"manufacturer_part_no",
|
"manufacturer_part_no",
|
||||||
"accounting",
|
"accounting",
|
||||||
"expense_account",
|
"expense_account",
|
||||||
|
"wip_composite_asset",
|
||||||
"col_break5",
|
"col_break5",
|
||||||
"is_fixed_asset",
|
"is_fixed_asset",
|
||||||
"asset_location",
|
"asset_location",
|
||||||
@ -473,6 +474,7 @@
|
|||||||
"label": "Accounting"
|
"label": "Accounting"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"fieldname": "expense_account",
|
"fieldname": "expense_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Expense Head",
|
"label": "Expense Head",
|
||||||
@ -902,12 +904,18 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Serial and Batch Bundle",
|
"options": "Serial and Batch Bundle",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "wip_composite_asset",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "WIP Composite Asset",
|
||||||
|
"options": "Asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-26 12:54:53.178156",
|
"modified": "2023-10-03 21:01:01.824892",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
|
@ -86,6 +86,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"columns": 2,
|
"columns": 2,
|
||||||
"fieldname": "account_head",
|
"fieldname": "account_head",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@ -97,6 +98,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"default": ":Company",
|
"default": ":Company",
|
||||||
"fieldname": "cost_center",
|
"fieldname": "cost_center",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-27 15:47:58.975034",
|
"modified": "2023-09-26 14:21:27.362567",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Accounting Ledger",
|
"name": "Repost Accounting Ledger",
|
||||||
@ -77,5 +77,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": [],
|
||||||
|
"track_changes": 1
|
||||||
}
|
}
|
@ -21,29 +21,8 @@ class RepostAccountingLedger(Document):
|
|||||||
|
|
||||||
def validate_for_deferred_accounting(self):
|
def validate_for_deferred_accounting(self):
|
||||||
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
|
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
|
||||||
docs_with_deferred_revenue = frappe.db.get_all(
|
|
||||||
"Sales Invoice Item",
|
|
||||||
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
|
|
||||||
fields=["parent"],
|
|
||||||
as_list=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
|
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
|
||||||
docs_with_deferred_expense = frappe.db.get_all(
|
validate_docs_for_deferred_accounting(sales_docs, purchase_docs)
|
||||||
"Purchase Invoice Item",
|
|
||||||
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
|
|
||||||
fields=["parent"],
|
|
||||||
as_list=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
if docs_with_deferred_revenue or docs_with_deferred_expense:
|
|
||||||
frappe.throw(
|
|
||||||
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
|
|
||||||
frappe.bold(
|
|
||||||
comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_for_closed_fiscal_year(self):
|
def validate_for_closed_fiscal_year(self):
|
||||||
if self.vouchers:
|
if self.vouchers:
|
||||||
@ -139,14 +118,17 @@ class RepostAccountingLedger(Document):
|
|||||||
return rendered_page
|
return rendered_page
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
job_name = "repost_accounting_ledger_" + self.name
|
if len(self.vouchers) > 1:
|
||||||
frappe.enqueue(
|
job_name = "repost_accounting_ledger_" + self.name
|
||||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
frappe.enqueue(
|
||||||
account_repost_doc=self.name,
|
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||||
is_async=True,
|
account_repost_doc=self.name,
|
||||||
job_name=job_name,
|
is_async=True,
|
||||||
)
|
job_name=job_name,
|
||||||
frappe.msgprint(_("Repost has started in the background"))
|
)
|
||||||
|
frappe.msgprint(_("Repost has started in the background"))
|
||||||
|
else:
|
||||||
|
start_repost(self.name)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -181,3 +163,26 @@ def start_repost(account_repost_doc=str) -> None:
|
|||||||
doc.make_gl_entries()
|
doc.make_gl_entries()
|
||||||
|
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||||
|
docs_with_deferred_revenue = frappe.db.get_all(
|
||||||
|
"Sales Invoice Item",
|
||||||
|
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
|
||||||
|
fields=["parent"],
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
docs_with_deferred_expense = frappe.db.get_all(
|
||||||
|
"Purchase Invoice Item",
|
||||||
|
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
|
||||||
|
fields=["parent"],
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
if docs_with_deferred_revenue or docs_with_deferred_expense:
|
||||||
|
frappe.throw(
|
||||||
|
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
|
||||||
|
frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@ -99,7 +99,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-11-08 07:38:40.079038",
|
"modified": "2023-09-26 14:21:35.719727",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Repost Payment Ledger",
|
"name": "Repost Payment Ledger",
|
||||||
@ -155,5 +155,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": [],
|
||||||
|
"track_changes": 1
|
||||||
}
|
}
|
@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
super.onload();
|
super.onload();
|
||||||
|
|
||||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
|
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
|
||||||
|
|
||||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||||
// show debit_to in print format
|
// show debit_to in print format
|
||||||
@ -98,8 +98,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
if (doc.docstatus == 1 && doc.outstanding_amount!=0) {
|
||||||
&& !(cint(doc.is_return) && doc.return_against)) {
|
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__('Payment'),
|
__('Payment'),
|
||||||
() => this.make_payment_entry(),
|
() => this.make_payment_entry(),
|
||||||
@ -184,8 +183,11 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
}, __('Create'));
|
}, __('Create'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
make_maintenance_schedule() {
|
make_maintenance_schedule() {
|
||||||
frappe.model.open_mapped_doc({
|
frappe.model.open_mapped_doc({
|
||||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
||||||
|
@ -11,13 +11,13 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form
|
|||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
|
||||||
get_accounting_dimensions,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||||
get_loyalty_program_details_with_points,
|
get_loyalty_program_details_with_points,
|
||||||
validate_loyalty_points,
|
validate_loyalty_points,
|
||||||
)
|
)
|
||||||
|
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||||
|
validate_docs_for_deferred_accounting,
|
||||||
|
)
|
||||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||||
get_party_tax_withholding_details,
|
get_party_tax_withholding_details,
|
||||||
)
|
)
|
||||||
@ -168,6 +168,12 @@ class SalesInvoice(SellingController):
|
|||||||
self.validate_account_for_change_amount()
|
self.validate_account_for_change_amount()
|
||||||
self.validate_income_account()
|
self.validate_income_account()
|
||||||
|
|
||||||
|
def validate_for_repost(self):
|
||||||
|
self.validate_write_off_account()
|
||||||
|
self.validate_account_for_change_amount()
|
||||||
|
self.validate_income_account()
|
||||||
|
validate_docs_for_deferred_accounting([self.name], [])
|
||||||
|
|
||||||
def validate_fixed_asset(self):
|
def validate_fixed_asset(self):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
|
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
|
||||||
@ -388,6 +394,8 @@ class SalesInvoice(SellingController):
|
|||||||
"Repost Payment Ledger Items",
|
"Repost Payment Ledger Items",
|
||||||
"Repost Accounting Ledger",
|
"Repost Accounting Ledger",
|
||||||
"Repost Accounting Ledger Items",
|
"Repost Accounting Ledger Items",
|
||||||
|
"Unreconcile Payments",
|
||||||
|
"Unreconcile Payment Entries",
|
||||||
"Payment Ledger Entry",
|
"Payment Ledger Entry",
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
)
|
)
|
||||||
@ -515,90 +523,21 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
if hasattr(self, "repost_required"):
|
if hasattr(self, "repost_required"):
|
||||||
needs_repost = 0
|
fields_to_check = [
|
||||||
|
"additional_discount_account",
|
||||||
# Check if any field affecting accounting entry is altered
|
"cash_bank_account",
|
||||||
doc_before_update = self.get_doc_before_save()
|
"account_for_change_amount",
|
||||||
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
|
"write_off_account",
|
||||||
|
"loyalty_redemption_account",
|
||||||
# Check if opening entry check updated
|
"unrealized_profit_loss_account",
|
||||||
if doc_before_update.get("is_opening") != self.is_opening:
|
]
|
||||||
needs_repost = 1
|
child_tables = {
|
||||||
|
"items": ("income_account", "expense_account", "discount_account"),
|
||||||
if not needs_repost:
|
"taxes": ("account_head",),
|
||||||
# Parent Level Accounts excluding party account
|
}
|
||||||
for field in (
|
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||||
"additional_discount_account",
|
self.validate_for_repost()
|
||||||
"cash_bank_account",
|
self.db_set("repost_required", self.needs_repost)
|
||||||
"account_for_change_amount",
|
|
||||||
"write_off_account",
|
|
||||||
"loyalty_redemption_account",
|
|
||||||
"unrealized_profit_loss_account",
|
|
||||||
):
|
|
||||||
if doc_before_update.get(field) != self.get(field):
|
|
||||||
needs_repost = 1
|
|
||||||
break
|
|
||||||
|
|
||||||
# Check for parent accounting dimensions
|
|
||||||
for dimension in accounting_dimensions:
|
|
||||||
if doc_before_update.get(dimension) != self.get(dimension):
|
|
||||||
needs_repost = 1
|
|
||||||
break
|
|
||||||
|
|
||||||
# Check for child tables
|
|
||||||
if self.check_if_child_table_updated(
|
|
||||||
"items",
|
|
||||||
doc_before_update,
|
|
||||||
("income_account", "expense_account", "discount_account"),
|
|
||||||
accounting_dimensions,
|
|
||||||
):
|
|
||||||
needs_repost = 1
|
|
||||||
|
|
||||||
if self.check_if_child_table_updated(
|
|
||||||
"taxes", doc_before_update, ("account_head",), accounting_dimensions
|
|
||||||
):
|
|
||||||
needs_repost = 1
|
|
||||||
|
|
||||||
self.validate_accounts()
|
|
||||||
|
|
||||||
# validate if deferred revenue is enabled for any item
|
|
||||||
# Don't allow to update the invoice if deferred revenue is enabled
|
|
||||||
if needs_repost:
|
|
||||||
for item in self.get("items"):
|
|
||||||
if item.enable_deferred_revenue:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission."
|
|
||||||
).format(item.item_code)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.db_set("repost_required", needs_repost)
|
|
||||||
|
|
||||||
def check_if_child_table_updated(
|
|
||||||
self, child_table, doc_before_update, fields_to_check, accounting_dimensions
|
|
||||||
):
|
|
||||||
# Check if any field affecting accounting entry is altered
|
|
||||||
for index, item in enumerate(self.get(child_table)):
|
|
||||||
for field in fields_to_check:
|
|
||||||
if doc_before_update.get(child_table)[index].get(field) != item.get(field):
|
|
||||||
return True
|
|
||||||
|
|
||||||
for dimension in accounting_dimensions:
|
|
||||||
if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def repost_accounting_entries(self):
|
|
||||||
if self.repost_required:
|
|
||||||
self.docstatus = 2
|
|
||||||
self.make_gl_entries_on_cancel()
|
|
||||||
self.docstatus = 1
|
|
||||||
self.make_gl_entries()
|
|
||||||
self.db_set("repost_required", 0)
|
|
||||||
else:
|
|
||||||
frappe.throw(_("No updates pending for reposting"))
|
|
||||||
|
|
||||||
def set_paid_amount(self):
|
def set_paid_amount(self):
|
||||||
paid_amount = 0.0
|
paid_amount = 0.0
|
||||||
@ -1104,9 +1043,7 @@ class SalesInvoice(SellingController):
|
|||||||
"debit_in_account_currency": base_grand_total
|
"debit_in_account_currency": base_grand_total
|
||||||
if self.party_account_currency == self.company_currency
|
if self.party_account_currency == self.company_currency
|
||||||
else grand_total,
|
else grand_total,
|
||||||
"against_voucher": self.return_against
|
"against_voucher": self.name,
|
||||||
if cint(self.is_return) and self.return_against
|
|
||||||
else self.name,
|
|
||||||
"against_voucher_type": self.doctype,
|
"against_voucher_type": self.doctype,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"project": self.project,
|
"project": self.project,
|
||||||
@ -1732,12 +1669,8 @@ class SalesInvoice(SellingController):
|
|||||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||||
self.status = "Unpaid"
|
self.status = "Unpaid"
|
||||||
# Check if outstanding amount is 0 due to credit note issued against invoice
|
# Check if outstanding amount is 0 due to credit note issued against invoice
|
||||||
elif (
|
elif self.is_return == 0 and frappe.db.get_value(
|
||||||
outstanding_amount <= 0
|
"Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||||
and self.is_return == 0
|
|
||||||
and frappe.db.get_value(
|
|
||||||
"Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
self.status = "Credit Note Issued"
|
self.status = "Credit Note Issued"
|
||||||
elif self.is_return == 1:
|
elif self.is_return == 1:
|
||||||
|
@ -15,9 +15,11 @@ def get_data():
|
|||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Sales Order": ["items", "sales_order"],
|
"Sales Order": ["items", "sales_order"],
|
||||||
"Delivery Note": ["items", "delivery_note"],
|
|
||||||
"Timesheet": ["timesheets", "time_sheet"],
|
"Timesheet": ["timesheets", "time_sheet"],
|
||||||
},
|
},
|
||||||
|
"internal_and_external_links": {
|
||||||
|
"Delivery Note": ["items", "delivery_note"],
|
||||||
|
},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{
|
{
|
||||||
"label": _("Payment"),
|
"label": _("Payment"),
|
||||||
|
@ -26,6 +26,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
|||||||
from erpnext.controllers.accounts_controller import update_invoice_status
|
from erpnext.controllers.accounts_controller import update_invoice_status
|
||||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||||
|
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
|
||||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
@ -1500,8 +1501,8 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(party_credited, 1000)
|
self.assertEqual(party_credited, 1000)
|
||||||
|
|
||||||
# Check outstanding amount
|
# Check outstanding amount
|
||||||
self.assertFalse(si1.outstanding_amount)
|
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
|
||||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
|
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
|
||||||
|
|
||||||
def test_gle_made_when_asset_is_returned(self):
|
def test_gle_made_when_asset_is_returned(self):
|
||||||
create_asset_data()
|
create_asset_data()
|
||||||
@ -1801,6 +1802,10 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
|
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
|
||||||
|
"""Test impact of advance PE submission/cancellation on SI and SO."""
|
||||||
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
|
||||||
|
sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
|
||||||
pe = frappe.get_doc(
|
pe = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Payment Entry",
|
"doctype": "Payment Entry",
|
||||||
@ -1820,10 +1825,25 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
"paid_to": "_Test Cash - _TC",
|
"paid_to": "_Test Cash - _TC",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{
|
||||||
|
"reference_doctype": "Sales Order",
|
||||||
|
"reference_name": sales_order.name,
|
||||||
|
"total_amount": sales_order.grand_total,
|
||||||
|
"outstanding_amount": sales_order.grand_total,
|
||||||
|
"allocated_amount": 300,
|
||||||
|
},
|
||||||
|
)
|
||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
pe.submit()
|
||||||
|
|
||||||
|
sales_order.reload()
|
||||||
|
self.assertEqual(sales_order.advance_paid, 300)
|
||||||
|
|
||||||
si = frappe.copy_doc(test_records[0])
|
si = frappe.copy_doc(test_records[0])
|
||||||
|
si.items[0].sales_order = sales_order.name
|
||||||
|
si.items[0].so_detail = sales_order.get("items")[0].name
|
||||||
si.is_pos = 0
|
si.is_pos = 0
|
||||||
si.append(
|
si.append(
|
||||||
"advances",
|
"advances",
|
||||||
@ -1831,6 +1851,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
"doctype": "Sales Invoice Advance",
|
"doctype": "Sales Invoice Advance",
|
||||||
"reference_type": "Payment Entry",
|
"reference_type": "Payment Entry",
|
||||||
"reference_name": pe.name,
|
"reference_name": pe.name,
|
||||||
|
"reference_row": pe.references[0].name,
|
||||||
"advance_amount": 300,
|
"advance_amount": 300,
|
||||||
"allocated_amount": 300,
|
"allocated_amount": 300,
|
||||||
"remarks": pe.remarks,
|
"remarks": pe.remarks,
|
||||||
@ -1839,7 +1860,13 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.insert()
|
si.insert()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
si.load_from_db()
|
si.reload()
|
||||||
|
pe.reload()
|
||||||
|
sales_order.reload()
|
||||||
|
|
||||||
|
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
|
||||||
|
self.assertEqual(pe.references[0].reference_name, si.name)
|
||||||
|
self.assertEqual(sales_order.advance_paid, 0.0)
|
||||||
|
|
||||||
# check outstanding after advance allocation
|
# check outstanding after advance allocation
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -1847,11 +1874,9 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
|
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
|
||||||
)
|
)
|
||||||
|
|
||||||
# added to avoid Document has been modified exception
|
|
||||||
pe = frappe.get_doc("Payment Entry", pe.name)
|
|
||||||
pe.cancel()
|
pe.cancel()
|
||||||
|
si.reload()
|
||||||
|
|
||||||
si.load_from_db()
|
|
||||||
# check outstanding after advance cancellation
|
# check outstanding after advance cancellation
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
flt(si.outstanding_amount),
|
flt(si.outstanding_amount),
|
||||||
@ -2322,7 +2347,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
item = create_item("_Test Item for Deferred Accounting")
|
item = create_item("_Test Item for Deferred Accounting")
|
||||||
item.enable_deferred_revenue = 1
|
item.enable_deferred_revenue = 1
|
||||||
item.deferred_revenue_account = deferred_account
|
item.item_defaults[0].deferred_revenue_account = deferred_account
|
||||||
item.no_of_months = 12
|
item.no_of_months = 12
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
@ -3102,7 +3127,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
item = create_item("_Test Item for Deferred Accounting")
|
item = create_item("_Test Item for Deferred Accounting")
|
||||||
item.enable_deferred_expense = 1
|
item.enable_deferred_expense = 1
|
||||||
item.deferred_revenue_account = deferred_account
|
item.item_defaults[0].deferred_revenue_account = deferred_account
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
@ -3376,6 +3401,24 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
||||||
|
|
||||||
|
@change_settings("Selling Settings", {"customer_group": None, "territory": None})
|
||||||
|
def test_sales_invoice_without_customer_group_and_territory(self):
|
||||||
|
# create a customer
|
||||||
|
if not frappe.db.exists("Customer", "_Test Simple Customer"):
|
||||||
|
customer_dict = get_customer_dict("_Test Simple Customer")
|
||||||
|
customer_dict.pop("customer_group")
|
||||||
|
customer_dict.pop("territory")
|
||||||
|
customer = frappe.get_doc(customer_dict).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
self.assertEqual(customer.customer_group, None)
|
||||||
|
self.assertEqual(customer.territory, None)
|
||||||
|
|
||||||
|
# create a sales invoice
|
||||||
|
si = create_sales_invoice(customer="_Test Simple Customer")
|
||||||
|
self.assertEqual(si.docstatus, 1)
|
||||||
|
self.assertEqual(si.customer_group, None)
|
||||||
|
self.assertEqual(si.territory, None)
|
||||||
|
|
||||||
@change_settings("Selling Settings", {"allow_negative_rates_for_items": 0})
|
@change_settings("Selling Settings", {"allow_negative_rates_for_items": 0})
|
||||||
def test_sales_return_negative_rate(self):
|
def test_sales_return_negative_rate(self):
|
||||||
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
|
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
|
||||||
|
@ -157,7 +157,6 @@
|
|||||||
"oldfieldname": "description",
|
"oldfieldname": "description",
|
||||||
"oldfieldtype": "Text",
|
"oldfieldtype": "Text",
|
||||||
"print_width": "200px",
|
"print_width": "200px",
|
||||||
"reqd": 1,
|
|
||||||
"width": "200px"
|
"width": "200px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -24,8 +24,9 @@
|
|||||||
"current_invoice_start",
|
"current_invoice_start",
|
||||||
"current_invoice_end",
|
"current_invoice_end",
|
||||||
"days_until_due",
|
"days_until_due",
|
||||||
|
"generate_invoice_at",
|
||||||
|
"number_of_days",
|
||||||
"cancel_at_period_end",
|
"cancel_at_period_end",
|
||||||
"generate_invoice_at_period_start",
|
|
||||||
"sb_4",
|
"sb_4",
|
||||||
"plans",
|
"plans",
|
||||||
"sb_1",
|
"sb_1",
|
||||||
@ -86,12 +87,14 @@
|
|||||||
"fieldname": "current_invoice_start",
|
"fieldname": "current_invoice_start",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Current Invoice Start Date",
|
"label": "Current Invoice Start Date",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "current_invoice_end",
|
"fieldname": "current_invoice_end",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Current Invoice End Date",
|
"label": "Current Invoice End Date",
|
||||||
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -107,12 +110,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Cancel At End Of Period"
|
"label": "Cancel At End Of Period"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "generate_invoice_at_period_start",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Generate Invoice At Beginning Of Period"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "sb_4",
|
"fieldname": "sb_4",
|
||||||
@ -240,6 +237,21 @@
|
|||||||
"fieldname": "submit_invoice",
|
"fieldname": "submit_invoice",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Submit Generated Invoices"
|
"label": "Submit Generated Invoices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "End of the current subscription period",
|
||||||
|
"fieldname": "generate_invoice_at",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Generate Invoice At",
|
||||||
|
"options": "End of the current subscription period\nBeginning of the current subscription period\nDays before the current subscription period",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.generate_invoice_at === \"Days before the current subscription period\"",
|
||||||
|
"fieldname": "number_of_days",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Number of Days",
|
||||||
|
"mandatory_depends_on": "eval:doc.generate_invoice_at === \"Days before the current subscription period\""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@ -255,7 +267,7 @@
|
|||||||
"link_fieldname": "subscription"
|
"link_fieldname": "subscription"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2022-02-18 23:24:57.185054",
|
"modified": "2023-09-18 17:48:21.900252",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Subscription",
|
"name": "Subscription",
|
||||||
|
@ -36,12 +36,15 @@ class InvoiceNotCancelled(frappe.ValidationError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
DateTimeLikeObject = Union[str, datetime.date]
|
||||||
|
|
||||||
|
|
||||||
class Subscription(Document):
|
class Subscription(Document):
|
||||||
def before_insert(self):
|
def before_insert(self):
|
||||||
# update start just before the subscription doc is created
|
# update start just before the subscription doc is created
|
||||||
self.update_subscription_period(self.start_date)
|
self.update_subscription_period(self.start_date)
|
||||||
|
|
||||||
def update_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
|
def update_subscription_period(self, date: Optional["DateTimeLikeObject"] = None):
|
||||||
"""
|
"""
|
||||||
Subscription period is the period to be billed. This method updates the
|
Subscription period is the period to be billed. This method updates the
|
||||||
beginning of the billing period and end of the billing period.
|
beginning of the billing period and end of the billing period.
|
||||||
@ -52,14 +55,14 @@ class Subscription(Document):
|
|||||||
self.current_invoice_start = self.get_current_invoice_start(date)
|
self.current_invoice_start = self.get_current_invoice_start(date)
|
||||||
self.current_invoice_end = self.get_current_invoice_end(self.current_invoice_start)
|
self.current_invoice_end = self.get_current_invoice_end(self.current_invoice_start)
|
||||||
|
|
||||||
def _get_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
|
def _get_subscription_period(self, date: Optional["DateTimeLikeObject"] = None):
|
||||||
_current_invoice_start = self.get_current_invoice_start(date)
|
_current_invoice_start = self.get_current_invoice_start(date)
|
||||||
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
|
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
|
||||||
|
|
||||||
return _current_invoice_start, _current_invoice_end
|
return _current_invoice_start, _current_invoice_end
|
||||||
|
|
||||||
def get_current_invoice_start(
|
def get_current_invoice_start(
|
||||||
self, date: Optional[Union[datetime.date, str]] = None
|
self, date: Optional["DateTimeLikeObject"] = None
|
||||||
) -> Union[datetime.date, str]:
|
) -> Union[datetime.date, str]:
|
||||||
"""
|
"""
|
||||||
This returns the date of the beginning of the current billing period.
|
This returns the date of the beginning of the current billing period.
|
||||||
@ -84,7 +87,7 @@ class Subscription(Document):
|
|||||||
return _current_invoice_start
|
return _current_invoice_start
|
||||||
|
|
||||||
def get_current_invoice_end(
|
def get_current_invoice_end(
|
||||||
self, date: Optional[Union[datetime.date, str]] = None
|
self, date: Optional["DateTimeLikeObject"] = None
|
||||||
) -> Union[datetime.date, str]:
|
) -> Union[datetime.date, str]:
|
||||||
"""
|
"""
|
||||||
This returns the date of the end of the current billing period.
|
This returns the date of the end of the current billing period.
|
||||||
@ -179,30 +182,24 @@ class Subscription(Document):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def set_subscription_status(self) -> None:
|
def set_subscription_status(self, posting_date: Optional["DateTimeLikeObject"] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the status of the `Subscription`
|
Sets the status of the `Subscription`
|
||||||
"""
|
"""
|
||||||
if self.is_trialling():
|
if self.is_trialling():
|
||||||
self.status = "Trialling"
|
self.status = "Trialling"
|
||||||
elif (
|
elif (
|
||||||
self.status == "Active"
|
self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date)
|
||||||
and self.end_date
|
|
||||||
and getdate(frappe.flags.current_date) > getdate(self.end_date)
|
|
||||||
):
|
):
|
||||||
self.status = "Completed"
|
self.status = "Completed"
|
||||||
elif self.is_past_grace_period():
|
elif self.is_past_grace_period():
|
||||||
self.status = self.get_status_for_past_grace_period()
|
self.status = self.get_status_for_past_grace_period()
|
||||||
self.cancelation_date = (
|
self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
|
||||||
getdate(frappe.flags.current_date) if self.status == "Cancelled" else None
|
|
||||||
)
|
|
||||||
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
|
||||||
self.status = "Past Due Date"
|
self.status = "Past Due Date"
|
||||||
elif not self.has_outstanding_invoice() or self.is_new_subscription():
|
elif not self.has_outstanding_invoice() or self.is_new_subscription():
|
||||||
self.status = "Active"
|
self.status = "Active"
|
||||||
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
def is_trialling(self) -> bool:
|
def is_trialling(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns `True` if the `Subscription` is in trial period.
|
Returns `True` if the `Subscription` is in trial period.
|
||||||
@ -210,7 +207,9 @@ class Subscription(Document):
|
|||||||
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def period_has_passed(end_date: Union[str, datetime.date]) -> bool:
|
def period_has_passed(
|
||||||
|
end_date: Union[str, datetime.date], posting_date: Optional["DateTimeLikeObject"] = None
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns true if the given `end_date` has passed
|
Returns true if the given `end_date` has passed
|
||||||
"""
|
"""
|
||||||
@ -218,7 +217,7 @@ class Subscription(Document):
|
|||||||
if not end_date:
|
if not end_date:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return getdate(frappe.flags.current_date) > getdate(end_date)
|
return getdate(posting_date) > getdate(end_date)
|
||||||
|
|
||||||
def get_status_for_past_grace_period(self) -> str:
|
def get_status_for_past_grace_period(self) -> str:
|
||||||
cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace"))
|
cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace"))
|
||||||
@ -229,7 +228,7 @@ class Subscription(Document):
|
|||||||
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def is_past_grace_period(self) -> bool:
|
def is_past_grace_period(self, posting_date: Optional["DateTimeLikeObject"] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns `True` if the grace period for the `Subscription` has passed
|
Returns `True` if the grace period for the `Subscription` has passed
|
||||||
"""
|
"""
|
||||||
@ -237,18 +236,18 @@ class Subscription(Document):
|
|||||||
return
|
return
|
||||||
|
|
||||||
grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period"))
|
grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period"))
|
||||||
return getdate(frappe.flags.current_date) >= getdate(
|
return getdate(posting_date) >= getdate(add_days(self.current_invoice.due_date, grace_period))
|
||||||
add_days(self.current_invoice.due_date, grace_period)
|
|
||||||
)
|
|
||||||
|
|
||||||
def current_invoice_is_past_due(self) -> bool:
|
def current_invoice_is_past_due(
|
||||||
|
self, posting_date: Optional["DateTimeLikeObject"] = None
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Returns `True` if the current generated invoice is overdue
|
Returns `True` if the current generated invoice is overdue
|
||||||
"""
|
"""
|
||||||
if not self.current_invoice or self.is_paid(self.current_invoice):
|
if not self.current_invoice or self.is_paid(self.current_invoice):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return getdate(frappe.flags.current_date) >= getdate(self.current_invoice.due_date)
|
return getdate(posting_date) >= getdate(self.current_invoice.due_date)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def invoice_document_type(self) -> str:
|
def invoice_document_type(self) -> str:
|
||||||
@ -270,6 +269,9 @@ class Subscription(Document):
|
|||||||
if not self.cost_center:
|
if not self.cost_center:
|
||||||
self.cost_center = get_default_cost_center(self.get("company"))
|
self.cost_center = get_default_cost_center(self.get("company"))
|
||||||
|
|
||||||
|
if self.is_new():
|
||||||
|
self.set_subscription_status()
|
||||||
|
|
||||||
def validate_trial_period(self) -> None:
|
def validate_trial_period(self) -> None:
|
||||||
"""
|
"""
|
||||||
Runs sanity checks on trial period dates for the `Subscription`
|
Runs sanity checks on trial period dates for the `Subscription`
|
||||||
@ -305,10 +307,6 @@ class Subscription(Document):
|
|||||||
if billing_info[0]["billing_interval"] != "Month":
|
if billing_info[0]["billing_interval"] != "Month":
|
||||||
frappe.throw(_("Billing Interval in Subscription Plan must be Month to follow calendar months"))
|
frappe.throw(_("Billing Interval in Subscription Plan must be Month to follow calendar months"))
|
||||||
|
|
||||||
def after_insert(self) -> None:
|
|
||||||
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
|
|
||||||
self.set_subscription_status()
|
|
||||||
|
|
||||||
def generate_invoice(
|
def generate_invoice(
|
||||||
self,
|
self,
|
||||||
from_date: Optional[Union[str, datetime.date]] = None,
|
from_date: Optional[Union[str, datetime.date]] = None,
|
||||||
@ -344,7 +342,7 @@ class Subscription(Document):
|
|||||||
invoice.set_posting_time = 1
|
invoice.set_posting_time = 1
|
||||||
invoice.posting_date = (
|
invoice.posting_date = (
|
||||||
self.current_invoice_start
|
self.current_invoice_start
|
||||||
if self.generate_invoice_at_period_start
|
if self.generate_invoice_at == "Beginning of the current subscription period"
|
||||||
else self.current_invoice_end
|
else self.current_invoice_end
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -438,7 +436,7 @@ class Subscription(Document):
|
|||||||
prorate_factor = get_prorata_factor(
|
prorate_factor = get_prorata_factor(
|
||||||
self.current_invoice_end,
|
self.current_invoice_end,
|
||||||
self.current_invoice_start,
|
self.current_invoice_start,
|
||||||
cint(self.generate_invoice_at_period_start),
|
cint(self.generate_invoice_at == "Beginning of the current subscription period"),
|
||||||
)
|
)
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
@ -503,42 +501,45 @@ class Subscription(Document):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def process(self) -> bool:
|
def process(self, posting_date: Optional["DateTimeLikeObject"] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
To be called by task periodically. It checks the subscription and takes appropriate action
|
To be called by task periodically. It checks the subscription and takes appropriate action
|
||||||
as need be. It calls either of these methods depending the `Subscription` status:
|
as need be. It calls either of these methods depending the `Subscription` status:
|
||||||
1. `process_for_active`
|
1. `process_for_active`
|
||||||
2. `process_for_past_due`
|
2. `process_for_past_due`
|
||||||
"""
|
"""
|
||||||
if (
|
if not self.is_current_invoice_generated(
|
||||||
not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
|
self.current_invoice_start, self.current_invoice_end
|
||||||
and self.can_generate_new_invoice()
|
) and self.can_generate_new_invoice(posting_date):
|
||||||
):
|
|
||||||
self.generate_invoice()
|
self.generate_invoice()
|
||||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||||
|
|
||||||
if self.cancel_at_period_end and (
|
if self.cancel_at_period_end and (
|
||||||
getdate(frappe.flags.current_date) >= getdate(self.current_invoice_end)
|
getdate(posting_date) >= getdate(self.current_invoice_end)
|
||||||
or getdate(frappe.flags.current_date) >= getdate(self.end_date)
|
or getdate(posting_date) >= getdate(self.end_date)
|
||||||
):
|
):
|
||||||
self.cancel_subscription()
|
self.cancel_subscription()
|
||||||
|
|
||||||
self.set_subscription_status()
|
self.set_subscription_status(posting_date=posting_date)
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def can_generate_new_invoice(self) -> bool:
|
def can_generate_new_invoice(self, posting_date: Optional["DateTimeLikeObject"] = None) -> bool:
|
||||||
if self.cancelation_date:
|
if self.cancelation_date:
|
||||||
return False
|
return False
|
||||||
elif self.generate_invoice_at_period_start and (
|
|
||||||
getdate(frappe.flags.current_date) == getdate(self.current_invoice_start)
|
if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
|
||||||
or self.is_new_subscription()
|
return False
|
||||||
|
|
||||||
|
if self.generate_invoice_at == "Beginning of the current subscription period" and (
|
||||||
|
getdate(posting_date) == getdate(self.current_invoice_start) or self.is_new_subscription()
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
elif getdate(frappe.flags.current_date) == getdate(self.current_invoice_end):
|
elif self.generate_invoice_at == "Days before the current subscription period" and (
|
||||||
if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
|
getdate(posting_date) == getdate(add_days(self.current_invoice_start, -1 * self.number_of_days))
|
||||||
return False
|
):
|
||||||
|
return True
|
||||||
|
elif getdate(posting_date) == getdate(self.current_invoice_end):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@ -628,7 +629,10 @@ class Subscription(Document):
|
|||||||
frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
|
frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
|
||||||
|
|
||||||
to_generate_invoice = (
|
to_generate_invoice = (
|
||||||
True if self.status == "Active" and not self.generate_invoice_at_period_start else False
|
True
|
||||||
|
if self.status == "Active"
|
||||||
|
and not self.generate_invoice_at == "Beginning of the current subscription period"
|
||||||
|
else False
|
||||||
)
|
)
|
||||||
self.status = "Cancelled"
|
self.status = "Cancelled"
|
||||||
self.cancelation_date = nowdate()
|
self.cancelation_date = nowdate()
|
||||||
@ -639,7 +643,7 @@ class Subscription(Document):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def restart_subscription(self) -> None:
|
def restart_subscription(self, posting_date: Optional["DateTimeLikeObject"] = None) -> None:
|
||||||
"""
|
"""
|
||||||
This sets the subscription as active. The subscription will be made to be like a new
|
This sets the subscription as active. The subscription will be made to be like a new
|
||||||
subscription and the `Subscription` will lose all the history of generated invoices
|
subscription and the `Subscription` will lose all the history of generated invoices
|
||||||
@ -650,7 +654,7 @@ class Subscription(Document):
|
|||||||
|
|
||||||
self.status = "Active"
|
self.status = "Active"
|
||||||
self.cancelation_date = None
|
self.cancelation_date = None
|
||||||
self.update_subscription_period(frappe.flags.current_date or nowdate())
|
self.update_subscription_period(posting_date or nowdate())
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
@ -671,14 +675,21 @@ def get_prorata_factor(
|
|||||||
return diff / plan_days
|
return diff / plan_days
|
||||||
|
|
||||||
|
|
||||||
def process_all() -> None:
|
def process_all(
|
||||||
|
subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||||
"""
|
"""
|
||||||
for subscription in frappe.get_all("Subscription", {"status": ("!=", "Cancelled")}, pluck="name"):
|
filters = {"status": ("!=", "Cancelled")}
|
||||||
|
|
||||||
|
if subscription:
|
||||||
|
filters["name"] = subscription
|
||||||
|
|
||||||
|
for subscription in frappe.get_all("Subscription", filters, pluck="name"):
|
||||||
try:
|
try:
|
||||||
subscription = frappe.get_doc("Subscription", subscription)
|
subscription = frappe.get_doc("Subscription", subscription)
|
||||||
subscription.process()
|
subscription.process(posting_date)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
except frappe.ValidationError:
|
except frappe.ValidationError:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
@ -8,6 +8,7 @@ from frappe.utils.data import (
|
|||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
add_to_date,
|
add_to_date,
|
||||||
|
cint,
|
||||||
date_diff,
|
date_diff,
|
||||||
flt,
|
flt,
|
||||||
get_date_str,
|
get_date_str,
|
||||||
@ -20,99 +21,16 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto
|
|||||||
test_dependencies = ("UOM", "Item Group", "Item")
|
test_dependencies = ("UOM", "Item Group", "Item")
|
||||||
|
|
||||||
|
|
||||||
def create_plan():
|
|
||||||
if not frappe.db.exists("Subscription Plan", "_Test Plan Name"):
|
|
||||||
plan = frappe.new_doc("Subscription Plan")
|
|
||||||
plan.plan_name = "_Test Plan Name"
|
|
||||||
plan.item = "_Test Non Stock Item"
|
|
||||||
plan.price_determination = "Fixed Rate"
|
|
||||||
plan.cost = 900
|
|
||||||
plan.billing_interval = "Month"
|
|
||||||
plan.billing_interval_count = 1
|
|
||||||
plan.insert()
|
|
||||||
|
|
||||||
if not frappe.db.exists("Subscription Plan", "_Test Plan Name 2"):
|
|
||||||
plan = frappe.new_doc("Subscription Plan")
|
|
||||||
plan.plan_name = "_Test Plan Name 2"
|
|
||||||
plan.item = "_Test Non Stock Item"
|
|
||||||
plan.price_determination = "Fixed Rate"
|
|
||||||
plan.cost = 1999
|
|
||||||
plan.billing_interval = "Month"
|
|
||||||
plan.billing_interval_count = 1
|
|
||||||
plan.insert()
|
|
||||||
|
|
||||||
if not frappe.db.exists("Subscription Plan", "_Test Plan Name 3"):
|
|
||||||
plan = frappe.new_doc("Subscription Plan")
|
|
||||||
plan.plan_name = "_Test Plan Name 3"
|
|
||||||
plan.item = "_Test Non Stock Item"
|
|
||||||
plan.price_determination = "Fixed Rate"
|
|
||||||
plan.cost = 1999
|
|
||||||
plan.billing_interval = "Day"
|
|
||||||
plan.billing_interval_count = 14
|
|
||||||
plan.insert()
|
|
||||||
|
|
||||||
# Defined a quarterly Subscription Plan
|
|
||||||
if not frappe.db.exists("Subscription Plan", "_Test Plan Name 4"):
|
|
||||||
plan = frappe.new_doc("Subscription Plan")
|
|
||||||
plan.plan_name = "_Test Plan Name 4"
|
|
||||||
plan.item = "_Test Non Stock Item"
|
|
||||||
plan.price_determination = "Monthly Rate"
|
|
||||||
plan.cost = 20000
|
|
||||||
plan.billing_interval = "Month"
|
|
||||||
plan.billing_interval_count = 3
|
|
||||||
plan.insert()
|
|
||||||
|
|
||||||
if not frappe.db.exists("Subscription Plan", "_Test Plan Multicurrency"):
|
|
||||||
plan = frappe.new_doc("Subscription Plan")
|
|
||||||
plan.plan_name = "_Test Plan Multicurrency"
|
|
||||||
plan.item = "_Test Non Stock Item"
|
|
||||||
plan.price_determination = "Fixed Rate"
|
|
||||||
plan.cost = 50
|
|
||||||
plan.currency = "USD"
|
|
||||||
plan.billing_interval = "Month"
|
|
||||||
plan.billing_interval_count = 1
|
|
||||||
plan.insert()
|
|
||||||
|
|
||||||
|
|
||||||
def create_parties():
|
|
||||||
if not frappe.db.exists("Supplier", "_Test Supplier"):
|
|
||||||
supplier = frappe.new_doc("Supplier")
|
|
||||||
supplier.supplier_name = "_Test Supplier"
|
|
||||||
supplier.supplier_group = "All Supplier Groups"
|
|
||||||
supplier.insert()
|
|
||||||
|
|
||||||
if not frappe.db.exists("Customer", "_Test Subscription Customer"):
|
|
||||||
customer = frappe.new_doc("Customer")
|
|
||||||
customer.customer_name = "_Test Subscription Customer"
|
|
||||||
customer.billing_currency = "USD"
|
|
||||||
customer.append(
|
|
||||||
"accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}
|
|
||||||
)
|
|
||||||
customer.insert()
|
|
||||||
|
|
||||||
|
|
||||||
def reset_settings():
|
|
||||||
settings = frappe.get_single("Subscription Settings")
|
|
||||||
settings.grace_period = 0
|
|
||||||
settings.cancel_after_grace = 0
|
|
||||||
settings.save()
|
|
||||||
|
|
||||||
|
|
||||||
class TestSubscription(unittest.TestCase):
|
class TestSubscription(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
create_plan()
|
make_plans()
|
||||||
create_parties()
|
create_parties()
|
||||||
reset_settings()
|
reset_settings()
|
||||||
|
|
||||||
def test_create_subscription_with_trial_with_correct_period(self):
|
def test_create_subscription_with_trial_with_correct_period(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Customer"
|
trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1)
|
||||||
subscription.party = "_Test Customer"
|
)
|
||||||
subscription.trial_period_start = nowdate()
|
|
||||||
subscription.trial_period_end = add_months(nowdate(), 1)
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
|
|
||||||
self.assertEqual(subscription.trial_period_start, nowdate())
|
self.assertEqual(subscription.trial_period_start, nowdate())
|
||||||
self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1))
|
self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -126,12 +44,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
self.assertEqual(subscription.status, "Trialling")
|
self.assertEqual(subscription.status, "Trialling")
|
||||||
|
|
||||||
def test_create_subscription_without_trial_with_correct_period(self):
|
def test_create_subscription_without_trial_with_correct_period(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription()
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
|
|
||||||
self.assertEqual(subscription.trial_period_start, None)
|
self.assertEqual(subscription.trial_period_start, None)
|
||||||
self.assertEqual(subscription.trial_period_end, None)
|
self.assertEqual(subscription.trial_period_end, None)
|
||||||
self.assertEqual(subscription.current_invoice_start, nowdate())
|
self.assertEqual(subscription.current_invoice_start, nowdate())
|
||||||
@ -141,55 +54,28 @@ class TestSubscription(unittest.TestCase):
|
|||||||
self.assertEqual(subscription.status, "Active")
|
self.assertEqual(subscription.status, "Active")
|
||||||
|
|
||||||
def test_create_subscription_trial_with_wrong_dates(self):
|
def test_create_subscription_trial_with_wrong_dates(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Customer"
|
trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True
|
||||||
subscription.party = "_Test Customer"
|
)
|
||||||
subscription.trial_period_end = nowdate()
|
|
||||||
subscription.trial_period_start = add_days(nowdate(), 30)
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, subscription.save)
|
|
||||||
|
|
||||||
def test_create_subscription_multi_with_different_billing_fails(self):
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.trial_period_end = nowdate()
|
|
||||||
subscription.trial_period_start = add_days(nowdate(), 30)
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1})
|
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, subscription.save)
|
self.assertRaises(frappe.ValidationError, subscription.save)
|
||||||
|
|
||||||
def test_invoice_is_generated_at_end_of_billing_period(self):
|
def test_invoice_is_generated_at_end_of_billing_period(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(start_date="2018-01-01")
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.insert()
|
|
||||||
|
|
||||||
self.assertEqual(subscription.status, "Active")
|
self.assertEqual(subscription.status, "Active")
|
||||||
self.assertEqual(subscription.current_invoice_start, "2018-01-01")
|
self.assertEqual(subscription.current_invoice_start, "2018-01-01")
|
||||||
self.assertEqual(subscription.current_invoice_end, "2018-01-31")
|
self.assertEqual(subscription.current_invoice_end, "2018-01-31")
|
||||||
frappe.flags.current_date = "2018-01-31"
|
|
||||||
subscription.process()
|
|
||||||
|
|
||||||
|
subscription.process(posting_date="2018-01-31")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.current_invoice_start, "2018-02-01")
|
self.assertEqual(subscription.current_invoice_start, "2018-02-01")
|
||||||
self.assertEqual(subscription.current_invoice_end, "2018-02-28")
|
self.assertEqual(subscription.current_invoice_end, "2018-02-28")
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
def test_status_goes_back_to_active_after_invoice_is_paid(self):
|
def test_status_goes_back_to_active_after_invoice_is_paid(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Customer"
|
start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period"
|
||||||
subscription.party = "_Test Customer"
|
)
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
subscription.process(posting_date="2018-01-01") # generate first invoice
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.generate_invoice_at_period_start = True
|
|
||||||
subscription.insert()
|
|
||||||
frappe.flags.current_date = "2018-01-01"
|
|
||||||
subscription.process() # generate first invoice
|
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
# Status is unpaid as Days until Due is zero and grace period is Zero
|
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||||
@ -213,18 +99,10 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.cancel_after_grace = 1
|
settings.cancel_after_grace = 1
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(start_date="2018-01-01")
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
# subscription.generate_invoice_at_period_start = True
|
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.insert()
|
|
||||||
|
|
||||||
self.assertEqual(subscription.status, "Active")
|
self.assertEqual(subscription.status, "Active")
|
||||||
|
|
||||||
frappe.flags.current_date = "2018-01-31"
|
subscription.process(posting_date="2018-01-31") # generate first invoice
|
||||||
subscription.process() # generate first invoice
|
|
||||||
# This should change status to Cancelled since grace period is 0
|
# This should change status to Cancelled since grace period is 0
|
||||||
# And is backdated subscription so subscription will be cancelled after processing
|
# And is backdated subscription so subscription will be cancelled after processing
|
||||||
self.assertEqual(subscription.status, "Cancelled")
|
self.assertEqual(subscription.status, "Cancelled")
|
||||||
@ -235,13 +113,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.cancel_after_grace = 0
|
settings.cancel_after_grace = 0
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(start_date="2018-01-01")
|
||||||
subscription.party_type = "Customer"
|
subscription.process(posting_date="2018-01-31") # generate first invoice
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.insert()
|
|
||||||
subscription.process() # generate first invoice
|
|
||||||
|
|
||||||
# Status is unpaid as Days until Due is zero and grace period is Zero
|
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
@ -251,21 +124,9 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_subscription_invoice_days_until_due(self):
|
def test_subscription_invoice_days_until_due(self):
|
||||||
_date = add_months(nowdate(), -1)
|
_date = add_months(nowdate(), -1)
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(start_date=_date, days_until_due=10)
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.days_until_due = 10
|
|
||||||
subscription.start_date = _date
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.insert()
|
|
||||||
|
|
||||||
frappe.flags.current_date = subscription.current_invoice_end
|
subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice
|
||||||
|
|
||||||
subscription.process() # generate first invoice
|
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
|
||||||
self.assertEqual(subscription.status, "Active")
|
|
||||||
|
|
||||||
frappe.flags.current_date = add_days(subscription.current_invoice_end, 3)
|
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, "Active")
|
self.assertEqual(subscription.status, "Active")
|
||||||
|
|
||||||
@ -275,16 +136,9 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.grace_period = 1000
|
settings.grace_period = 1000
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(start_date=add_days(nowdate(), -1000))
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.start_date = add_days(nowdate(), -1000)
|
|
||||||
subscription.insert()
|
|
||||||
|
|
||||||
frappe.flags.current_date = subscription.current_invoice_end
|
|
||||||
subscription.process() # generate first invoice
|
|
||||||
|
|
||||||
|
subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice
|
||||||
self.assertEqual(subscription.status, "Past Due Date")
|
self.assertEqual(subscription.status, "Past Due Date")
|
||||||
|
|
||||||
subscription.process()
|
subscription.process()
|
||||||
@ -301,12 +155,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
def test_subscription_remains_active_during_invoice_period(self):
|
def test_subscription_remains_active_during_invoice_period(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription() # no changes expected
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
subscription.process() # no changes expected
|
|
||||||
|
|
||||||
self.assertEqual(subscription.status, "Active")
|
self.assertEqual(subscription.status, "Active")
|
||||||
self.assertEqual(subscription.current_invoice_start, nowdate())
|
self.assertEqual(subscription.current_invoice_start, nowdate())
|
||||||
@ -325,12 +174,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
|
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
|
||||||
self.assertEqual(len(subscription.invoices), 0)
|
self.assertEqual(len(subscription.invoices), 0)
|
||||||
|
|
||||||
def test_subscription_cancelation(self):
|
def test_subscription_cancellation(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription()
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
|
|
||||||
self.assertEqual(subscription.status, "Cancelled")
|
self.assertEqual(subscription.status, "Cancelled")
|
||||||
@ -341,11 +186,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.prorate = 1
|
settings.prorate = 1
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription()
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
|
|
||||||
self.assertEqual(subscription.status, "Active")
|
self.assertEqual(subscription.status, "Active")
|
||||||
|
|
||||||
@ -365,7 +206,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
get_prorata_factor(
|
get_prorata_factor(
|
||||||
subscription.current_invoice_end,
|
subscription.current_invoice_end,
|
||||||
subscription.current_invoice_start,
|
subscription.current_invoice_start,
|
||||||
subscription.generate_invoice_at_period_start,
|
cint(subscription.generate_invoice_at == "Beginning of the current subscription period"),
|
||||||
),
|
),
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
@ -383,11 +224,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.prorate = 0
|
settings.prorate = 0
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription()
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
invoice = subscription.get_current_invoice()
|
invoice = subscription.get_current_invoice()
|
||||||
|
|
||||||
@ -402,11 +239,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.prorate = 1
|
settings.prorate = 1
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription()
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
|
|
||||||
invoice = subscription.get_current_invoice()
|
invoice = subscription.get_current_invoice()
|
||||||
@ -421,18 +254,13 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.prorate = to_prorate
|
settings.prorate = to_prorate
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
def test_subcription_cancellation_and_process(self):
|
def test_subscription_cancellation_and_process(self):
|
||||||
settings = frappe.get_single("Subscription Settings")
|
settings = frappe.get_single("Subscription Settings")
|
||||||
default_grace_period_action = settings.cancel_after_grace
|
default_grace_period_action = settings.cancel_after_grace
|
||||||
settings.cancel_after_grace = 1
|
settings.cancel_after_grace = 1
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(start_date="2018-01-01")
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.insert()
|
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
|
|
||||||
# Generate an invoice for the cancelled period
|
# Generate an invoice for the cancelled period
|
||||||
@ -458,14 +286,8 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.cancel_after_grace = 0
|
settings.cancel_after_grace = 0
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(start_date="2018-01-01")
|
||||||
subscription.party_type = "Customer"
|
subscription.process(posting_date="2018-01-31") # generate first invoice
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.insert()
|
|
||||||
frappe.flags.current_date = "2018-01-31"
|
|
||||||
subscription.process() # generate first invoice
|
|
||||||
|
|
||||||
# Status is unpaid as Days until Due is zero and grace period is Zero
|
# Status is unpaid as Days until Due is zero and grace period is Zero
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
@ -494,17 +316,10 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.cancel_after_grace = 0
|
settings.cancel_after_grace = 0
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Customer"
|
start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period"
|
||||||
subscription.party = "_Test Customer"
|
)
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
subscription.process(subscription.current_invoice_start) # generate first invoice
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.generate_invoice_at_period_start = True
|
|
||||||
subscription.insert()
|
|
||||||
|
|
||||||
frappe.flags.current_date = subscription.current_invoice_start
|
|
||||||
|
|
||||||
subscription.process() # generate first invoice
|
|
||||||
# This should change status to Unpaid since grace period is 0
|
# This should change status to Unpaid since grace period is 0
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
@ -516,29 +331,18 @@ class TestSubscription(unittest.TestCase):
|
|||||||
self.assertEqual(subscription.status, "Active")
|
self.assertEqual(subscription.status, "Active")
|
||||||
|
|
||||||
# A new invoice is generated
|
# A new invoice is generated
|
||||||
frappe.flags.current_date = subscription.current_invoice_start
|
subscription.process(posting_date=subscription.current_invoice_start)
|
||||||
subscription.process()
|
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
settings.cancel_after_grace = default_grace_period_action
|
settings.cancel_after_grace = default_grace_period_action
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
def test_restart_active_subscription(self):
|
def test_restart_active_subscription(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription()
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
|
self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
|
||||||
|
|
||||||
def test_subscription_invoice_discount_percentage(self):
|
def test_subscription_invoice_discount_percentage(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(additional_discount_percentage=10)
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.additional_discount_percentage = 10
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
|
|
||||||
invoice = subscription.get_current_invoice()
|
invoice = subscription.get_current_invoice()
|
||||||
@ -547,12 +351,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
self.assertEqual(invoice.apply_discount_on, "Grand Total")
|
self.assertEqual(invoice.apply_discount_on, "Grand Total")
|
||||||
|
|
||||||
def test_subscription_invoice_discount_amount(self):
|
def test_subscription_invoice_discount_amount(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(additional_discount_amount=11)
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.additional_discount_amount = 11
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
|
|
||||||
invoice = subscription.get_current_invoice()
|
invoice = subscription.get_current_invoice()
|
||||||
@ -563,18 +362,13 @@ class TestSubscription(unittest.TestCase):
|
|||||||
def test_prepaid_subscriptions(self):
|
def test_prepaid_subscriptions(self):
|
||||||
# Create a non pre-billed subscription, processing should not create
|
# Create a non pre-billed subscription, processing should not create
|
||||||
# invoices.
|
# invoices.
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription()
|
||||||
subscription.party_type = "Customer"
|
|
||||||
subscription.party = "_Test Customer"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
subscription.process()
|
subscription.process()
|
||||||
|
|
||||||
self.assertEqual(len(subscription.invoices), 0)
|
self.assertEqual(len(subscription.invoices), 0)
|
||||||
|
|
||||||
# Change the subscription type to prebilled and process it.
|
# Change the subscription type to prebilled and process it.
|
||||||
# Prepaid invoice should be generated
|
# Prepaid invoice should be generated
|
||||||
subscription.generate_invoice_at_period_start = True
|
subscription.generate_invoice_at = "Beginning of the current subscription period"
|
||||||
subscription.save()
|
subscription.save()
|
||||||
subscription.process()
|
subscription.process()
|
||||||
|
|
||||||
@ -586,12 +380,9 @@ class TestSubscription(unittest.TestCase):
|
|||||||
settings.prorate = 1
|
settings.prorate = 1
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Customer"
|
generate_invoice_at="Beginning of the current subscription period"
|
||||||
subscription.party = "_Test Customer"
|
)
|
||||||
subscription.generate_invoice_at_period_start = True
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
subscription.process()
|
subscription.process()
|
||||||
subscription.cancel_subscription()
|
subscription.cancel_subscription()
|
||||||
|
|
||||||
@ -609,9 +400,10 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_subscription_with_follow_calendar_months(self):
|
def test_subscription_with_follow_calendar_months(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = frappe.new_doc("Subscription")
|
||||||
|
subscription.company = "_Test Company"
|
||||||
subscription.party_type = "Supplier"
|
subscription.party_type = "Supplier"
|
||||||
subscription.party = "_Test Supplier"
|
subscription.party = "_Test Supplier"
|
||||||
subscription.generate_invoice_at_period_start = 1
|
subscription.generate_invoice_at = "Beginning of the current subscription period"
|
||||||
subscription.follow_calendar_months = 1
|
subscription.follow_calendar_months = 1
|
||||||
|
|
||||||
# select subscription start date as "2018-01-15"
|
# select subscription start date as "2018-01-15"
|
||||||
@ -625,39 +417,33 @@ class TestSubscription(unittest.TestCase):
|
|||||||
self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
|
self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
|
||||||
|
|
||||||
def test_subscription_generate_invoice_past_due(self):
|
def test_subscription_generate_invoice_past_due(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Supplier"
|
start_date="2018-01-01",
|
||||||
subscription.party = "_Test Supplier"
|
party_type="Supplier",
|
||||||
subscription.generate_invoice_at_period_start = 1
|
party="_Test Supplier",
|
||||||
subscription.generate_new_invoices_past_due_date = 1
|
generate_invoice_at="Beginning of the current subscription period",
|
||||||
# select subscription start date as "2018-01-15"
|
generate_new_invoices_past_due_date=1,
|
||||||
subscription.start_date = "2018-01-01"
|
plans=[{"plan": "_Test Plan Name 4", "qty": 1}],
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
|
)
|
||||||
subscription.save()
|
|
||||||
|
|
||||||
frappe.flags.current_date = "2018-01-01"
|
|
||||||
# Process subscription and create first invoice
|
# Process subscription and create first invoice
|
||||||
# Subscription status will be unpaid since due date has already passed
|
# Subscription status will be unpaid since due date has already passed
|
||||||
subscription.process()
|
subscription.process(posting_date="2018-01-01")
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
self.assertEqual(subscription.status, "Unpaid")
|
self.assertEqual(subscription.status, "Unpaid")
|
||||||
|
|
||||||
# Now the Subscription is unpaid
|
# Now the Subscription is unpaid
|
||||||
# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
|
# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
|
||||||
# subscription and the interval between the subscriptions is 3 months
|
# subscription and the interval between the subscriptions is 3 months
|
||||||
frappe.flags.current_date = "2018-04-01"
|
subscription.process(posting_date="2018-04-01")
|
||||||
subscription.process()
|
|
||||||
self.assertEqual(len(subscription.invoices), 2)
|
self.assertEqual(len(subscription.invoices), 2)
|
||||||
|
|
||||||
def test_subscription_without_generate_invoice_past_due(self):
|
def test_subscription_without_generate_invoice_past_due(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Supplier"
|
start_date="2018-01-01",
|
||||||
subscription.party = "_Test Supplier"
|
generate_invoice_at="Beginning of the current subscription period",
|
||||||
subscription.generate_invoice_at_period_start = 1
|
plans=[{"plan": "_Test Plan Name 4", "qty": 1}],
|
||||||
# select subscription start date as "2018-01-15"
|
)
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
|
|
||||||
# Process subscription and create first invoice
|
# Process subscription and create first invoice
|
||||||
# Subscription status will be unpaid since due date has already passed
|
# Subscription status will be unpaid since due date has already passed
|
||||||
@ -668,16 +454,13 @@ class TestSubscription(unittest.TestCase):
|
|||||||
subscription.process()
|
subscription.process()
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
def test_multicurrency_subscription(self):
|
def test_multi_currency_subscription(self):
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Customer"
|
start_date="2018-01-01",
|
||||||
subscription.party = "_Test Subscription Customer"
|
generate_invoice_at="Beginning of the current subscription period",
|
||||||
subscription.generate_invoice_at_period_start = 1
|
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}],
|
||||||
subscription.company = "_Test Company"
|
party="_Test Subscription Customer",
|
||||||
# select subscription start date as "2018-01-15"
|
)
|
||||||
subscription.start_date = "2018-01-01"
|
|
||||||
subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1})
|
|
||||||
subscription.save()
|
|
||||||
|
|
||||||
subscription.process()
|
subscription.process()
|
||||||
self.assertEqual(len(subscription.invoices), 1)
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
@ -689,42 +472,135 @@ class TestSubscription(unittest.TestCase):
|
|||||||
|
|
||||||
def test_subscription_recovery(self):
|
def test_subscription_recovery(self):
|
||||||
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
|
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
|
||||||
subscription = frappe.new_doc("Subscription")
|
subscription = create_subscription(
|
||||||
subscription.party_type = "Customer"
|
start_date="2021-01-01",
|
||||||
subscription.party = "_Test Subscription Customer"
|
submit_invoice=0,
|
||||||
subscription.company = "_Test Company"
|
generate_new_invoices_past_due_date=1,
|
||||||
subscription.start_date = "2021-12-01"
|
party="_Test Subscription Customer",
|
||||||
subscription.generate_new_invoices_past_due_date = 1
|
)
|
||||||
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
|
||||||
subscription.submit_invoice = 0
|
|
||||||
subscription.save()
|
|
||||||
|
|
||||||
# create invoices for the first two moths
|
# create invoices for the first two moths
|
||||||
frappe.flags.current_date = "2021-12-31"
|
subscription.process(posting_date="2021-01-31")
|
||||||
subscription.process()
|
|
||||||
|
|
||||||
frappe.flags.current_date = "2022-01-31"
|
subscription.process(posting_date="2021-02-28")
|
||||||
subscription.process()
|
|
||||||
|
|
||||||
self.assertEqual(len(subscription.invoices), 2)
|
self.assertEqual(len(subscription.invoices), 2)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
|
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
|
||||||
getdate("2021-12-01"),
|
getdate("2021-01-01"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
|
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
|
||||||
getdate("2022-01-01"),
|
getdate("2021-02-01"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# recreate most recent invoice
|
# recreate most recent invoice
|
||||||
subscription.process()
|
subscription.process(posting_date="2022-01-31")
|
||||||
|
|
||||||
self.assertEqual(len(subscription.invoices), 2)
|
self.assertEqual(len(subscription.invoices), 2)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
|
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
|
||||||
getdate("2021-12-01"),
|
getdate("2021-01-01"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
|
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
|
||||||
getdate("2022-01-01"),
|
getdate("2021-02-01"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_subscription_invoice_generation_before_days(self):
|
||||||
|
subscription = create_subscription(
|
||||||
|
start_date="2023-01-01",
|
||||||
|
generate_invoice_at="Days before the current subscription period",
|
||||||
|
number_of_days=10,
|
||||||
|
generate_new_invoices_past_due_date=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
subscription.process(posting_date="2022-12-22")
|
||||||
|
self.assertEqual(len(subscription.invoices), 1)
|
||||||
|
|
||||||
|
subscription.process(posting_date="2023-01-22")
|
||||||
|
self.assertEqual(len(subscription.invoices), 2)
|
||||||
|
|
||||||
|
|
||||||
|
def make_plans():
|
||||||
|
create_plan(plan_name="_Test Plan Name", cost=900)
|
||||||
|
create_plan(plan_name="_Test Plan Name 2", cost=1999)
|
||||||
|
create_plan(
|
||||||
|
plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14
|
||||||
|
)
|
||||||
|
create_plan(
|
||||||
|
plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3
|
||||||
|
)
|
||||||
|
create_plan(
|
||||||
|
plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_plan(**kwargs):
|
||||||
|
if not frappe.db.exists("Subscription Plan", kwargs.get("plan_name")):
|
||||||
|
plan = frappe.new_doc("Subscription Plan")
|
||||||
|
plan.plan_name = kwargs.get("plan_name") or "_Test Plan Name"
|
||||||
|
plan.item = kwargs.get("item") or "_Test Non Stock Item"
|
||||||
|
plan.price_determination = kwargs.get("price_determination") or "Fixed Rate"
|
||||||
|
plan.cost = kwargs.get("cost") or 1000
|
||||||
|
plan.billing_interval = kwargs.get("billing_interval") or "Month"
|
||||||
|
plan.billing_interval_count = kwargs.get("billing_interval_count") or 1
|
||||||
|
plan.currency = kwargs.get("currency")
|
||||||
|
plan.insert()
|
||||||
|
|
||||||
|
|
||||||
|
def create_parties():
|
||||||
|
if not frappe.db.exists("Supplier", "_Test Supplier"):
|
||||||
|
supplier = frappe.new_doc("Supplier")
|
||||||
|
supplier.supplier_name = "_Test Supplier"
|
||||||
|
supplier.supplier_group = "All Supplier Groups"
|
||||||
|
supplier.insert()
|
||||||
|
|
||||||
|
if not frappe.db.exists("Customer", "_Test Subscription Customer"):
|
||||||
|
customer = frappe.new_doc("Customer")
|
||||||
|
customer.customer_name = "_Test Subscription Customer"
|
||||||
|
customer.billing_currency = "USD"
|
||||||
|
customer.append(
|
||||||
|
"accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}
|
||||||
|
)
|
||||||
|
customer.insert()
|
||||||
|
|
||||||
|
|
||||||
|
def reset_settings():
|
||||||
|
settings = frappe.get_single("Subscription Settings")
|
||||||
|
settings.grace_period = 0
|
||||||
|
settings.cancel_after_grace = 0
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
|
||||||
|
def create_subscription(**kwargs):
|
||||||
|
subscription = frappe.new_doc("Subscription")
|
||||||
|
subscription.party_type = (kwargs.get("party_type") or "Customer",)
|
||||||
|
subscription.company = kwargs.get("company") or "_Test Company"
|
||||||
|
subscription.party = kwargs.get("party") or "_Test Customer"
|
||||||
|
subscription.trial_period_start = kwargs.get("trial_period_start")
|
||||||
|
subscription.trial_period_end = kwargs.get("trial_period_end")
|
||||||
|
subscription.start_date = kwargs.get("start_date")
|
||||||
|
subscription.generate_invoice_at = kwargs.get("generate_invoice_at")
|
||||||
|
subscription.additional_discount_percentage = kwargs.get("additional_discount_percentage")
|
||||||
|
subscription.additional_discount_amount = kwargs.get("additional_discount_amount")
|
||||||
|
subscription.follow_calendar_months = kwargs.get("follow_calendar_months")
|
||||||
|
subscription.generate_new_invoices_past_due_date = kwargs.get(
|
||||||
|
"generate_new_invoices_past_due_date"
|
||||||
|
)
|
||||||
|
subscription.submit_invoice = kwargs.get("submit_invoice")
|
||||||
|
subscription.days_until_due = kwargs.get("days_until_due")
|
||||||
|
subscription.number_of_days = kwargs.get("number_of_days")
|
||||||
|
|
||||||
|
if not kwargs.get("plans"):
|
||||||
|
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
|
||||||
|
else:
|
||||||
|
for plan in kwargs.get("plans"):
|
||||||
|
subscription.append("plans", plan)
|
||||||
|
|
||||||
|
if kwargs.get("do_not_save"):
|
||||||
|
return subscription
|
||||||
|
|
||||||
|
subscription.save()
|
||||||
|
|
||||||
|
return subscription
|
||||||
|
@ -57,18 +57,17 @@ def get_plan_rate(
|
|||||||
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
|
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
|
||||||
|
|
||||||
if prorate:
|
if prorate:
|
||||||
prorate_factor = flt(
|
cost -= plan.cost * get_prorate_factor(start_date, end_date)
|
||||||
date_diff(start_date, get_first_day(start_date))
|
|
||||||
/ date_diff(get_last_day(start_date), get_first_day(start_date)),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
|
|
||||||
prorate_factor += flt(
|
|
||||||
date_diff(get_last_day(end_date), end_date)
|
|
||||||
/ date_diff(get_last_day(end_date), get_first_day(end_date)),
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
|
|
||||||
cost -= plan.cost * prorate_factor
|
|
||||||
|
|
||||||
return cost
|
return cost
|
||||||
|
|
||||||
|
|
||||||
|
def get_prorate_factor(start_date, end_date):
|
||||||
|
total_days_to_skip = date_diff(start_date, get_first_day(start_date))
|
||||||
|
total_days_in_month = int(get_last_day(start_date).strftime("%d"))
|
||||||
|
prorate_factor = flt(total_days_to_skip / total_days_in_month)
|
||||||
|
|
||||||
|
total_days_to_skip = date_diff(get_last_day(end_date), end_date)
|
||||||
|
total_days_in_month = int(get_last_day(end_date).strftime("%d"))
|
||||||
|
prorate_factor += flt(total_days_to_skip / total_days_in_month)
|
||||||
|
|
||||||
|
return prorate_factor
|
||||||
|
@ -271,9 +271,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
|||||||
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
|
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
tax_amount = net_total * tax_details.rate / 100
|
||||||
else:
|
else:
|
||||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
tax_amount = net_total * tax_details.rate / 100
|
||||||
|
|
||||||
# once tds is deducted, not need to add vouchers in the invoice
|
# once tds is deducted, not need to add vouchers in the invoice
|
||||||
voucher_wise_amount = {}
|
voucher_wise_amount = {}
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2023-08-22 10:28:10.196712",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"account",
|
||||||
|
"party_type",
|
||||||
|
"party",
|
||||||
|
"reference_doctype",
|
||||||
|
"reference_name",
|
||||||
|
"allocated_amount",
|
||||||
|
"account_currency",
|
||||||
|
"unlinked"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "reference_name",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Reference Name",
|
||||||
|
"options": "reference_doctype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "allocated_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Allocated Amount",
|
||||||
|
"options": "account_currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "unlinked",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Unlinked",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reference_doctype",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Reference Type",
|
||||||
|
"options": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party_type",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Party Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Party"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Account Currency",
|
||||||
|
"options": "Currency",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-09-05 09:33:28.620149",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Unreconcile Payment Entries",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
# 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 UnreconcilePaymentEntries(Document):
|
||||||
|
pass
|
@ -0,0 +1,316 @@
|
|||||||
|
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import today
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.create_company()
|
||||||
|
self.create_customer()
|
||||||
|
self.create_usd_receivable_account()
|
||||||
|
self.create_item()
|
||||||
|
self.clear_old_entries()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def create_sales_invoice(self, do_not_submit=False):
|
||||||
|
si = create_sales_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
posting_date=today(),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
rate=100,
|
||||||
|
price_list_rate=100,
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
|
)
|
||||||
|
return si
|
||||||
|
|
||||||
|
def create_payment_entry(self):
|
||||||
|
pe = create_payment_entry(
|
||||||
|
company=self.company,
|
||||||
|
payment_type="Receive",
|
||||||
|
party_type="Customer",
|
||||||
|
party=self.customer,
|
||||||
|
paid_from=self.debit_to,
|
||||||
|
paid_to=self.cash,
|
||||||
|
paid_amount=200,
|
||||||
|
save=True,
|
||||||
|
)
|
||||||
|
return pe
|
||||||
|
|
||||||
|
def test_01_unreconcile_invoice(self):
|
||||||
|
si1 = self.create_sales_invoice()
|
||||||
|
si2 = self.create_sales_invoice()
|
||||||
|
|
||||||
|
pe = self.create_payment_entry()
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
|
||||||
|
)
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
|
||||||
|
)
|
||||||
|
# Allocation payment against both invoices
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
# Assert outstanding
|
||||||
|
[doc.reload() for doc in [si1, si2, pe]]
|
||||||
|
self.assertEqual(si1.outstanding_amount, 0)
|
||||||
|
self.assertEqual(si2.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pe.unallocated_amount, 0)
|
||||||
|
|
||||||
|
unreconcile = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Unreconcile Payments",
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": pe.doctype,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
unreconcile.add_references()
|
||||||
|
self.assertEqual(len(unreconcile.allocations), 2)
|
||||||
|
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||||
|
self.assertEquals([si1.name, si2.name], allocations)
|
||||||
|
# unreconcile si1
|
||||||
|
for x in unreconcile.allocations:
|
||||||
|
if x.reference_name != si1.name:
|
||||||
|
unreconcile.remove(x)
|
||||||
|
unreconcile.save().submit()
|
||||||
|
|
||||||
|
# Assert outstanding
|
||||||
|
[doc.reload() for doc in [si1, si2, pe]]
|
||||||
|
self.assertEqual(si1.outstanding_amount, 100)
|
||||||
|
self.assertEqual(si2.outstanding_amount, 0)
|
||||||
|
self.assertEqual(len(pe.references), 1)
|
||||||
|
self.assertEqual(pe.unallocated_amount, 100)
|
||||||
|
|
||||||
|
def test_02_unreconcile_one_payment_from_multi_payments(self):
|
||||||
|
"""
|
||||||
|
Scenario: 2 payments, both split against 2 different invoices
|
||||||
|
Unreconcile only one payment from one invoice
|
||||||
|
"""
|
||||||
|
si1 = self.create_sales_invoice()
|
||||||
|
si2 = self.create_sales_invoice()
|
||||||
|
pe1 = self.create_payment_entry()
|
||||||
|
pe1.paid_amount = 100
|
||||||
|
# Allocate payment against both invoices
|
||||||
|
pe1.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||||
|
)
|
||||||
|
pe1.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||||
|
)
|
||||||
|
pe1.save().submit()
|
||||||
|
|
||||||
|
pe2 = self.create_payment_entry()
|
||||||
|
pe2.paid_amount = 100
|
||||||
|
# Allocate payment against both invoices
|
||||||
|
pe2.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||||
|
)
|
||||||
|
pe2.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||||
|
)
|
||||||
|
pe2.save().submit()
|
||||||
|
|
||||||
|
# Assert outstanding and unallocated
|
||||||
|
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||||
|
self.assertEqual(si1.outstanding_amount, 0.0)
|
||||||
|
self.assertEqual(si2.outstanding_amount, 0.0)
|
||||||
|
self.assertEqual(pe1.unallocated_amount, 0.0)
|
||||||
|
self.assertEqual(pe2.unallocated_amount, 0.0)
|
||||||
|
|
||||||
|
unreconcile = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Unreconcile Payments",
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": pe2.doctype,
|
||||||
|
"voucher_no": pe2.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
unreconcile.add_references()
|
||||||
|
self.assertEqual(len(unreconcile.allocations), 2)
|
||||||
|
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||||
|
self.assertEquals([si1.name, si2.name], allocations)
|
||||||
|
# unreconcile si1 from pe2
|
||||||
|
for x in unreconcile.allocations:
|
||||||
|
if x.reference_name != si1.name:
|
||||||
|
unreconcile.remove(x)
|
||||||
|
unreconcile.save().submit()
|
||||||
|
|
||||||
|
# Assert outstanding and unallocated
|
||||||
|
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||||
|
self.assertEqual(si1.outstanding_amount, 50)
|
||||||
|
self.assertEqual(si2.outstanding_amount, 0)
|
||||||
|
self.assertEqual(len(pe1.references), 2)
|
||||||
|
self.assertEqual(len(pe2.references), 1)
|
||||||
|
self.assertEqual(pe1.unallocated_amount, 0)
|
||||||
|
self.assertEqual(pe2.unallocated_amount, 50)
|
||||||
|
|
||||||
|
def test_03_unreconciliation_on_multi_currency_invoice(self):
|
||||||
|
self.create_customer("_Test MC Customer USD", "USD")
|
||||||
|
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si1.currency = "USD"
|
||||||
|
si1.debit_to = self.debtors_usd
|
||||||
|
si1.conversion_rate = 80
|
||||||
|
si1.save().submit()
|
||||||
|
|
||||||
|
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si2.currency = "USD"
|
||||||
|
si2.debit_to = self.debtors_usd
|
||||||
|
si2.conversion_rate = 80
|
||||||
|
si2.save().submit()
|
||||||
|
|
||||||
|
pe = self.create_payment_entry()
|
||||||
|
pe.paid_from = self.debtors_usd
|
||||||
|
pe.paid_from_account_currency = "USD"
|
||||||
|
pe.source_exchange_rate = 75
|
||||||
|
pe.received_amount = 75 * 200
|
||||||
|
pe.save()
|
||||||
|
# Allocate payment against both invoices
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
|
||||||
|
)
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
|
||||||
|
)
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
unreconcile = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Unreconcile Payments",
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": pe.doctype,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
unreconcile.add_references()
|
||||||
|
self.assertEqual(len(unreconcile.allocations), 2)
|
||||||
|
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||||
|
self.assertEquals([si1.name, si2.name], allocations)
|
||||||
|
# unreconcile si1 from pe
|
||||||
|
for x in unreconcile.allocations:
|
||||||
|
if x.reference_name != si1.name:
|
||||||
|
unreconcile.remove(x)
|
||||||
|
unreconcile.save().submit()
|
||||||
|
|
||||||
|
# Assert outstanding and unallocated
|
||||||
|
[doc.reload() for doc in [si1, si2, pe]]
|
||||||
|
self.assertEqual(si1.outstanding_amount, 100)
|
||||||
|
self.assertEqual(si2.outstanding_amount, 0)
|
||||||
|
self.assertEqual(len(pe.references), 1)
|
||||||
|
self.assertEqual(pe.unallocated_amount, 100)
|
||||||
|
|
||||||
|
# Exc gain/loss JE should've been cancelled as well
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.count(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_04_unreconciliation_on_multi_currency_invoice(self):
|
||||||
|
"""
|
||||||
|
2 payments split against 2 foreign currency invoices
|
||||||
|
"""
|
||||||
|
self.create_customer("_Test MC Customer USD", "USD")
|
||||||
|
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si1.currency = "USD"
|
||||||
|
si1.debit_to = self.debtors_usd
|
||||||
|
si1.conversion_rate = 80
|
||||||
|
si1.save().submit()
|
||||||
|
|
||||||
|
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si2.currency = "USD"
|
||||||
|
si2.debit_to = self.debtors_usd
|
||||||
|
si2.conversion_rate = 80
|
||||||
|
si2.save().submit()
|
||||||
|
|
||||||
|
pe1 = self.create_payment_entry()
|
||||||
|
pe1.paid_from = self.debtors_usd
|
||||||
|
pe1.paid_from_account_currency = "USD"
|
||||||
|
pe1.source_exchange_rate = 75
|
||||||
|
pe1.received_amount = 75 * 100
|
||||||
|
pe1.save()
|
||||||
|
# Allocate payment against both invoices
|
||||||
|
pe1.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||||
|
)
|
||||||
|
pe1.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||||
|
)
|
||||||
|
pe1.save().submit()
|
||||||
|
|
||||||
|
pe2 = self.create_payment_entry()
|
||||||
|
pe2.paid_from = self.debtors_usd
|
||||||
|
pe2.paid_from_account_currency = "USD"
|
||||||
|
pe2.source_exchange_rate = 75
|
||||||
|
pe2.received_amount = 75 * 100
|
||||||
|
pe2.save()
|
||||||
|
# Allocate payment against both invoices
|
||||||
|
pe2.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||||
|
)
|
||||||
|
pe2.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||||
|
)
|
||||||
|
pe2.save().submit()
|
||||||
|
|
||||||
|
unreconcile = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Unreconcile Payments",
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": pe2.doctype,
|
||||||
|
"voucher_no": pe2.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
unreconcile.add_references()
|
||||||
|
self.assertEqual(len(unreconcile.allocations), 2)
|
||||||
|
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||||
|
self.assertEquals([si1.name, si2.name], allocations)
|
||||||
|
# unreconcile si1 from pe2
|
||||||
|
for x in unreconcile.allocations:
|
||||||
|
if x.reference_name != si1.name:
|
||||||
|
unreconcile.remove(x)
|
||||||
|
unreconcile.save().submit()
|
||||||
|
|
||||||
|
# Assert outstanding and unallocated
|
||||||
|
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||||
|
self.assertEqual(si1.outstanding_amount, 50)
|
||||||
|
self.assertEqual(si2.outstanding_amount, 0)
|
||||||
|
self.assertEqual(len(pe1.references), 2)
|
||||||
|
self.assertEqual(len(pe2.references), 1)
|
||||||
|
self.assertEqual(pe1.unallocated_amount, 0)
|
||||||
|
self.assertEqual(pe2.unallocated_amount, 50)
|
||||||
|
|
||||||
|
# Exc gain/loss JE from PE1 should be available
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.count(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
)
|
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on("Unreconcile Payments", {
|
||||||
|
refresh(frm) {
|
||||||
|
frm.set_query("voucher_type", function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
name: ["in", ["Payment Entry", "Journal Entry"]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
frm.set_query("voucher_no", function(doc) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: doc.company,
|
||||||
|
docstatus: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
get_allocations: function(frm) {
|
||||||
|
frm.clear_table("allocations");
|
||||||
|
frappe.call({
|
||||||
|
method: "get_allocations_from_payment",
|
||||||
|
doc: frm.doc,
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
r.message.forEach(x => {
|
||||||
|
frm.add_child("allocations", x)
|
||||||
|
})
|
||||||
|
frm.refresh_fields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,93 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "format:UNREC-{#####}",
|
||||||
|
"creation": "2023-08-22 10:26:34.421423",
|
||||||
|
"default_view": "List",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company",
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"get_allocations",
|
||||||
|
"allocations",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Unreconcile Payments",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Voucher Type",
|
||||||
|
"options": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Voucher No",
|
||||||
|
"options": "voucher_type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "get_allocations",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Allocations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "allocations",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Allocations",
|
||||||
|
"options": "Unreconcile Payment Entries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2023-08-28 17:42:50.261377",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Unreconcile Payments",
|
||||||
|
"naming_rule": "Expression",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"select": 1,
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"select": 1,
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
# 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.query_builder import Criterion
|
||||||
|
from frappe.query_builder.functions import Abs, Sum
|
||||||
|
from frappe.utils.data import comma_and
|
||||||
|
|
||||||
|
from erpnext.accounts.utils import (
|
||||||
|
cancel_exchange_gain_loss_journal,
|
||||||
|
unlink_ref_doc_from_payment_entries,
|
||||||
|
update_voucher_outstanding,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UnreconcilePayments(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.supported_types = ["Payment Entry", "Journal Entry"]
|
||||||
|
if not self.voucher_type in self.supported_types:
|
||||||
|
frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types)))
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_allocations_from_payment(self):
|
||||||
|
allocated_references = []
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
allocated_references = (
|
||||||
|
qb.from_(ple)
|
||||||
|
.select(
|
||||||
|
ple.account,
|
||||||
|
ple.party_type,
|
||||||
|
ple.party,
|
||||||
|
ple.against_voucher_type.as_("reference_doctype"),
|
||||||
|
ple.against_voucher_no.as_("reference_name"),
|
||||||
|
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||||
|
ple.account_currency,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(ple.docstatus == 1)
|
||||||
|
& (ple.voucher_type == self.voucher_type)
|
||||||
|
& (ple.voucher_no == self.voucher_no)
|
||||||
|
& (ple.voucher_no != ple.against_voucher_no)
|
||||||
|
)
|
||||||
|
.groupby(ple.against_voucher_type, ple.against_voucher_no)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
return allocated_references
|
||||||
|
|
||||||
|
def add_references(self):
|
||||||
|
allocations = self.get_allocations_from_payment()
|
||||||
|
|
||||||
|
for alloc in allocations:
|
||||||
|
self.append("allocations", alloc)
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
# todo: more granular unreconciliation
|
||||||
|
for alloc in self.allocations:
|
||||||
|
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
|
||||||
|
unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
|
||||||
|
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
|
||||||
|
update_voucher_outstanding(
|
||||||
|
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||||
|
)
|
||||||
|
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def doc_has_references(doctype: str = None, docname: str = None):
|
||||||
|
if doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
|
return frappe.db.count(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return frappe.db.count(
|
||||||
|
"Payment Ledger Entry",
|
||||||
|
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_linked_payments_for_doc(
|
||||||
|
company: str = None, doctype: str = None, docname: str = None
|
||||||
|
) -> list:
|
||||||
|
if company and doctype and docname:
|
||||||
|
_dt = doctype
|
||||||
|
_dn = docname
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
if _dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
|
criteria = [
|
||||||
|
(ple.company == company),
|
||||||
|
(ple.delinked == 0),
|
||||||
|
(ple.against_voucher_no == _dn),
|
||||||
|
(ple.amount < 0),
|
||||||
|
]
|
||||||
|
|
||||||
|
res = (
|
||||||
|
qb.from_(ple)
|
||||||
|
.select(
|
||||||
|
ple.company,
|
||||||
|
ple.voucher_type,
|
||||||
|
ple.voucher_no,
|
||||||
|
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||||
|
ple.account_currency,
|
||||||
|
)
|
||||||
|
.where(Criterion.all(criteria))
|
||||||
|
.groupby(ple.voucher_no, ple.against_voucher_no)
|
||||||
|
.having(qb.Field("allocated_amount") > 0)
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
criteria = [
|
||||||
|
(ple.company == company),
|
||||||
|
(ple.delinked == 0),
|
||||||
|
(ple.voucher_no == _dn),
|
||||||
|
(ple.against_voucher_no != _dn),
|
||||||
|
]
|
||||||
|
|
||||||
|
query = (
|
||||||
|
qb.from_(ple)
|
||||||
|
.select(
|
||||||
|
ple.company,
|
||||||
|
ple.against_voucher_type.as_("voucher_type"),
|
||||||
|
ple.against_voucher_no.as_("voucher_no"),
|
||||||
|
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||||
|
ple.account_currency,
|
||||||
|
)
|
||||||
|
.where(Criterion.all(criteria))
|
||||||
|
.groupby(ple.against_voucher_no)
|
||||||
|
)
|
||||||
|
res = query.run(as_dict=True)
|
||||||
|
return res
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_unreconcile_doc_for_selection(selections=None):
|
||||||
|
if selections:
|
||||||
|
selections = frappe.json.loads(selections)
|
||||||
|
# assuming each row is a unique voucher
|
||||||
|
for row in selections:
|
||||||
|
unrecon = frappe.new_doc("Unreconcile Payments")
|
||||||
|
unrecon.company = row.get("company")
|
||||||
|
unrecon.voucher_type = row.get("voucher_type")
|
||||||
|
unrecon.voucher_no = row.get("voucher_no")
|
||||||
|
unrecon.add_references()
|
||||||
|
|
||||||
|
# remove unselected references
|
||||||
|
unrecon.allocations = [
|
||||||
|
x
|
||||||
|
for x in unrecon.allocations
|
||||||
|
if x.reference_doctype == row.get("against_voucher_type")
|
||||||
|
and x.reference_name == row.get("against_voucher_no")
|
||||||
|
]
|
||||||
|
unrecon.save().submit()
|
@ -539,6 +539,10 @@ def get_round_off_account_and_cost_center(
|
|||||||
"Company", company, ["round_off_account", "round_off_cost_center"]
|
"Company", company, ["round_off_account", "round_off_cost_center"]
|
||||||
) or [None, None]
|
) or [None, None]
|
||||||
|
|
||||||
|
# Use expense account as fallback
|
||||||
|
if not round_off_account:
|
||||||
|
round_off_account = frappe.get_cached_value("Company", company, "default_expense_account")
|
||||||
|
|
||||||
meta = frappe.get_meta(voucher_type)
|
meta = frappe.get_meta(voucher_type)
|
||||||
|
|
||||||
# Give first preference to parent cost center for round off GLE
|
# Give first preference to parent cost center for round off GLE
|
||||||
|
@ -409,7 +409,7 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
|
|||||||
if (account and account_currency != existing_gle_currency) or not account:
|
if (account and account_currency != existing_gle_currency) or not account:
|
||||||
account = get_party_gle_account(party_type, party, company)
|
account = get_party_gle_account(party_type, party, company)
|
||||||
|
|
||||||
if include_advance and party_type in ["Customer", "Supplier"]:
|
if include_advance and party_type in ["Customer", "Supplier", "Student"]:
|
||||||
advance_account = get_party_advance_account(party_type, party, company)
|
advance_account = get_party_advance_account(party_type, party, company)
|
||||||
if advance_account:
|
if advance_account:
|
||||||
return [account, advance_account]
|
return [account, advance_account]
|
||||||
|
@ -37,24 +37,6 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "supplier",
|
|
||||||
"label": __("Supplier"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Supplier",
|
|
||||||
on_change: () => {
|
|
||||||
var supplier = frappe.query_report.get_filter_value('supplier');
|
|
||||||
if (supplier) {
|
|
||||||
frappe.db.get_value('Supplier', supplier, "tax_id", function(value) {
|
|
||||||
frappe.query_report.set_filter_value('tax_id', value["tax_id"]);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
frappe.query_report.set_filter_value('tax_id', "");
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.query_report.refresh();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "party_account",
|
"fieldname": "party_account",
|
||||||
"label": __("Payable Account"),
|
"label": __("Payable Account"),
|
||||||
@ -112,11 +94,35 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Payment Terms Template"
|
"options": "Payment Terms Template"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"party_type",
|
||||||
|
"label": __("Party Type"),
|
||||||
|
"fieldtype": "Autocomplete",
|
||||||
|
options: get_party_type_options(),
|
||||||
|
on_change: function() {
|
||||||
|
frappe.query_report.set_filter_value('party', "");
|
||||||
|
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"party",
|
||||||
|
"label": __("Party"),
|
||||||
|
"fieldtype": "MultiSelectList",
|
||||||
|
get_data: function(txt) {
|
||||||
|
if (!frappe.query_report.filters) return;
|
||||||
|
|
||||||
|
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||||
|
if (!party_type) return;
|
||||||
|
|
||||||
|
return frappe.db.get_link_options(party_type, txt);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "supplier_group",
|
"fieldname": "supplier_group",
|
||||||
"label": __("Supplier Group"),
|
"label": __("Supplier Group"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Supplier Group"
|
"options": "Supplier Group",
|
||||||
|
"hidden": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "group_by_party",
|
"fieldname": "group_by_party",
|
||||||
@ -133,12 +139,6 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
"label": __("Show Remarks"),
|
"label": __("Show Remarks"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "tax_id",
|
|
||||||
"label": __("Tax Id"),
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "show_future_payments",
|
"fieldname": "show_future_payments",
|
||||||
"label": __("Show Future Payments"),
|
"label": __("Show Future Payments"),
|
||||||
@ -164,3 +164,15 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
erpnext.utils.add_dimensions('Accounts Payable', 9);
|
erpnext.utils.add_dimensions('Accounts Payable', 9);
|
||||||
|
|
||||||
|
function get_party_type_options() {
|
||||||
|
let options = [];
|
||||||
|
frappe.db.get_list(
|
||||||
|
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
|
||||||
|
).then((res) => {
|
||||||
|
res.forEach((party_type) => {
|
||||||
|
options.push(party_type.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
from frappe.utils import add_days, flt, getdate, today
|
||||||
|
|
||||||
|
from erpnext import get_default_cost_center
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.create_company()
|
||||||
|
self.create_customer()
|
||||||
|
self.create_item()
|
||||||
|
self.create_supplier(currency="USD", supplier_name="Test Supplier2")
|
||||||
|
self.create_usd_payable_account()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def test_accounts_payable_for_foreign_currency_supplier(self):
|
||||||
|
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||||
|
pi.currency = "USD"
|
||||||
|
pi.conversion_rate = 80
|
||||||
|
pi.credit_to = self.creditors_usd
|
||||||
|
pi = pi.save().submit()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"party_type": "Supplier",
|
||||||
|
"party": [self.supplier],
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = execute(filters)
|
||||||
|
self.assertEqual(data[1][0].get("outstanding"), 300)
|
||||||
|
self.assertEqual(data[1][0].get("currency"), "USD")
|
||||||
|
|
||||||
|
def create_purchase_invoice(self, do_not_submit=False):
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
supplier=self.supplier,
|
||||||
|
is_return=False,
|
||||||
|
update_stock=False,
|
||||||
|
posting_date=frappe.utils.datetime.date(2021, 5, 1),
|
||||||
|
do_not_save=1,
|
||||||
|
rate=300,
|
||||||
|
price_list_rate=300,
|
||||||
|
qty=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
pi = pi.save()
|
||||||
|
if not do_not_submit:
|
||||||
|
pi = pi.submit()
|
||||||
|
return pi
|
@ -72,10 +72,27 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"supplier",
|
"fieldname":"party_type",
|
||||||
"label": __("Supplier"),
|
"label": __("Party Type"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Autocomplete",
|
||||||
"options": "Supplier"
|
options: get_party_type_options(),
|
||||||
|
on_change: function() {
|
||||||
|
frappe.query_report.set_filter_value('party', "");
|
||||||
|
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"party",
|
||||||
|
"label": __("Party"),
|
||||||
|
"fieldtype": "MultiSelectList",
|
||||||
|
get_data: function(txt) {
|
||||||
|
if (!frappe.query_report.filters) return;
|
||||||
|
|
||||||
|
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||||
|
if (!party_type) return;
|
||||||
|
|
||||||
|
return frappe.db.get_link_options(party_type, txt);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"payment_terms_template",
|
"fieldname":"payment_terms_template",
|
||||||
@ -105,3 +122,15 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
erpnext.utils.add_dimensions('Accounts Payable Summary', 9);
|
erpnext.utils.add_dimensions('Accounts Payable Summary', 9);
|
||||||
|
|
||||||
|
function get_party_type_options() {
|
||||||
|
let options = [];
|
||||||
|
frappe.db.get_list(
|
||||||
|
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
|
||||||
|
).then((res) => {
|
||||||
|
res.forEach((party_type) => {
|
||||||
|
options.push(party_type.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
frappe.provide("erpnext.utils");
|
||||||
|
|
||||||
frappe.query_reports["Accounts Receivable"] = {
|
frappe.query_reports["Accounts Receivable"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
{
|
{
|
||||||
@ -38,34 +40,28 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "customer",
|
"fieldname":"party_type",
|
||||||
"label": __("Customer"),
|
"label": __("Party Type"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Autocomplete",
|
||||||
"options": "Customer",
|
options: get_party_type_options(),
|
||||||
on_change: () => {
|
on_change: function() {
|
||||||
var customer = frappe.query_report.get_filter_value('customer');
|
frappe.query_report.set_filter_value('party', "");
|
||||||
var company = frappe.query_report.get_filter_value('company');
|
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
|
||||||
if (customer) {
|
|
||||||
frappe.db.get_value('Customer', customer, ["tax_id", "customer_name", "payment_terms"], function(value) {
|
|
||||||
frappe.query_report.set_filter_value('tax_id', value["tax_id"]);
|
|
||||||
frappe.query_report.set_filter_value('customer_name', value["customer_name"]);
|
|
||||||
frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
|
|
||||||
["credit_limit"], function(value) {
|
|
||||||
if (value) {
|
|
||||||
frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
|
|
||||||
}
|
|
||||||
}, "Customer");
|
|
||||||
} else {
|
|
||||||
frappe.query_report.set_filter_value('tax_id', "");
|
|
||||||
frappe.query_report.set_filter_value('customer_name', "");
|
|
||||||
frappe.query_report.set_filter_value('credit_limit', "");
|
|
||||||
frappe.query_report.set_filter_value('payment_terms', "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"party",
|
||||||
|
"label": __("Party"),
|
||||||
|
"fieldtype": "MultiSelectList",
|
||||||
|
get_data: function(txt) {
|
||||||
|
if (!frappe.query_report.filters) return;
|
||||||
|
|
||||||
|
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||||
|
if (!party_type) return;
|
||||||
|
|
||||||
|
return frappe.db.get_link_options(party_type, txt);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "party_account",
|
"fieldname": "party_account",
|
||||||
"label": __("Receivable Account"),
|
"label": __("Receivable Account"),
|
||||||
@ -172,34 +168,10 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
"label": __("Show Sales Person"),
|
"label": __("Show Sales Person"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "tax_id",
|
|
||||||
"label": __("Tax Id"),
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "show_remarks",
|
"fieldname": "show_remarks",
|
||||||
"label": __("Show Remarks"),
|
"label": __("Show Remarks"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "customer_name",
|
|
||||||
"label": __("Customer Name"),
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "payment_terms",
|
|
||||||
"label": __("Payment Tems"),
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "credit_limit",
|
|
||||||
"label": __("Credit Limit"),
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"hidden": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -221,3 +193,16 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
erpnext.utils.add_dimensions('Accounts Receivable', 9);
|
erpnext.utils.add_dimensions('Accounts Receivable', 9);
|
||||||
|
|
||||||
|
|
||||||
|
function get_party_type_options() {
|
||||||
|
let options = [];
|
||||||
|
frappe.db.get_list(
|
||||||
|
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
|
||||||
|
).then((res) => {
|
||||||
|
res.forEach((party_type) => {
|
||||||
|
options.push(party_type.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
@ -211,9 +211,8 @@ class ReceivablePayableReport(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||||
for party_type in self.party_type:
|
if self.filters.get("party_type") and self.filters.get("party"):
|
||||||
if self.filters.get(scrub(party_type)):
|
amount = ple.amount_in_account_currency
|
||||||
amount = ple.amount_in_account_currency
|
|
||||||
else:
|
else:
|
||||||
amount = ple.amount
|
amount = ple.amount
|
||||||
amount_in_account_currency = ple.amount_in_account_currency
|
amount_in_account_currency = ple.amount_in_account_currency
|
||||||
@ -426,10 +425,9 @@ class ReceivablePayableReport(object):
|
|||||||
# customer / supplier name
|
# customer / supplier name
|
||||||
party_details = self.get_party_details(row.party) or {}
|
party_details = self.get_party_details(row.party) or {}
|
||||||
row.update(party_details)
|
row.update(party_details)
|
||||||
for party_type in self.party_type:
|
|
||||||
if self.filters.get(scrub(party_type)):
|
if self.filters.get("party_type") and self.filters.get("party"):
|
||||||
row.currency = row.account_currency
|
row.currency = row.account_currency
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
row.currency = self.company_currency
|
row.currency = self.company_currency
|
||||||
|
|
||||||
@ -469,6 +467,10 @@ class ReceivablePayableReport(object):
|
|||||||
original_row = frappe._dict(row)
|
original_row = frappe._dict(row)
|
||||||
row.payment_terms = []
|
row.payment_terms = []
|
||||||
|
|
||||||
|
# Cr Note's don't have Payment Terms
|
||||||
|
if not payment_terms_details:
|
||||||
|
return
|
||||||
|
|
||||||
# Advance allocated during invoicing is not considered in payment terms
|
# Advance allocated during invoicing is not considered in payment terms
|
||||||
# Deduct that from paid amount pre allocation
|
# Deduct that from paid amount pre allocation
|
||||||
row.paid -= flt(payment_terms_details[0].total_advance)
|
row.paid -= flt(payment_terms_details[0].total_advance)
|
||||||
@ -765,16 +767,14 @@ class ReceivablePayableReport(object):
|
|||||||
def prepare_conditions(self):
|
def prepare_conditions(self):
|
||||||
self.qb_selection_filter = []
|
self.qb_selection_filter = []
|
||||||
self.or_filters = []
|
self.or_filters = []
|
||||||
|
|
||||||
for party_type in self.party_type:
|
for party_type in self.party_type:
|
||||||
party_type_field = scrub(party_type)
|
self.add_common_filters()
|
||||||
self.or_filters.append(self.ple.party_type == party_type)
|
|
||||||
|
|
||||||
self.add_common_filters(party_type_field=party_type_field)
|
if self.account_type == "Receivable":
|
||||||
|
|
||||||
if party_type_field == "customer":
|
|
||||||
self.add_customer_filters()
|
self.add_customer_filters()
|
||||||
|
|
||||||
elif party_type_field == "supplier":
|
elif self.account_type == "Payable":
|
||||||
self.add_supplier_filters()
|
self.add_supplier_filters()
|
||||||
|
|
||||||
if self.filters.cost_center:
|
if self.filters.cost_center:
|
||||||
@ -790,15 +790,18 @@ class ReceivablePayableReport(object):
|
|||||||
]
|
]
|
||||||
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||||
|
|
||||||
def add_common_filters(self, party_type_field):
|
def add_common_filters(self):
|
||||||
if self.filters.company:
|
if self.filters.company:
|
||||||
self.qb_selection_filter.append(self.ple.company == self.filters.company)
|
self.qb_selection_filter.append(self.ple.company == self.filters.company)
|
||||||
|
|
||||||
if self.filters.finance_book:
|
if self.filters.finance_book:
|
||||||
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
|
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
|
||||||
|
|
||||||
if self.filters.get(party_type_field):
|
if self.filters.get("party_type"):
|
||||||
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
|
self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type)
|
||||||
|
|
||||||
|
if self.filters.get("party"):
|
||||||
|
self.qb_selection_filter.append(self.ple.party.isin(self.filters.party))
|
||||||
|
|
||||||
if self.filters.party_account:
|
if self.filters.party_account:
|
||||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||||
@ -960,6 +963,20 @@ class ReceivablePayableReport(object):
|
|||||||
fieldtype="Link",
|
fieldtype="Link",
|
||||||
options="Contact",
|
options="Contact",
|
||||||
)
|
)
|
||||||
|
if self.filters.party_type == "Customer":
|
||||||
|
self.add_column(
|
||||||
|
_("Customer Name"),
|
||||||
|
fieldname="customer_name",
|
||||||
|
fieldtype="Link",
|
||||||
|
options="Customer",
|
||||||
|
)
|
||||||
|
elif self.filters.party_type == "Supplier":
|
||||||
|
self.add_column(
|
||||||
|
_("Supplier Name"),
|
||||||
|
fieldname="supplier_name",
|
||||||
|
fieldtype="Link",
|
||||||
|
options="Supplier",
|
||||||
|
)
|
||||||
|
|
||||||
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
|
||||||
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
|
||||||
|
@ -8,20 +8,17 @@ 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.payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
|
||||||
|
|
||||||
class TestAccountsReceivable(FrappeTestCase):
|
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
self.create_company()
|
||||||
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
self.create_customer()
|
||||||
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
|
self.create_item()
|
||||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
self.create_usd_receivable_account()
|
||||||
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
|
self.clear_old_entries()
|
||||||
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
|
|
||||||
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
|
|
||||||
|
|
||||||
self.create_usd_account()
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
@ -49,29 +46,84 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
debtors_usd.account_type = debtors.account_type
|
debtors_usd.account_type = debtors.account_type
|
||||||
self.debtors_usd = debtors_usd.save().name
|
self.debtors_usd = debtors_usd.save().name
|
||||||
|
|
||||||
|
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||||
|
frappe.set_user("Administrator")
|
||||||
|
si = create_sales_invoice(
|
||||||
|
item=self.item,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
posting_date=today(),
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
rate=100,
|
||||||
|
price_list_rate=100,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
if not no_payment_schedule:
|
||||||
|
si.append(
|
||||||
|
"payment_schedule",
|
||||||
|
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
||||||
|
)
|
||||||
|
si.append(
|
||||||
|
"payment_schedule",
|
||||||
|
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
|
||||||
|
)
|
||||||
|
si.append(
|
||||||
|
"payment_schedule",
|
||||||
|
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
||||||
|
)
|
||||||
|
si = si.save()
|
||||||
|
if not do_not_submit:
|
||||||
|
si = si.submit()
|
||||||
|
return si
|
||||||
|
|
||||||
|
def create_payment_entry(self, docname):
|
||||||
|
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
|
||||||
|
pe.paid_from = self.debit_to
|
||||||
|
pe.insert()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
def create_credit_note(self, docname):
|
||||||
|
credit_note = create_sales_invoice(
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
item=self.item,
|
||||||
|
qty=-1,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
is_return=1,
|
||||||
|
return_against=docname,
|
||||||
|
)
|
||||||
|
|
||||||
|
return credit_note
|
||||||
|
|
||||||
def test_accounts_receivable(self):
|
def test_accounts_receivable(self):
|
||||||
filters = {
|
filters = {
|
||||||
"company": "_Test Company 2",
|
"company": self.company,
|
||||||
"based_on_payment_terms": 1,
|
"based_on_payment_terms": 1,
|
||||||
"report_date": today(),
|
"report_date": today(),
|
||||||
"range1": 30,
|
"range1": 30,
|
||||||
"range2": 60,
|
"range2": 60,
|
||||||
"range3": 90,
|
"range3": 90,
|
||||||
"range4": 120,
|
"range4": 120,
|
||||||
|
"show_remarks": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||||
name = make_sales_invoice().name
|
si = self.create_sales_invoice()
|
||||||
|
name = si.name
|
||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data = [[100, 30], [100, 50], [100, 20]]
|
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
|
||||||
|
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
row = report[1][i - 1]
|
row = report[1][i - 1]
|
||||||
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
|
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||||
|
|
||||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||||
make_payment(name)
|
self.create_payment_entry(si.name)
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]
|
expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]
|
||||||
@ -84,10 +136,10 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||||
make_credit_note(name)
|
self.create_credit_note(si.name)
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"]
|
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||||
|
|
||||||
row = report[1][0]
|
row = report[1][0]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -108,21 +160,20 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
so = make_sales_order(
|
so = make_sales_order(
|
||||||
company="_Test Company 2",
|
company=self.company,
|
||||||
customer="_Test Customer 2",
|
customer=self.customer,
|
||||||
warehouse="Finished Goods - _TC2",
|
warehouse=self.warehouse,
|
||||||
currency="EUR",
|
debit_to=self.debit_to,
|
||||||
debit_to="Debtors - _TC2",
|
income_account=self.income_account,
|
||||||
income_account="Sales - _TC2",
|
expense_account=self.expense_account,
|
||||||
expense_account="Cost of Goods Sold - _TC2",
|
cost_center=self.cost_center,
|
||||||
cost_center="Main - _TC2",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
pe = get_payment_entry(so.doctype, so.name)
|
pe = get_payment_entry(so.doctype, so.name)
|
||||||
pe = pe.save().submit()
|
pe = pe.save().submit()
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
"company": "_Test Company 2",
|
"company": self.company,
|
||||||
"based_on_payment_terms": 0,
|
"based_on_payment_terms": 0,
|
||||||
"report_date": today(),
|
"report_date": today(),
|
||||||
"range1": 30,
|
"range1": 30,
|
||||||
@ -147,34 +198,32 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@change_settings(
|
@change_settings(
|
||||||
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
|
"Accounts Settings",
|
||||||
|
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
|
||||||
)
|
)
|
||||||
def test_exchange_revaluation_for_party(self):
|
def test_exchange_revaluation_for_party(self):
|
||||||
"""
|
"""
|
||||||
Exchange Revaluation for party on Receivable/Payable shoule be included
|
Exchange Revaluation for party on Receivable/Payable should be included
|
||||||
"""
|
"""
|
||||||
|
|
||||||
company = "_Test Company 2"
|
|
||||||
customer = "_Test Customer 2"
|
|
||||||
|
|
||||||
# Using Exchange Gain/Loss account for unrealized as well.
|
# Using Exchange Gain/Loss account for unrealized as well.
|
||||||
company_doc = frappe.get_doc("Company", company)
|
company_doc = frappe.get_doc("Company", self.company)
|
||||||
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
|
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
|
||||||
company_doc.save()
|
company_doc.save()
|
||||||
|
|
||||||
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||||
si.currency = "USD"
|
si.currency = "USD"
|
||||||
si.conversion_rate = 0.90
|
si.conversion_rate = 80
|
||||||
si.debit_to = self.debtors_usd
|
si.debit_to = self.debtors_usd
|
||||||
si = si.save().submit()
|
si = si.save().submit()
|
||||||
|
|
||||||
# Exchange Revaluation
|
# Exchange Revaluation
|
||||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||||
err.company = company
|
err.company = self.company
|
||||||
err.posting_date = today()
|
err.posting_date = today()
|
||||||
accounts = err.get_accounts_data()
|
accounts = err.get_accounts_data()
|
||||||
err.extend("accounts", accounts)
|
err.extend("accounts", accounts)
|
||||||
err.accounts[0].new_exchange_rate = 0.95
|
err.accounts[0].new_exchange_rate = 85
|
||||||
row = err.accounts[0]
|
row = err.accounts[0]
|
||||||
row.new_balance_in_base_currency = flt(
|
row.new_balance_in_base_currency = flt(
|
||||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||||
@ -189,7 +238,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
je = je.submit()
|
je = je.submit()
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
"company": company,
|
"company": self.company,
|
||||||
"report_date": today(),
|
"report_date": today(),
|
||||||
"range1": 30,
|
"range1": 30,
|
||||||
"range2": 60,
|
"range2": 60,
|
||||||
@ -198,7 +247,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data_for_err = [0, -5, 0, 5]
|
expected_data_for_err = [0, -500, 0, 500]
|
||||||
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
|
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
expected_data_for_err,
|
expected_data_for_err,
|
||||||
@ -214,46 +263,43 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
"""
|
"""
|
||||||
Payment against credit/debit note should be considered against the parent invoice
|
Payment against credit/debit note should be considered against the parent invoice
|
||||||
"""
|
"""
|
||||||
company = "_Test Company 2"
|
|
||||||
customer = "_Test Customer 2"
|
|
||||||
|
|
||||||
si1 = make_sales_invoice()
|
si1 = self.create_sales_invoice()
|
||||||
|
|
||||||
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
|
pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash)
|
||||||
pe.paid_from = "Debtors - _TC2"
|
pe.paid_from = self.debit_to
|
||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
pe.submit()
|
||||||
|
|
||||||
cr_note = make_credit_note(si1.name)
|
cr_note = self.create_credit_note(si1.name)
|
||||||
|
|
||||||
si2 = make_sales_invoice()
|
si2 = self.create_sales_invoice()
|
||||||
|
|
||||||
# manually link cr_note with si2 using journal entry
|
# manually link cr_note with si2 using journal entry
|
||||||
je = frappe.new_doc("Journal Entry")
|
je = frappe.new_doc("Journal Entry")
|
||||||
je.company = company
|
je.company = self.company
|
||||||
je.voucher_type = "Credit Note"
|
je.voucher_type = "Credit Note"
|
||||||
je.posting_date = today()
|
je.posting_date = today()
|
||||||
|
|
||||||
debit_account = "Debtors - _TC2"
|
|
||||||
debit_entry = {
|
debit_entry = {
|
||||||
"account": debit_account,
|
"account": self.debit_to,
|
||||||
"party_type": "Customer",
|
"party_type": "Customer",
|
||||||
"party": customer,
|
"party": self.customer,
|
||||||
"debit": 100,
|
"debit": 100,
|
||||||
"debit_in_account_currency": 100,
|
"debit_in_account_currency": 100,
|
||||||
"reference_type": cr_note.doctype,
|
"reference_type": cr_note.doctype,
|
||||||
"reference_name": cr_note.name,
|
"reference_name": cr_note.name,
|
||||||
"cost_center": "Main - _TC2",
|
"cost_center": self.cost_center,
|
||||||
}
|
}
|
||||||
credit_entry = {
|
credit_entry = {
|
||||||
"account": debit_account,
|
"account": self.debit_to,
|
||||||
"party_type": "Customer",
|
"party_type": "Customer",
|
||||||
"party": customer,
|
"party": self.customer,
|
||||||
"credit": 100,
|
"credit": 100,
|
||||||
"credit_in_account_currency": 100,
|
"credit_in_account_currency": 100,
|
||||||
"reference_type": si2.doctype,
|
"reference_type": si2.doctype,
|
||||||
"reference_name": si2.name,
|
"reference_name": si2.name,
|
||||||
"cost_center": "Main - _TC2",
|
"cost_center": self.cost_center,
|
||||||
}
|
}
|
||||||
|
|
||||||
je.append("accounts", debit_entry)
|
je.append("accounts", debit_entry)
|
||||||
@ -261,7 +307,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
je = je.save().submit()
|
je = je.save().submit()
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
"company": company,
|
"company": self.company,
|
||||||
"report_date": today(),
|
"report_date": today(),
|
||||||
"range1": 30,
|
"range1": 30,
|
||||||
"range2": 60,
|
"range2": 60,
|
||||||
@ -271,64 +317,329 @@ class TestAccountsReceivable(FrappeTestCase):
|
|||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
self.assertEqual(report[1], [])
|
self.assertEqual(report[1], [])
|
||||||
|
|
||||||
|
def test_group_by_party(self):
|
||||||
|
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si1.posting_date = add_days(today(), -1)
|
||||||
|
si1.save().submit()
|
||||||
|
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si2.items[0].rate = 85
|
||||||
|
si2.save().submit()
|
||||||
|
|
||||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
filters = {
|
||||||
frappe.set_user("Administrator")
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
"group_by_party": True,
|
||||||
|
}
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 5)
|
||||||
|
|
||||||
si = create_sales_invoice(
|
# assert voucher rows
|
||||||
company="_Test Company 2",
|
expected_voucher_rows = [
|
||||||
customer="_Test Customer 2",
|
[100.0, 100.0, 100.0, 100.0],
|
||||||
currency="EUR",
|
[85.0, 85.0, 85.0, 85.0],
|
||||||
warehouse="Finished Goods - _TC2",
|
]
|
||||||
debit_to="Debtors - _TC2",
|
voucher_rows = []
|
||||||
income_account="Sales - _TC2",
|
for x in report[0:2]:
|
||||||
expense_account="Cost of Goods Sold - _TC2",
|
voucher_rows.append(
|
||||||
cost_center="Main - _TC2",
|
[x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency]
|
||||||
do_not_save=1,
|
)
|
||||||
)
|
self.assertEqual(expected_voucher_rows, voucher_rows)
|
||||||
|
|
||||||
if not no_payment_schedule:
|
# assert total rows
|
||||||
si.append(
|
expected_total_rows = [
|
||||||
"payment_schedule",
|
[self.customer, 185.0, 185.0], # party total
|
||||||
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
|
{}, # empty row for padding
|
||||||
|
["Total", 185.0, 185.0], # grand total
|
||||||
|
]
|
||||||
|
party_total_row = report[2]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_total_rows[0],
|
||||||
|
[
|
||||||
|
party_total_row.get("party"),
|
||||||
|
party_total_row.get("invoiced"),
|
||||||
|
party_total_row.get("outstanding"),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
si.append(
|
empty_row = report[3]
|
||||||
"payment_schedule",
|
self.assertEqual(expected_total_rows[1], empty_row)
|
||||||
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
|
grand_total_row = report[4]
|
||||||
)
|
self.assertEqual(
|
||||||
si.append(
|
expected_total_rows[2],
|
||||||
"payment_schedule",
|
[
|
||||||
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
|
grand_total_row.get("party"),
|
||||||
|
grand_total_row.get("invoiced"),
|
||||||
|
grand_total_row.get("outstanding"),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
si = si.save()
|
def test_future_payments(self):
|
||||||
|
si = self.create_sales_invoice()
|
||||||
|
pe = get_payment_entry(si.doctype, si.name)
|
||||||
|
pe.posting_date = add_days(today(), 1)
|
||||||
|
pe.paid_amount = 90.0
|
||||||
|
pe.references[0].allocated_amount = 90.0
|
||||||
|
pe.save().submit()
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
"show_future_payments": True,
|
||||||
|
}
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
|
||||||
if not do_not_submit:
|
expected_data = [100.0, 100.0, 10.0, 90.0]
|
||||||
si = si.submit()
|
|
||||||
|
|
||||||
return si
|
row = report[0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||||
|
)
|
||||||
|
|
||||||
|
pe.cancel()
|
||||||
|
# full payment in future date
|
||||||
|
pe = get_payment_entry(si.doctype, si.name)
|
||||||
|
pe.posting_date = add_days(today(), 1)
|
||||||
|
pe.save().submit()
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
expected_data = [100.0, 100.0, 0.0, 100.0]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||||
|
)
|
||||||
|
|
||||||
def make_payment(docname):
|
pe.cancel()
|
||||||
pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40)
|
# over payment in future date
|
||||||
pe.paid_from = "Debtors - _TC2"
|
pe = get_payment_entry(si.doctype, si.name)
|
||||||
pe.insert()
|
pe.posting_date = add_days(today(), 1)
|
||||||
pe.submit()
|
pe.paid_amount = 110
|
||||||
|
pe.save().submit()
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 2)
|
||||||
|
expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]]
|
||||||
|
for idx, row in enumerate(report):
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data[idx],
|
||||||
|
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sales_person(self):
|
||||||
|
sales_person = (
|
||||||
|
frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True})
|
||||||
|
.insert()
|
||||||
|
.submit()
|
||||||
|
)
|
||||||
|
si = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
def make_credit_note(docname):
|
filters = {
|
||||||
credit_note = create_sales_invoice(
|
"company": self.company,
|
||||||
company="_Test Company 2",
|
"report_date": today(),
|
||||||
customer="_Test Customer 2",
|
"range1": 30,
|
||||||
currency="EUR",
|
"range2": 60,
|
||||||
qty=-1,
|
"range3": 90,
|
||||||
warehouse="Finished Goods - _TC2",
|
"range4": 120,
|
||||||
debit_to="Debtors - _TC2",
|
"sales_person": sales_person.name,
|
||||||
income_account="Sales - _TC2",
|
"show_sales_person": True,
|
||||||
expense_account="Cost of Goods Sold - _TC2",
|
}
|
||||||
cost_center="Main - _TC2",
|
report = execute(filters)[1]
|
||||||
is_return=1,
|
self.assertEqual(len(report), 1)
|
||||||
return_against=docname,
|
|
||||||
)
|
|
||||||
|
|
||||||
return credit_note
|
expected_data = [100.0, 100.0, sales_person.name]
|
||||||
|
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person])
|
||||||
|
|
||||||
|
def test_cost_center_filter(self):
|
||||||
|
si = self.create_sales_invoice()
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
|
}
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
expected_data = [100.0, 100.0, self.cost_center]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center])
|
||||||
|
|
||||||
|
def test_customer_group_filter(self):
|
||||||
|
si = self.create_sales_invoice()
|
||||||
|
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
"customer_group": cus_group,
|
||||||
|
}
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
expected_data = [100.0, 100.0, cus_group]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group])
|
||||||
|
|
||||||
|
filters.update({"customer_group": "Individual"})
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 0)
|
||||||
|
|
||||||
|
def test_party_account_filter(self):
|
||||||
|
si1 = self.create_sales_invoice()
|
||||||
|
self.customer2 = (
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Customer",
|
||||||
|
"customer_name": "Jane Doe",
|
||||||
|
"type": "Individual",
|
||||||
|
"default_currency": "USD",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.insert()
|
||||||
|
.submit()
|
||||||
|
)
|
||||||
|
|
||||||
|
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si2.posting_date = add_days(today(), -1)
|
||||||
|
si2.customer = self.customer2
|
||||||
|
si2.currency = "USD"
|
||||||
|
si2.conversion_rate = 80
|
||||||
|
si2.debit_to = self.debtors_usd
|
||||||
|
si2.save().submit()
|
||||||
|
|
||||||
|
# Filter on company currency receivable account
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
"party_account": self.debit_to,
|
||||||
|
}
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
expected_data = [100.0, 100.0, self.debit_to, si1.currency]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filter on USD receivable account
|
||||||
|
filters.update({"party_account": self.debtors_usd})
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
|
||||||
|
)
|
||||||
|
|
||||||
|
# without filter on party account
|
||||||
|
filters.pop("party_account")
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 2)
|
||||||
|
expected_data = [
|
||||||
|
[8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency],
|
||||||
|
[100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency],
|
||||||
|
]
|
||||||
|
for idx, row in enumerate(report):
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data[idx],
|
||||||
|
[
|
||||||
|
row.invoiced,
|
||||||
|
row.outstanding,
|
||||||
|
row.invoiced_in_account_currency,
|
||||||
|
row.outstanding_in_account_currency,
|
||||||
|
row.party_account,
|
||||||
|
row.account_currency,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_usd_customer_filter(self):
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": [self.customer],
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
}
|
||||||
|
|
||||||
|
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||||
|
si.currency = "USD"
|
||||||
|
si.conversion_rate = 80
|
||||||
|
si.debit_to = self.debtors_usd
|
||||||
|
si.save().submit()
|
||||||
|
name = si.name
|
||||||
|
|
||||||
|
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"voucher_type": si.doctype,
|
||||||
|
"voucher_no": si.name,
|
||||||
|
"party_account": self.debtors_usd,
|
||||||
|
"customer_name": self.customer,
|
||||||
|
"invoiced": 100.0,
|
||||||
|
"outstanding": 100.0,
|
||||||
|
"account_currency": "USD",
|
||||||
|
}
|
||||||
|
self.assertEqual(len(report[1]), 1)
|
||||||
|
report_output = report[1][0]
|
||||||
|
for field in expected:
|
||||||
|
with self.subTest(field=field):
|
||||||
|
self.assertEqual(report_output.get(field), expected.get(field))
|
||||||
|
|
||||||
|
def test_multi_select_party_filter(self):
|
||||||
|
self.customer1 = self.customer
|
||||||
|
self.create_customer("_Test Customer 2")
|
||||||
|
self.customer2 = self.customer
|
||||||
|
self.create_customer("_Test Customer 3")
|
||||||
|
self.customer3 = self.customer
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": [self.customer1, self.customer3],
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
}
|
||||||
|
|
||||||
|
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||||
|
si1.customer = self.customer1
|
||||||
|
si1.save().submit()
|
||||||
|
|
||||||
|
si2 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||||
|
si2.customer = self.customer2
|
||||||
|
si2.save().submit()
|
||||||
|
|
||||||
|
si3 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||||
|
si3.customer = self.customer3
|
||||||
|
si3.save().submit()
|
||||||
|
|
||||||
|
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||||
|
report = execute(filters)
|
||||||
|
|
||||||
|
expected_output = {self.customer1, self.customer3}
|
||||||
|
self.assertEqual(len(report[1]), 2)
|
||||||
|
output_for = set([x.party for x in report[1]])
|
||||||
|
self.assertEqual(output_for, expected_output)
|
||||||
|
@ -72,10 +72,27 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"customer",
|
"fieldname":"party_type",
|
||||||
"label": __("Customer"),
|
"label": __("Party Type"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Autocomplete",
|
||||||
"options": "Customer"
|
options: get_party_type_options(),
|
||||||
|
on_change: function() {
|
||||||
|
frappe.query_report.set_filter_value('party', "");
|
||||||
|
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"party",
|
||||||
|
"label": __("Party"),
|
||||||
|
"fieldtype": "MultiSelectList",
|
||||||
|
get_data: function(txt) {
|
||||||
|
if (!frappe.query_report.filters) return;
|
||||||
|
|
||||||
|
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||||
|
if (!party_type) return;
|
||||||
|
|
||||||
|
return frappe.db.get_link_options(party_type, txt);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"customer_group",
|
"fieldname":"customer_group",
|
||||||
@ -133,3 +150,15 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
erpnext.utils.add_dimensions('Accounts Receivable Summary', 9);
|
erpnext.utils.add_dimensions('Accounts Receivable Summary', 9);
|
||||||
|
|
||||||
|
function get_party_type_options() {
|
||||||
|
let options = [];
|
||||||
|
frappe.db.get_list(
|
||||||
|
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
|
||||||
|
).then((res) => {
|
||||||
|
res.forEach((party_type) => {
|
||||||
|
options.push(party_type.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
@ -99,13 +99,11 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
# Add all amount columns
|
# Add all amount columns
|
||||||
for k in list(self.party_total[d.party]):
|
for k in list(self.party_total[d.party]):
|
||||||
if k not in ["currency", "sales_person"]:
|
if isinstance(self.party_total[d.party][k], float):
|
||||||
|
self.party_total[d.party][k] += d.get(k) or 0.0
|
||||||
self.party_total[d.party][k] += d.get(k, 0.0)
|
|
||||||
|
|
||||||
# set territory, customer_group, sales person etc
|
# set territory, customer_group, sales person etc
|
||||||
self.set_party_details(d)
|
self.set_party_details(d)
|
||||||
self.party_total[d.party].update({"party_type": d.party_type})
|
|
||||||
|
|
||||||
def init_party_total(self, row):
|
def init_party_total(self, row):
|
||||||
self.party_total.setdefault(
|
self.party_total.setdefault(
|
||||||
@ -124,6 +122,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
"total_due": 0.0,
|
"total_due": 0.0,
|
||||||
"future_amount": 0.0,
|
"future_amount": 0.0,
|
||||||
"sales_person": [],
|
"sales_person": [],
|
||||||
|
"party_type": row.party_type,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -133,13 +132,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
|
|
||||||
for key in ("territory", "customer_group", "supplier_group"):
|
for key in ("territory", "customer_group", "supplier_group"):
|
||||||
if row.get(key):
|
if row.get(key):
|
||||||
self.party_total[row.party][key] = row.get(key)
|
self.party_total[row.party][key] = row.get(key, "")
|
||||||
|
|
||||||
if row.sales_person:
|
if row.sales_person:
|
||||||
self.party_total[row.party].sales_person.append(row.sales_person)
|
self.party_total[row.party].sales_person.append(row.get("sales_person", ""))
|
||||||
|
|
||||||
if self.filters.sales_partner:
|
if self.filters.sales_partner:
|
||||||
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner")
|
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner", "")
|
||||||
|
|
||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
self.columns = []
|
self.columns = []
|
||||||
|
@ -58,6 +58,9 @@ def get_data(filters):
|
|||||||
|
|
||||||
|
|
||||||
def get_asset_categories(filters):
|
def get_asset_categories(filters):
|
||||||
|
condition = ""
|
||||||
|
if filters.get("asset_category"):
|
||||||
|
condition += " and asset_category = %(asset_category)s"
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
SELECT asset_category,
|
SELECT asset_category,
|
||||||
@ -98,15 +101,25 @@ def get_asset_categories(filters):
|
|||||||
0
|
0
|
||||||
end), 0) as cost_of_scrapped_asset
|
end), 0) as cost_of_scrapped_asset
|
||||||
from `tabAsset`
|
from `tabAsset`
|
||||||
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
|
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
|
||||||
group by asset_category
|
group by asset_category
|
||||||
""",
|
""".format(
|
||||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
condition
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"to_date": filters.to_date,
|
||||||
|
"from_date": filters.from_date,
|
||||||
|
"company": filters.company,
|
||||||
|
"asset_category": filters.get("asset_category"),
|
||||||
|
},
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_assets(filters):
|
def get_assets(filters):
|
||||||
|
condition = ""
|
||||||
|
if filters.get("asset_category"):
|
||||||
|
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
SELECT results.asset_category,
|
SELECT results.asset_category,
|
||||||
@ -138,7 +151,7 @@ def get_assets(filters):
|
|||||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||||
join `tabCompany` company on
|
join `tabCompany` company on
|
||||||
company.name = %(company)s
|
company.name = %(company)s
|
||||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
|
||||||
group by a.asset_category
|
group by a.asset_category
|
||||||
union
|
union
|
||||||
SELECT a.asset_category,
|
SELECT a.asset_category,
|
||||||
@ -154,10 +167,12 @@ def get_assets(filters):
|
|||||||
end), 0) as depreciation_eliminated_during_the_period,
|
end), 0) as depreciation_eliminated_during_the_period,
|
||||||
0 as depreciation_amount_during_the_period
|
0 as depreciation_amount_during_the_period
|
||||||
from `tabAsset` a
|
from `tabAsset` a
|
||||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
|
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
|
||||||
group by a.asset_category) as results
|
group by a.asset_category) as results
|
||||||
group by results.asset_category
|
group by results.asset_category
|
||||||
""",
|
""".format(
|
||||||
|
condition
|
||||||
|
),
|
||||||
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
@ -1,26 +1,23 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
frappe.require("assets/erpnext/js/financial_statements.js", function () {
|
frappe.query_reports["Balance Sheet"] = $.extend(
|
||||||
frappe.query_reports["Balance Sheet"] = $.extend(
|
{},
|
||||||
{},
|
erpnext.financial_statements
|
||||||
erpnext.financial_statements
|
);
|
||||||
);
|
|
||||||
|
|
||||||
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
||||||
|
|
||||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||||
fieldname: "accumulated_values",
|
fieldname: "accumulated_values",
|
||||||
label: __("Accumulated Values"),
|
label: __("Accumulated Values"),
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
default: 1,
|
default: 1,
|
||||||
});
|
});
|
||||||
console.log(frappe.query_reports["Balance Sheet"]["filters"]);
|
|
||||||
|
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
fieldname: "include_default_book_entries",
|
||||||
fieldname: "include_default_book_entries",
|
label: __("Include Default Book Entries"),
|
||||||
label: __("Include Default Book Entries"),
|
fieldtype: "Check",
|
||||||
fieldtype: "Check",
|
default: 1,
|
||||||
default: 1,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
// Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
frappe.query_reports["Cash Flow"] = $.extend(
|
||||||
frappe.query_reports["Cash Flow"] = $.extend({},
|
{},
|
||||||
erpnext.financial_statements);
|
erpnext.financial_statements
|
||||||
|
);
|
||||||
|
|
||||||
erpnext.utils.add_dimensions('Cash Flow', 10);
|
erpnext.utils.add_dimensions('Cash Flow', 10);
|
||||||
|
|
||||||
// The last item in the array is the definition for Presentation Currency
|
// The last item in the array is the definition for Presentation Currency
|
||||||
// filter. It won't be used in cash flow for now so we pop it. Please take
|
// filter. It won't be used in cash flow for now so we pop it. Please take
|
||||||
// of this if you are working here.
|
// of this if you are working here.
|
||||||
|
|
||||||
frappe.query_reports["Cash Flow"]["filters"].splice(8, 1);
|
frappe.query_reports["Cash Flow"]["filters"].splice(8, 1);
|
||||||
|
|
||||||
frappe.query_reports["Cash Flow"]["filters"].push(
|
frappe.query_reports["Cash Flow"]["filters"].push(
|
||||||
{
|
{
|
||||||
"fieldname": "include_default_book_entries",
|
"fieldname": "include_default_book_entries",
|
||||||
"label": __("Include Default Book Entries"),
|
"label": __("Include Default Book Entries"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"default": 1
|
"default": 1
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
@ -2,152 +2,150 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
frappe.query_reports["Consolidated Financial Statement"] = {
|
||||||
frappe.query_reports["Consolidated Financial Statement"] = {
|
"filters": [
|
||||||
"filters": [
|
{
|
||||||
{
|
"fieldname":"company",
|
||||||
"fieldname":"company",
|
"label": __("Company"),
|
||||||
"label": __("Company"),
|
"fieldtype": "Link",
|
||||||
"fieldtype": "Link",
|
"options": "Company",
|
||||||
"options": "Company",
|
"default": frappe.defaults.get_user_default("Company"),
|
||||||
"default": frappe.defaults.get_user_default("Company"),
|
"reqd": 1
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"filter_based_on",
|
|
||||||
"label": __("Filter Based On"),
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"options": ["Fiscal Year", "Date Range"],
|
|
||||||
"default": ["Fiscal Year"],
|
|
||||||
"reqd": 1,
|
|
||||||
on_change: function() {
|
|
||||||
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
|
|
||||||
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
|
|
||||||
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
|
|
||||||
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
|
|
||||||
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
|
|
||||||
|
|
||||||
frappe.query_report.refresh();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"period_start_date",
|
|
||||||
"label": __("Start Date"),
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"hidden": 1,
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"period_end_date",
|
|
||||||
"label": __("End Date"),
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"hidden": 1,
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"from_fiscal_year",
|
|
||||||
"label": __("Start Year"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Fiscal Year",
|
|
||||||
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
|
||||||
"reqd": 1,
|
|
||||||
on_change: () => {
|
|
||||||
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) {
|
|
||||||
let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date");
|
|
||||||
frappe.query_report.set_filter_value({
|
|
||||||
period_start_date: year_start_date
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"to_fiscal_year",
|
|
||||||
"label": __("End Year"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Fiscal Year",
|
|
||||||
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
|
||||||
"reqd": 1,
|
|
||||||
on_change: () => {
|
|
||||||
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) {
|
|
||||||
let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date");
|
|
||||||
frappe.query_report.set_filter_value({
|
|
||||||
period_end_date: year_end_date
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"finance_book",
|
|
||||||
"label": __("Finance Book"),
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"options": "Finance Book"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"report",
|
|
||||||
"label": __("Report"),
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"options": ["Profit and Loss Statement", "Balance Sheet", "Cash Flow"],
|
|
||||||
"default": "Balance Sheet",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "presentation_currency",
|
|
||||||
"label": __("Currency"),
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"options": erpnext.get_presentation_currency_list(),
|
|
||||||
"default": frappe.defaults.get_user_default("Currency")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname":"accumulated_in_group_company",
|
|
||||||
"label": __("Accumulated Values in Group Company"),
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "include_default_book_entries",
|
|
||||||
"label": __("Include Default Book Entries"),
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"default": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "show_zero_values",
|
|
||||||
"label": __("Show zero values"),
|
|
||||||
"fieldtype": "Check"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"formatter": function(value, row, column, data, default_formatter) {
|
|
||||||
if (data && column.fieldname=="account") {
|
|
||||||
value = data.account_name || value;
|
|
||||||
|
|
||||||
column.link_onclick =
|
|
||||||
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
|
||||||
column.is_tree = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data && data.account && column.apply_currency_formatter) {
|
|
||||||
data.currency = erpnext.get_currency(column.company_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
value = default_formatter(value, row, column, data);
|
|
||||||
if (!data.parent_account) {
|
|
||||||
value = $(`<span>${value}</span>`);
|
|
||||||
|
|
||||||
var $value = $(value).css("font-weight", "bold");
|
|
||||||
|
|
||||||
value = $value.wrap("<p></p>").parent().html();
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
},
|
||||||
onload: function() {
|
{
|
||||||
let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
|
"fieldname":"filter_based_on",
|
||||||
|
"label": __("Filter Based On"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": ["Fiscal Year", "Date Range"],
|
||||||
|
"default": ["Fiscal Year"],
|
||||||
|
"reqd": 1,
|
||||||
|
on_change: function() {
|
||||||
|
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
|
||||||
|
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
|
||||||
|
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
|
||||||
|
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
|
||||||
|
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
|
||||||
|
|
||||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
frappe.query_report.refresh();
|
||||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
}
|
||||||
frappe.query_report.set_filter_value({
|
},
|
||||||
period_start_date: fy.year_start_date,
|
{
|
||||||
period_end_date: fy.year_end_date
|
"fieldname":"period_start_date",
|
||||||
|
"label": __("Start Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"hidden": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"period_end_date",
|
||||||
|
"label": __("End Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"hidden": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"from_fiscal_year",
|
||||||
|
"label": __("Start Year"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Fiscal Year",
|
||||||
|
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||||
|
"reqd": 1,
|
||||||
|
on_change: () => {
|
||||||
|
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) {
|
||||||
|
let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date");
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
period_start_date: year_start_date
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"to_fiscal_year",
|
||||||
|
"label": __("End Year"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Fiscal Year",
|
||||||
|
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||||
|
"reqd": 1,
|
||||||
|
on_change: () => {
|
||||||
|
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) {
|
||||||
|
let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date");
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
period_end_date: year_end_date
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"finance_book",
|
||||||
|
"label": __("Finance Book"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Finance Book"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"report",
|
||||||
|
"label": __("Report"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": ["Profit and Loss Statement", "Balance Sheet", "Cash Flow"],
|
||||||
|
"default": "Balance Sheet",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "presentation_currency",
|
||||||
|
"label": __("Currency"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": erpnext.get_presentation_currency_list(),
|
||||||
|
"default": frappe.defaults.get_user_default("Currency")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"accumulated_in_group_company",
|
||||||
|
"label": __("Accumulated Values in Group Company"),
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "include_default_book_entries",
|
||||||
|
"label": __("Include Default Book Entries"),
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "show_zero_values",
|
||||||
|
"label": __("Show zero values"),
|
||||||
|
"fieldtype": "Check"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"formatter": function(value, row, column, data, default_formatter) {
|
||||||
|
if (data && column.fieldname=="account") {
|
||||||
|
value = data.account_name || value;
|
||||||
|
|
||||||
|
column.link_onclick =
|
||||||
|
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
||||||
|
column.is_tree = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.account && column.apply_currency_formatter) {
|
||||||
|
data.currency = erpnext.get_currency(column.company_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
value = default_formatter(value, row, column, data);
|
||||||
|
if (!data.parent_account) {
|
||||||
|
value = $(`<span>${value}</span>`);
|
||||||
|
|
||||||
|
var $value = $(value).css("font-weight", "bold");
|
||||||
|
|
||||||
|
value = $value.wrap("<p></p>").parent().html();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
onload: function() {
|
||||||
|
let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
|
||||||
|
|
||||||
|
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||||
|
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
period_start_date: fy.year_start_date,
|
||||||
|
period_end_date: fy.year_end_date
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
@ -81,7 +81,7 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
|||||||
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
|
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
|
||||||
item = frappe.get_doc("Item", self.item)
|
item = frappe.get_doc("Item", self.item)
|
||||||
item.enable_deferred_revenue = 1
|
item.enable_deferred_revenue = 1
|
||||||
item.deferred_revenue_account = self.deferred_revenue_account
|
item.item_defaults[0].deferred_revenue_account = self.deferred_revenue_account
|
||||||
item.no_of_months = 3
|
item.no_of_months = 3
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
|||||||
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||||
item = frappe.get_doc("Item", self.item)
|
item = frappe.get_doc("Item", self.item)
|
||||||
item.enable_deferred_expense = 1
|
item.enable_deferred_expense = 1
|
||||||
item.deferred_expense_account = self.deferred_expense_account
|
item.item_defaults[0].deferred_expense_account = self.deferred_expense_account
|
||||||
item.no_of_months_exp = 3
|
item.no_of_months_exp = 3
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
|
@ -2,83 +2,81 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
frappe.query_reports["Dimension-wise Accounts Balance Report"] = {
|
||||||
frappe.query_reports["Dimension-wise Accounts Balance Report"] = {
|
"filters": [
|
||||||
"filters": [
|
{
|
||||||
{
|
"fieldname": "company",
|
||||||
"fieldname": "company",
|
"label": __("Company"),
|
||||||
"label": __("Company"),
|
"fieldtype": "Link",
|
||||||
"fieldtype": "Link",
|
"options": "Company",
|
||||||
"options": "Company",
|
"default": frappe.defaults.get_user_default("Company"),
|
||||||
"default": frappe.defaults.get_user_default("Company"),
|
"reqd": 1
|
||||||
"reqd": 1
|
},
|
||||||
},
|
{
|
||||||
{
|
"fieldname": "fiscal_year",
|
||||||
"fieldname": "fiscal_year",
|
"label": __("Fiscal Year"),
|
||||||
"label": __("Fiscal Year"),
|
"fieldtype": "Link",
|
||||||
"fieldtype": "Link",
|
"options": "Fiscal Year",
|
||||||
"options": "Fiscal Year",
|
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||||
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
"reqd": 1,
|
||||||
"reqd": 1,
|
"on_change": function(query_report) {
|
||||||
"on_change": function(query_report) {
|
var fiscal_year = query_report.get_values().fiscal_year;
|
||||||
var fiscal_year = query_report.get_values().fiscal_year;
|
if (!fiscal_year) {
|
||||||
if (!fiscal_year) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
|
||||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
|
||||||
frappe.query_report.set_filter_value({
|
|
||||||
from_date: fy.year_start_date,
|
|
||||||
to_date: fy.year_end_date
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||||
{
|
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||||
"fieldname": "from_date",
|
frappe.query_report.set_filter_value({
|
||||||
"label": __("From Date"),
|
from_date: fy.year_start_date,
|
||||||
"fieldtype": "Date",
|
to_date: fy.year_end_date
|
||||||
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
});
|
||||||
"reqd": 1
|
});
|
||||||
},
|
}
|
||||||
{
|
},
|
||||||
"fieldname": "to_date",
|
{
|
||||||
"label": __("To Date"),
|
"fieldname": "from_date",
|
||||||
"fieldtype": "Date",
|
"label": __("From Date"),
|
||||||
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
"fieldtype": "Date",
|
||||||
"reqd": 1
|
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||||
},
|
"reqd": 1
|
||||||
{
|
},
|
||||||
"fieldname": "finance_book",
|
{
|
||||||
"label": __("Finance Book"),
|
"fieldname": "to_date",
|
||||||
"fieldtype": "Link",
|
"label": __("To Date"),
|
||||||
"options": "Finance Book",
|
"fieldtype": "Date",
|
||||||
},
|
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
|
||||||
{
|
"reqd": 1
|
||||||
"fieldname": "dimension",
|
},
|
||||||
"label": __("Select Dimension"),
|
{
|
||||||
"fieldtype": "Select",
|
"fieldname": "finance_book",
|
||||||
"default": "Cost Center",
|
"label": __("Finance Book"),
|
||||||
"options": get_accounting_dimension_options(),
|
"fieldtype": "Link",
|
||||||
"reqd": 1,
|
"options": "Finance Book",
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
"formatter": erpnext.financial_statements.formatter,
|
"fieldname": "dimension",
|
||||||
"tree": true,
|
"label": __("Select Dimension"),
|
||||||
"name_field": "account",
|
"fieldtype": "Select",
|
||||||
"parent_field": "parent_account",
|
"default": "Cost Center",
|
||||||
"initial_depth": 3
|
"options": get_accounting_dimension_options(),
|
||||||
}
|
"reqd": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"formatter": erpnext.financial_statements.formatter,
|
||||||
|
"tree": true,
|
||||||
|
"name_field": "account",
|
||||||
|
"parent_field": "parent_account",
|
||||||
|
"initial_depth": 3
|
||||||
|
}
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function get_accounting_dimension_options() {
|
function get_accounting_dimension_options() {
|
||||||
let options =["Cost Center", "Project"];
|
let options =["Cost Center", "Project"];
|
||||||
frappe.db.get_list('Accounting Dimension',
|
frappe.db.get_list('Accounting Dimension',
|
||||||
{fields:['document_type']}).then((res) => {
|
{fields:['document_type']}).then((res) => {
|
||||||
res.forEach((dimension) => {
|
res.forEach((dimension) => {
|
||||||
options.push(dimension.document_type);
|
options.push(dimension.document_type);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user