Merge branch 'develop' into payment_terms_report
This commit is contained in:
commit
2f5bfcc055
1
.flake8
1
.flake8
@ -28,6 +28,7 @@ ignore =
|
||||
B007,
|
||||
B950,
|
||||
W191,
|
||||
E124, # closing bracket, irritating while writing QB code
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
|
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -40,6 +40,7 @@ body:
|
||||
- HR
|
||||
- projects
|
||||
- support
|
||||
- CRM
|
||||
- assets
|
||||
- integrations
|
||||
- quality
|
||||
@ -48,6 +49,7 @@ body:
|
||||
- agriculture
|
||||
- education
|
||||
- non-profit
|
||||
- other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
34
.github/helper/install.sh
vendored
34
.github/helper/install.sh
vendored
@ -8,21 +8,37 @@ sudo apt-get install redis-server libcups2-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" --depth 1
|
||||
frappeuser=${FRAPPE_USER:-"frappe"}
|
||||
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
|
||||
|
||||
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
|
||||
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
|
||||
|
||||
mkdir ~/frappe-bench/sites/test_site
|
||||
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/test_site/site_config.json
|
||||
else
|
||||
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_postgres.json" ~/frappe-bench/sites/test_site/site_config.json
|
||||
fi
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
|
||||
if [ "$DB" == "mariadb" ];then
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
|
||||
fi
|
||||
|
||||
if [ "$DB" == "postgres" ];then
|
||||
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres;
|
||||
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
|
||||
fi
|
||||
|
||||
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
|
||||
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
|
||||
|
@ -13,4 +13,4 @@
|
||||
"host_name": "http://test_site:8000",
|
||||
"install_apps": ["erpnext"],
|
||||
"throttle_user_limit": 100
|
||||
}
|
||||
}
|
18
.github/helper/site_config_postgres.json
vendored
Normal file
18
.github/helper/site_config_postgres.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_host": "127.0.0.1",
|
||||
"db_port": 5432,
|
||||
"db_name": "test_frappe",
|
||||
"db_password": "test_frappe",
|
||||
"db_type": "postgres",
|
||||
"allow_tests": true,
|
||||
"auto_email_id": "test@example.com",
|
||||
"mail_server": "smtp.example.com",
|
||||
"mail_login": "test@example.com",
|
||||
"mail_password": "test",
|
||||
"admin_password": "admin",
|
||||
"root_login": "postgres",
|
||||
"root_password": "travis",
|
||||
"host_name": "http://test_site:8000",
|
||||
"install_apps": ["erpnext"],
|
||||
"throttle_user_limit": 100
|
||||
}
|
55
.github/labeler.yml
vendored
Normal file
55
.github/labeler.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
accounts:
|
||||
- erpnext/accounts/*
|
||||
- erpnext/controllers/accounts_controller.py
|
||||
- erpnext/controllers/taxes_and_totals.py
|
||||
|
||||
stock:
|
||||
- erpnext/stock/*
|
||||
- erpnext/controllers/stock_controller.py
|
||||
- erpnext/controllers/item_variant.py
|
||||
|
||||
assets:
|
||||
- erpnext/assets/*
|
||||
|
||||
regional:
|
||||
- erpnext/regional/*
|
||||
|
||||
selling:
|
||||
- erpnext/selling/*
|
||||
- erpnext/controllers/selling_controller.py
|
||||
|
||||
buying:
|
||||
- erpnext/buying/*
|
||||
- erpnext/controllers/buying_controller.py
|
||||
|
||||
support:
|
||||
- erpnext/support/*
|
||||
|
||||
POS:
|
||||
- pos*
|
||||
|
||||
ecommerce:
|
||||
- erpnext/e_commerce/*
|
||||
|
||||
maintenance:
|
||||
- erpnext/maintenance/*
|
||||
|
||||
manufacturing:
|
||||
- erpnext/manufacturing/*
|
||||
|
||||
crm:
|
||||
- erpnext/crm/*
|
||||
|
||||
HR:
|
||||
- erpnext/hr/*
|
||||
|
||||
payroll:
|
||||
- erpnext/payroll*
|
||||
|
||||
projects:
|
||||
- erpnext/projects/*
|
||||
|
||||
# Any python files modifed but no test files modified
|
||||
needs-tests:
|
||||
- any: ['erpnext/**/*.py']
|
||||
all: ['!erpnext/**/test*.py']
|
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@ -30,6 +30,7 @@ issues:
|
||||
exemptLabels:
|
||||
- valid
|
||||
- to-validate
|
||||
- QA
|
||||
markComment: >
|
||||
This issue has been automatically marked as inactive because it has not had
|
||||
recent activity and it wasn't validated by maintainer team. It will be
|
||||
|
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: 3.8
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v2
|
||||
|
12
.github/workflows/labeller.yml
vendored
Normal file
12
.github/workflows/labeller.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v3
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
5
.github/workflows/patch.yml
vendored
5
.github/workflows/patch.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
@ -80,6 +80,9 @@ jobs:
|
||||
|
||||
- name: Install
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
|
||||
- name: Run Patch Tests
|
||||
run: |
|
||||
|
@ -1,19 +1,31 @@
|
||||
name: Server
|
||||
name: Server (Mariadb)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
- '**.html'
|
||||
push:
|
||||
branches: [ develop ]
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
user:
|
||||
description: 'user'
|
||||
required: true
|
||||
default: 'frappe'
|
||||
type: string
|
||||
branch:
|
||||
description: 'Branch name'
|
||||
default: 'develop'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: server-develop-${{ github.event.number }}
|
||||
group: server-mariadb-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@ -45,7 +57,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
@ -92,7 +104,10 @@ jobs:
|
||||
- name: Install
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
FRAPPE_USER: ${{ github.event.inputs.user }}
|
||||
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Run Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
|
105
.github/workflows/server-tests-postgres.yml
vendored
Normal file
105
.github/workflows/server-tests-postgres.yml
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
name: Server (Postgres)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
types: [opened, labelled, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: server-postgres-develop-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
container: [1, 2, 3]
|
||||
|
||||
name: Python Unit Tests
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13.3
|
||||
env:
|
||||
POSTGRES_PASSWORD: travis
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
check-latest: true
|
||||
|
||||
- name: Add to Hosts
|
||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
|
||||
- name: Install
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: postgres
|
||||
TYPE: server
|
||||
|
||||
- name: Run Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
|
||||
env:
|
||||
TYPE: server
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.8
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
|
@ -23,13 +23,13 @@ erpnext/stock/ @marination @rohitwaghchaure @ankush
|
||||
|
||||
erpnext/crm/ @ruchamahabal @pateljannat
|
||||
erpnext/education/ @ruchamahabal @pateljannat
|
||||
erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
|
||||
erpnext/hr/ @ruchamahabal @pateljannat
|
||||
erpnext/non_profit/ @ruchamahabal
|
||||
erpnext/payroll @ruchamahabal @pateljannat
|
||||
erpnext/projects/ @ruchamahabal @pateljannat
|
||||
|
||||
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush
|
||||
erpnext/public/ @nextchamp-saqib @marination
|
||||
|
||||
.github/ @surajshetty3416 @ankush
|
||||
.github/ @ankush
|
||||
requirements.txt @gavindsouza
|
||||
|
44
cypress/integration/test_bulk_transaction_processing.js
Normal file
44
cypress/integration/test_bulk_transaction_processing.js
Normal file
@ -0,0 +1,44 @@
|
||||
describe("Bulk Transaction Processing", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit("/app/website");
|
||||
});
|
||||
|
||||
it("Creates To Sales Order", () => {
|
||||
cy.visit("/app/sales-order");
|
||||
cy.url().should("include", "/sales-order");
|
||||
cy.window()
|
||||
.its("frappe.csrf_token")
|
||||
.then((csrf_token) => {
|
||||
return cy
|
||||
.request({
|
||||
url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Frappe-CSRF-Token": csrf_token,
|
||||
},
|
||||
timeout: 60000,
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res.status).eq(200);
|
||||
});
|
||||
});
|
||||
cy.wait(5000);
|
||||
cy.get(
|
||||
".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
|
||||
).check({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".actions-btn-group > .btn-primary").click({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
|
||||
.contains("Sales Invoice")
|
||||
.click({ force: true });
|
||||
cy.wait(3000);
|
||||
cy.get(".modal-content > .modal-footer > .standard-actions")
|
||||
.contains("Yes")
|
||||
.click({ force: true });
|
||||
cy.contains("Creation of Sales Invoice successful");
|
||||
});
|
||||
});
|
@ -2,8 +2,6 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = '14.0.0-dev'
|
||||
|
||||
def get_default_company(user=None):
|
||||
@ -121,14 +119,17 @@ def allow_regional(fn):
|
||||
@erpnext.allow_regional
|
||||
def myfunction():
|
||||
pass'''
|
||||
|
||||
def caller(*args, **kwargs):
|
||||
region = get_region()
|
||||
fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__
|
||||
if region in regional_overrides and fn_name in regional_overrides[region]:
|
||||
return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs)
|
||||
else:
|
||||
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
|
||||
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
|
||||
|
||||
if not overrides or function_path not in overrides:
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
# Priority given to last installed app
|
||||
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
||||
|
||||
return caller
|
||||
|
||||
def get_last_membership(member):
|
||||
|
@ -254,11 +254,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = "enable_deferred_revenue" \
|
||||
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
|
||||
|
||||
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
|
||||
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
|
||||
if not (start_date and end_date): return
|
||||
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
account_currency = get_account_currency(item.expense_account or item.income_account)
|
||||
if doc.doctype == "Sales Invoice":
|
||||
against, project = doc.customer, doc.project
|
||||
credit_account, debit_account = item.income_account, item.deferred_revenue_account
|
||||
@ -279,6 +281,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
if not amount:
|
||||
return
|
||||
|
||||
# check if books nor frozen till endate:
|
||||
if getdate(end_date) >= getdate(accounts_frozen_upto):
|
||||
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
||||
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
||||
@ -406,8 +412,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
'account': credit_account,
|
||||
'credit': base_amount,
|
||||
'credit_in_account_currency': amount,
|
||||
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
|
||||
'party': against,
|
||||
'account_currency': account_currency,
|
||||
'reference_name': doc.name,
|
||||
'reference_type': doc.doctype,
|
||||
@ -420,8 +424,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
'account': debit_account,
|
||||
'debit': base_amount,
|
||||
'debit_in_account_currency': amount,
|
||||
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
|
||||
'party': against,
|
||||
'account_currency': account_currency,
|
||||
'reference_name': doc.name,
|
||||
'reference_type': doc.doctype,
|
||||
|
@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
|
||||
frm.trigger('add_toolbar_buttons');
|
||||
}
|
||||
if (frm.has_perm('write')) {
|
||||
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
||||
frm.trigger("update_account_number");
|
||||
});
|
||||
frm.add_custom_button(__('Merge Account'), function () {
|
||||
frm.trigger("merge_account");
|
||||
});
|
||||
}, __('Actions'));
|
||||
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
||||
frm.trigger("update_account_number");
|
||||
}, __('Actions'));
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -59,11 +59,12 @@ frappe.ui.form.on('Account', {
|
||||
}
|
||||
},
|
||||
add_toolbar_buttons: function(frm) {
|
||||
frm.add_custom_button(__('Chart of Accounts'),
|
||||
function () { frappe.set_route("Tree", "Account"); });
|
||||
frm.add_custom_button(__('Chart of Accounts'), () => {
|
||||
frappe.set_route("Tree", "Account");
|
||||
}, __('View'));
|
||||
|
||||
if (frm.doc.is_group == 1) {
|
||||
frm.add_custom_button(__('Group to Non-Group'), function () {
|
||||
frm.add_custom_button(__('Convert to Non-Group'), function () {
|
||||
return frappe.call({
|
||||
doc: frm.doc,
|
||||
method: 'convert_group_to_ledger',
|
||||
@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, __('Actions'));
|
||||
|
||||
} else if (cint(frm.doc.is_group) == 0
|
||||
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
|
||||
frm.add_custom_button(__('Ledger'), function () {
|
||||
frm.add_custom_button(__('General Ledger'), function () {
|
||||
frappe.route_options = {
|
||||
"account": frm.doc.name,
|
||||
"from_date": frappe.sys_defaults.year_start_date,
|
||||
@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
|
||||
"company": frm.doc.company
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
});
|
||||
}, __('View'));
|
||||
|
||||
frm.add_custom_button(__('Non-Group to Group'), function () {
|
||||
frm.add_custom_button(__('Convert to Group'), function () {
|
||||
return frappe.call({
|
||||
doc: frm.doc,
|
||||
method: 'convert_ledger_to_group',
|
||||
@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, __('Actions'));
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -7,35 +7,30 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"column_break_11",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"invoice_and_billing_tab",
|
||||
"enable_features_section",
|
||||
"unlink_payment_on_cancellation_of_invoice",
|
||||
"automatically_fetch_payment_terms",
|
||||
"delete_linked_ledger_entries",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"column_break_13",
|
||||
"delete_linked_ledger_entries",
|
||||
"invoicing_features_section",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"automatically_fetch_payment_terms",
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"post_change_gl_entries",
|
||||
"enable_discount_accounting",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"frozen_accounts_modifier",
|
||||
"column_break_4",
|
||||
"report_setting_section",
|
||||
"use_custom_cash_flow",
|
||||
"deferred_accounting_settings_section",
|
||||
"book_deferred_entries_based_on",
|
||||
"column_break_18",
|
||||
"automatically_process_deferred_accounting_entry",
|
||||
"book_deferred_entries_via_journal_entry",
|
||||
"submit_journal_entries",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"print_settings",
|
||||
"show_inclusive_tax_in_print",
|
||||
"column_break_12",
|
||||
@ -43,8 +38,25 @@
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"stale_days",
|
||||
"report_settings_sb",
|
||||
"use_custom_cash_flow"
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
"column_break_11",
|
||||
"role_allowed_to_over_bill",
|
||||
"credit_controller",
|
||||
"make_payment_via_journal_entry",
|
||||
"pos_tab",
|
||||
"pos_setting_section",
|
||||
"post_change_gl_entries",
|
||||
"assets_tab",
|
||||
"asset_settings_section",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"report_settings_sb"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -70,10 +82,6 @@
|
||||
"label": "Determine Address Tax Category From",
|
||||
"options": "Billing Address\nShipping Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_controller",
|
||||
"fieldtype": "Link",
|
||||
@ -83,6 +91,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field",
|
||||
"fieldname": "check_supplier_invoice_uniqueness",
|
||||
"fieldtype": "Check",
|
||||
"label": "Check Supplier Invoice Number Uniqueness"
|
||||
@ -168,7 +177,7 @@
|
||||
"description": "Only select this if you have set up the Cash Flow Mapper documents",
|
||||
"fieldname": "use_custom_cash_flow",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Custom Cash Flow Format"
|
||||
"label": "Enable Custom Cash Flow Format"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@ -241,7 +250,7 @@
|
||||
{
|
||||
"fieldname": "accounts_transactions_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transactions Settings"
|
||||
"label": "Credit Limit Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
@ -272,9 +281,72 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
"fieldname": "enable_common_party_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
},
|
||||
{
|
||||
"fieldname": "enable_features_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Invoice Cancellation"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_25",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "asset_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Asset Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoicing_settings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Credit Limits"
|
||||
},
|
||||
{
|
||||
"fieldname": "assets_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Assets"
|
||||
},
|
||||
{
|
||||
"fieldname": "closing_settings_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Accounts Closing"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "POS Setting"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_and_billing_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Invoice and Billing"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoicing_features_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Invoicing Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "POS"
|
||||
},
|
||||
{
|
||||
"fieldname": "report_setting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Report Setting"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -282,7 +354,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-11 17:42:36.427699",
|
||||
"modified": "2022-02-04 12:32:36.805652",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@ -309,5 +381,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -7,13 +7,17 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: ["in", frm.doc.company],
|
||||
company: frm.doc.company,
|
||||
'is_company_account': 1
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
frm.trigger('bank_account');
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frappe.require("bank-reconciliation-tool.bundle.js", () =>
|
||||
frm.trigger("make_reconciliation_tool")
|
||||
@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
bank_account: function (frm) {
|
||||
frappe.db.get_value(
|
||||
"Bank Account",
|
||||
frm.bank_account,
|
||||
frm.doc.bank_account,
|
||||
"account",
|
||||
(r) => {
|
||||
frappe.db.get_value(
|
||||
|
@ -218,6 +218,8 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
# updated clear date of all the vouchers based on the bank transaction
|
||||
vouchers = json.loads(vouchers)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account')
|
||||
|
||||
if transaction.unallocated_amount == 0:
|
||||
frappe.throw(_("This bank transaction is already fully reconciled"))
|
||||
total_amount = 0
|
||||
@ -226,7 +228,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
total_amount += get_paid_amount(frappe._dict({
|
||||
'payment_document': voucher['payment_doctype'],
|
||||
'payment_entry': voucher['payment_name'],
|
||||
}), transaction.currency)
|
||||
}), transaction.currency, company_account)
|
||||
|
||||
if total_amount > transaction.unallocated_amount:
|
||||
frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
|
||||
@ -261,7 +263,7 @@ def get_linked_payments(bank_transaction_name, document_types = None):
|
||||
return matching
|
||||
|
||||
def check_matching(bank_account, company, transaction, document_types):
|
||||
# combine all types of vocuhers
|
||||
# combine all types of vouchers
|
||||
subquery = get_queries(bank_account, company, transaction, document_types)
|
||||
filters = {
|
||||
"amount": transaction.unallocated_amount,
|
||||
@ -343,13 +345,11 @@ def get_pe_matching_query(amount_condition, account_from_to, transaction):
|
||||
def get_je_matching_query(amount_condition, transaction):
|
||||
# get matching journal entry query
|
||||
|
||||
# We have mapping at the bank level
|
||||
# So one bank could have both types of bank accounts like asset and liability
|
||||
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
|
||||
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
|
||||
root_type = frappe.get_value("Account", company_account, "root_type")
|
||||
|
||||
if root_type == "Liability":
|
||||
cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
|
||||
else:
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||
|
||||
return f"""
|
||||
|
||||
|
@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
"withdrawal",
|
||||
"description",
|
||||
"reference_number",
|
||||
"bank_account"
|
||||
"bank_account",
|
||||
"currency"
|
||||
],
|
||||
},
|
||||
});
|
||||
|
@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
|
||||
from openpyxl.styles import Font
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
INVALID_VALUES = ("", None)
|
||||
|
||||
class BankStatementImport(DataImport):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -95,6 +96,18 @@ def download_errored_template(data_import_name):
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
data_import.export_errored_rows()
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
for i, row in enumerate(raw_data):
|
||||
if all(v in INVALID_VALUES for v in row):
|
||||
# empty row
|
||||
continue
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
|
||||
"""This method runs in background job"""
|
||||
|
||||
@ -104,7 +117,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
file = import_file_path if import_file_path else google_sheets_url
|
||||
|
||||
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
|
||||
data = import_file.raw_data
|
||||
|
||||
data = parse_data_from_template(import_file.raw_data)
|
||||
|
||||
if import_file_path:
|
||||
add_bank_account(data, bank_account)
|
||||
|
@ -2,9 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from six.moves import reduce
|
||||
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
@ -102,7 +103,7 @@ def get_total_allocated_amount(payment_entry):
|
||||
AND
|
||||
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
|
||||
|
||||
def get_paid_amount(payment_entry, currency):
|
||||
def get_paid_amount(payment_entry, currency, bank_account):
|
||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||
|
||||
paid_amount_field = "paid_amount"
|
||||
@ -115,7 +116,7 @@ def get_paid_amount(payment_entry, currency):
|
||||
payment_entry.payment_entry, paid_amount_field)
|
||||
|
||||
elif payment_entry.payment_document == "Journal Entry":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")
|
||||
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
|
||||
|
||||
elif payment_entry.payment_document == "Expense Claim":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
|
||||
|
@ -15,17 +15,6 @@ frappe.ui.form.on('Cost Center', {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query("cost_center", "distributed_cost_center", function() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
enable_distributed_cost_center: 0,
|
||||
name: ['!=', frm.doc.name]
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if (!frm.is_new()) {
|
||||
|
@ -16,9 +16,6 @@
|
||||
"cb0",
|
||||
"is_group",
|
||||
"disabled",
|
||||
"section_break_9",
|
||||
"enable_distributed_cost_center",
|
||||
"distributed_cost_center",
|
||||
"lft",
|
||||
"rgt",
|
||||
"old_parent"
|
||||
@ -122,31 +119,13 @@
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_distributed_cost_center",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Distributed Cost Center"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_group==0",
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "enable_distributed_cost_center",
|
||||
"fieldname": "distributed_cost_center",
|
||||
"fieldtype": "Table",
|
||||
"label": "Distributed Cost Center",
|
||||
"options": "Distributed Cost Center"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-17 16:09:30.025214",
|
||||
"modified": "2022-01-31 13:22:58.916273",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cost Center",
|
||||
@ -189,5 +168,6 @@
|
||||
"search_fields": "parent_cost_center, is_group",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
from erpnext.accounts.utils import validate_field_number
|
||||
@ -20,24 +19,6 @@ class CostCenter(NestedSet):
|
||||
def validate(self):
|
||||
self.validate_mandatory()
|
||||
self.validate_parent_cost_center()
|
||||
self.validate_distributed_cost_center()
|
||||
|
||||
def validate_distributed_cost_center(self):
|
||||
if cint(self.enable_distributed_cost_center):
|
||||
if not self.distributed_cost_center:
|
||||
frappe.throw(_("Please enter distributed cost center"))
|
||||
if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
|
||||
frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
|
||||
if not self.get('__islocal'):
|
||||
if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
|
||||
and self.check_if_part_of_distributed_cost_center():
|
||||
frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
|
||||
if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
|
||||
frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
|
||||
if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
|
||||
frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
|
||||
else:
|
||||
self.distributed_cost_center = []
|
||||
|
||||
def validate_mandatory(self):
|
||||
if self.cost_center_name != self.company and not self.parent_cost_center:
|
||||
@ -64,10 +45,10 @@ class CostCenter(NestedSet):
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_ledger_to_group(self):
|
||||
if cint(self.enable_distributed_cost_center):
|
||||
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
|
||||
if self.check_if_part_of_distributed_cost_center():
|
||||
frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group"))
|
||||
if self.if_allocation_exists_against_cost_center():
|
||||
frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
|
||||
if self.check_if_part_of_cost_center_allocation():
|
||||
frappe.throw(_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group"))
|
||||
if self.check_gle_exists():
|
||||
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
|
||||
self.is_group = 1
|
||||
@ -81,8 +62,17 @@ class CostCenter(NestedSet):
|
||||
return frappe.db.sql("select name from `tabCost Center` where \
|
||||
parent_cost_center = %s and docstatus != 2", self.name)
|
||||
|
||||
def check_if_part_of_distributed_cost_center(self):
|
||||
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name})
|
||||
def if_allocation_exists_against_cost_center(self):
|
||||
return frappe.db.get_value("Cost Center Allocation", filters = {
|
||||
"main_cost_center": self.name,
|
||||
"docstatus": 1
|
||||
})
|
||||
|
||||
def check_if_part_of_cost_center_allocation(self):
|
||||
return frappe.db.get_value("Cost Center Allocation Percentage", filters = {
|
||||
"cost_center": self.name,
|
||||
"docstatus": 1
|
||||
})
|
||||
|
||||
def before_rename(self, olddn, newdn, merge=False):
|
||||
# Add company abbr if not provided
|
||||
@ -126,8 +116,4 @@ def on_doctype_update():
|
||||
def get_name_with_number(new_account, account_number):
|
||||
if account_number and not new_account[0].isdigit():
|
||||
new_account = account_number + " - " + new_account
|
||||
return new_account
|
||||
|
||||
def check_if_distributed_cost_center_enabled(cost_center_list):
|
||||
value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
|
||||
return next((True for x in value_list if x[0]), False)
|
||||
return new_account
|
@ -23,33 +23,6 @@ class TestCostCenter(unittest.TestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, cost_center.save)
|
||||
|
||||
def test_validate_distributed_cost_center(self):
|
||||
|
||||
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
|
||||
frappe.get_doc(test_records[0]).insert()
|
||||
|
||||
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
|
||||
frappe.get_doc(test_records[1]).insert()
|
||||
|
||||
invalid_distributed_cost_center = frappe.get_doc({
|
||||
"company": "_Test Company",
|
||||
"cost_center_name": "_Test Distributed Cost Center",
|
||||
"doctype": "Cost Center",
|
||||
"is_group": 0,
|
||||
"parent_cost_center": "_Test Company - _TC",
|
||||
"enable_distributed_cost_center": 1,
|
||||
"distributed_cost_center": [{
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"percentage_allocation": 40
|
||||
}, {
|
||||
"cost_center": "_Test Cost Center 2 - _TC",
|
||||
"percentage_allocation": 50
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
|
||||
|
||||
def create_cost_center(**args):
|
||||
args = frappe._dict(args)
|
||||
if args.cost_center_name:
|
||||
|
@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Cost Center Allocation', {
|
||||
setup: function(frm) {
|
||||
let filters = {"is_group": 0};
|
||||
if (frm.doc.company) {
|
||||
$.extend(filters, {
|
||||
"company": frm.doc.company
|
||||
});
|
||||
}
|
||||
|
||||
frm.set_query('main_cost_center', function() {
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,128 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "CC-ALLOC-.#####",
|
||||
"creation": "2022-01-13 20:07:29.871109",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_cost_center",
|
||||
"company",
|
||||
"column_break_2",
|
||||
"valid_from",
|
||||
"section_break_5",
|
||||
"allocation_percentages",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "main_cost_center",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Main Cost Center",
|
||||
"options": "Cost Center",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "valid_from",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Valid From",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "main_cost_center.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "allocation_percentages",
|
||||
"fieldtype": "Table",
|
||||
"label": "Cost Center Allocation Percentages",
|
||||
"options": "Cost Center Allocation Percentage",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Cost Center Allocation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-31 11:47:12.086253",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cost Center Allocation",
|
||||
"name_case": "UPPER CASE",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 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,90 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, format_date, getdate
|
||||
|
||||
|
||||
class MainCostCenterCantBeChild(frappe.ValidationError):
|
||||
pass
|
||||
class InvalidMainCostCenter(frappe.ValidationError):
|
||||
pass
|
||||
class InvalidChildCostCenter(frappe.ValidationError):
|
||||
pass
|
||||
class WrongPercentageAllocation(frappe.ValidationError):
|
||||
pass
|
||||
class InvalidDateError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
class CostCenterAllocation(Document):
|
||||
def validate(self):
|
||||
self.validate_total_allocation_percentage()
|
||||
self.validate_from_date_based_on_existing_gle()
|
||||
self.validate_backdated_allocation()
|
||||
self.validate_main_cost_center()
|
||||
self.validate_child_cost_centers()
|
||||
|
||||
def validate_total_allocation_percentage(self):
|
||||
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
||||
|
||||
if total_percentage != 100:
|
||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||
|
||||
def validate_from_date_based_on_existing_gle(self):
|
||||
# Check if GLE exists against the main cost center
|
||||
# If exists ensure from date is set after posting date of last GLE
|
||||
|
||||
last_gle_date = frappe.db.get_value("GL Entry",
|
||||
{"cost_center": self.main_cost_center, "is_cancelled": 0},
|
||||
"posting_date", order_by="posting_date desc")
|
||||
|
||||
if last_gle_date:
|
||||
if getdate(self.valid_from) <= getdate(last_gle_date):
|
||||
frappe.throw(_("Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date")
|
||||
.format(last_gle_date, self.main_cost_center), InvalidDateError)
|
||||
|
||||
def validate_backdated_allocation(self):
|
||||
# Check if there are any future existing allocation records against the main cost center
|
||||
# If exists, warn the user about it
|
||||
|
||||
future_allocation = frappe.db.get_value("Cost Center Allocation", filters = {
|
||||
"main_cost_center": self.main_cost_center,
|
||||
"valid_from": (">=", self.valid_from),
|
||||
"name": ("!=", self.name),
|
||||
"docstatus": 1
|
||||
}, fieldname=['valid_from', 'name'], order_by='valid_from', as_dict=1)
|
||||
|
||||
if future_allocation:
|
||||
frappe.msgprint(_("Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}")
|
||||
.format(frappe.bold(future_allocation.name), frappe.bold(format_date(future_allocation.valid_from)),
|
||||
frappe.bold(format_date(add_days(future_allocation.valid_from, -1)))),
|
||||
title=_("Warning!"), indicator="orange", alert=1
|
||||
)
|
||||
|
||||
def validate_main_cost_center(self):
|
||||
# Main cost center itself cannot be entered in child table
|
||||
if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]:
|
||||
frappe.throw(_("Main Cost Center {0} cannot be entered in the child table")
|
||||
.format(self.main_cost_center), MainCostCenterCantBeChild)
|
||||
|
||||
# If main cost center is used for allocation under any other cost center,
|
||||
# allocation cannot be done against it
|
||||
parent = frappe.db.get_value("Cost Center Allocation Percentage", filters = {
|
||||
"cost_center": self.main_cost_center,
|
||||
"docstatus": 1
|
||||
}, fieldname='parent')
|
||||
if parent:
|
||||
frappe.throw(_("{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}")
|
||||
.format(self.main_cost_center, parent), InvalidMainCostCenter)
|
||||
|
||||
def validate_child_cost_centers(self):
|
||||
# Check if child cost center is used as main cost center in any existing allocation
|
||||
main_cost_centers = [d.main_cost_center for d in
|
||||
frappe.get_all("Cost Center Allocation", {'docstatus': 1}, 'main_cost_center')]
|
||||
|
||||
for d in self.allocation_percentages:
|
||||
if d.cost_center in main_cost_centers:
|
||||
frappe.throw(_("Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.")
|
||||
.format(d.cost_center), InvalidChildCostCenter)
|
@ -0,0 +1,156 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation import (
|
||||
InvalidChildCostCenter,
|
||||
InvalidDateError,
|
||||
InvalidMainCostCenter,
|
||||
MainCostCenterCantBeChild,
|
||||
WrongPercentageAllocation,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
|
||||
|
||||
class TestCostCenterAllocation(unittest.TestCase):
|
||||
def setUp(self):
|
||||
cost_centers = ["Main Cost Center 1", "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2"]
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
|
||||
def test_gle_based_on_cost_center_allocation(self):
|
||||
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}
|
||||
)
|
||||
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
|
||||
cost_center = "Main Cost Center 1 - _TC", submit=True)
|
||||
|
||||
expected_values = [
|
||||
["Sub Cost Center 1 - _TC", 0.0, 60],
|
||||
["Sub Cost Center 2 - _TC", 0.0, 40]
|
||||
]
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.cost_center, gle.debit, gle.credit)
|
||||
.where(gle.voucher_type == 'Journal Entry')
|
||||
.where(gle.voucher_no == jv.name)
|
||||
.where(gle.account == 'Sales - _TC')
|
||||
.orderby(gle.cost_center)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[i][0], gle.cost_center)
|
||||
self.assertEqual(expected_values[i][1], gle.debit)
|
||||
self.assertEqual(expected_values[i][2], gle.credit)
|
||||
|
||||
cca.cancel()
|
||||
jv.cancel()
|
||||
|
||||
def test_main_cost_center_cant_be_child(self):
|
||||
# Main cost center itself cannot be entered in child table
|
||||
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Main Cost Center 1 - _TC": 40
|
||||
}, save=False
|
||||
)
|
||||
|
||||
self.assertRaises(MainCostCenterCantBeChild, cca.save)
|
||||
|
||||
def test_invalid_main_cost_center(self):
|
||||
# If main cost center is used for allocation under any other cost center,
|
||||
# allocation cannot be done against it
|
||||
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}
|
||||
)
|
||||
|
||||
cca2 = create_cost_center_allocation("_Test Company", "Sub Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 2 - _TC": 100
|
||||
}, save=False
|
||||
)
|
||||
|
||||
self.assertRaises(InvalidMainCostCenter, cca2.save)
|
||||
|
||||
cca1.cancel()
|
||||
|
||||
def test_if_child_cost_center_has_any_allocation_record(self):
|
||||
# Check if any child cost center is used as main cost center in any other existing allocation
|
||||
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}
|
||||
)
|
||||
|
||||
cca2 = create_cost_center_allocation("_Test Company", "Main Cost Center 2 - _TC",
|
||||
{
|
||||
"Main Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 1 - _TC": 40
|
||||
}, save=False
|
||||
)
|
||||
|
||||
self.assertRaises(InvalidChildCostCenter, cca2.save)
|
||||
|
||||
cca1.cancel()
|
||||
|
||||
def test_total_percentage(self):
|
||||
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 40,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}, save=False
|
||||
)
|
||||
self.assertRaises(WrongPercentageAllocation, cca.save)
|
||||
|
||||
def test_valid_from_based_on_existing_gle(self):
|
||||
# GLE posted against Sub Cost Center 1 on today
|
||||
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
|
||||
cost_center = "Main Cost Center 1 - _TC", posting_date=today(), submit=True)
|
||||
|
||||
# try to set valid from as yesterday
|
||||
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
|
||||
{
|
||||
"Sub Cost Center 1 - _TC": 60,
|
||||
"Sub Cost Center 2 - _TC": 40
|
||||
}, valid_from=add_days(today(), -1), save=False
|
||||
)
|
||||
|
||||
self.assertRaises(InvalidDateError, cca.save)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
def create_cost_center_allocation(company, main_cost_center, allocation_percentages,
|
||||
valid_from=None, valid_upto=None, save=True, submit=True):
|
||||
doc = frappe.new_doc("Cost Center Allocation")
|
||||
doc.main_cost_center = main_cost_center
|
||||
doc.company = company
|
||||
doc.valid_from = valid_from or today()
|
||||
doc.valid_upto = valid_upto
|
||||
for cc, percentage in allocation_percentages.items():
|
||||
doc.append("allocation_percentages", {
|
||||
"cost_center": cc,
|
||||
"percentage": percentage
|
||||
})
|
||||
if save:
|
||||
doc.save()
|
||||
if submit:
|
||||
doc.submit()
|
||||
|
||||
return doc
|
@ -1,12 +1,13 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2020-03-19 12:34:01.500390",
|
||||
"allow_rename": 1,
|
||||
"creation": "2022-01-13 20:07:30.096306",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"cost_center",
|
||||
"percentage_allocation"
|
||||
"percentage"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -18,23 +19,23 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "percentage_allocation",
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "percentage",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Percentage Allocation",
|
||||
"label": "Percentage (%)",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-19 12:54:43.674655",
|
||||
"modified": "2022-02-01 22:22:31.589523",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Distributed Cost Center",
|
||||
"name": "Cost Center Allocation Percentage",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CostCenterAllocationPercentage(Document):
|
||||
pass
|
@ -39,9 +39,6 @@ def test_create_test_data():
|
||||
"selling_cost_center": "Main - _TC",
|
||||
"income_account": "Sales - _TC"
|
||||
}],
|
||||
"show_in_website": 1,
|
||||
"route":"-test-tesla-car",
|
||||
"website_warehouse": "Stores - _TC"
|
||||
})
|
||||
item.insert()
|
||||
# create test item price
|
||||
|
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Currency Exchange Settings', {
|
||||
service_provider: function(frm) {
|
||||
if (frm.doc.service_provider == "exchangerate.host") {
|
||||
let result = ['result'];
|
||||
let params = {
|
||||
date: '{transaction_date}',
|
||||
from: '{from_currency}',
|
||||
to: '{to_currency}'
|
||||
};
|
||||
add_param(frm, "https://api.exchangerate.host/convert", params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
let result = ['rates', '{to_currency}'];
|
||||
let params = {
|
||||
base: '{from_currency}',
|
||||
symbols: '{to_currency}'
|
||||
};
|
||||
add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function add_param(frm, api, params, result) {
|
||||
var row;
|
||||
frm.clear_table("req_params");
|
||||
frm.clear_table("result_key");
|
||||
|
||||
frm.doc.api_endpoint = api;
|
||||
|
||||
$.each(params, function(key, value) {
|
||||
row = frm.add_child("req_params");
|
||||
row.key = key;
|
||||
row.value = value;
|
||||
});
|
||||
|
||||
$.each(result, function(key, value) {
|
||||
row = frm.add_child("result_key");
|
||||
row.key = value;
|
||||
});
|
||||
|
||||
frm.refresh_fields();
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2022-01-10 13:03:26.237081",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"api_details_section",
|
||||
"service_provider",
|
||||
"api_endpoint",
|
||||
"url",
|
||||
"column_break_3",
|
||||
"help",
|
||||
"section_break_2",
|
||||
"req_params",
|
||||
"column_break_4",
|
||||
"result_key"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "api_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "API Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "api_endpoint",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "API Endpoint",
|
||||
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Example URL",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "help",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Help",
|
||||
"options": "<h3>Currency Exchange Settings Help</h3>\n<p>There are 3 variables that could be used within the endpoint, result key and in values of the parameter.</p>\n<p>Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.</p>\n<p>Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_2",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Request Parameters"
|
||||
},
|
||||
{
|
||||
"fieldname": "req_params",
|
||||
"fieldtype": "Table",
|
||||
"label": "Parameters",
|
||||
"options": "Currency Exchange Settings Details",
|
||||
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "result_key",
|
||||
"fieldtype": "Table",
|
||||
"label": "Result Key",
|
||||
"options": "Currency Exchange Settings Result",
|
||||
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-10 15:51:14.521174",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import nowdate
|
||||
|
||||
|
||||
class CurrencyExchangeSettings(Document):
|
||||
def validate(self):
|
||||
self.set_parameters_and_result()
|
||||
response, value = self.validate_parameters()
|
||||
self.validate_result(response, value)
|
||||
|
||||
def set_parameters_and_result(self):
|
||||
if self.service_provider == 'exchangerate.host':
|
||||
self.set('result_key', [])
|
||||
self.set('req_params', [])
|
||||
|
||||
self.api_endpoint = "https://api.exchangerate.host/convert"
|
||||
self.append('result_key', {'key': 'result'})
|
||||
self.append('req_params', {'key': 'date', 'value': '{transaction_date}'})
|
||||
self.append('req_params', {'key': 'from', 'value': '{from_currency}'})
|
||||
self.append('req_params', {'key': 'to', 'value': '{to_currency}'})
|
||||
elif self.service_provider == 'frankfurter.app':
|
||||
self.set('result_key', [])
|
||||
self.set('req_params', [])
|
||||
|
||||
self.api_endpoint = "https://frankfurter.app/{transaction_date}"
|
||||
self.append('result_key', {'key': 'rates'})
|
||||
self.append('result_key', {'key': '{to_currency}'})
|
||||
self.append('req_params', {'key': 'base', 'value': '{from_currency}'})
|
||||
self.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
|
||||
|
||||
def validate_parameters(self):
|
||||
if frappe.flags.in_test:
|
||||
return None, None
|
||||
|
||||
params = {}
|
||||
for row in self.req_params:
|
||||
params[row.key] = row.value.format(
|
||||
transaction_date=nowdate(),
|
||||
to_currency='INR',
|
||||
from_currency='USD'
|
||||
)
|
||||
|
||||
api_url = self.api_endpoint.format(
|
||||
transaction_date=nowdate(),
|
||||
to_currency='INR',
|
||||
from_currency='USD'
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.get(api_url, params=params)
|
||||
except requests.exceptions.RequestException as e:
|
||||
frappe.throw("Error: " + str(e))
|
||||
|
||||
response.raise_for_status()
|
||||
value = response.json()
|
||||
|
||||
return response, value
|
||||
|
||||
def validate_result(self, response, value):
|
||||
if frappe.flags.in_test:
|
||||
return
|
||||
|
||||
try:
|
||||
for key in self.result_key:
|
||||
value = value[str(key.key).format(
|
||||
transaction_date=nowdate(),
|
||||
to_currency='INR',
|
||||
from_currency='USD'
|
||||
)]
|
||||
except Exception:
|
||||
frappe.throw("Invalid result key. Response: " + response.text)
|
||||
if not isinstance(value, (int, float)):
|
||||
frappe.throw(_("Returned exchange rate is neither integer not float."))
|
||||
|
||||
self.url = response.url
|
||||
frappe.msgprint("Exchange rate of USD to INR is " + str(value))
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCurrencyExchangeSettings(unittest.TestCase):
|
||||
pass
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-09-02 14:54:49.033512",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "key",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Key",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "value",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Value",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-03 19:14:55.889037",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings Details",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CurrencyExchangeSettingsDetails(Document):
|
||||
pass
|
@ -0,0 +1,31 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-09-03 13:17:22.088259",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"key"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "key",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Key",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-03 19:14:40.054245",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings Result",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CurrencyExchangeSettingsResult(Document):
|
||||
pass
|
@ -2,7 +2,7 @@
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2018-11-22 22:45:00.370913",
|
||||
"creation": "2022-01-19 01:09:13.297137",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
@ -10,6 +10,9 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"company",
|
||||
"column_break_3",
|
||||
"disabled",
|
||||
"section_break_5",
|
||||
"taxes"
|
||||
],
|
||||
"fields": [
|
||||
@ -36,10 +39,24 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-03-08 19:50:21.416513",
|
||||
"modified": "2022-01-18 21:11:23.105589",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template",
|
||||
@ -82,6 +99,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
@ -8,6 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
@ -31,7 +32,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
if(frm.doc.docstatus==1) {
|
||||
frm.add_custom_button(__('Reverse Journal Entry'), function() {
|
||||
return erpnext.journal_entry.reverse_journal_entry(frm);
|
||||
}, __('Make'));
|
||||
}, __('Actions'));
|
||||
}
|
||||
|
||||
if (frm.doc.__islocal) {
|
||||
|
@ -13,6 +13,7 @@
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
"finance_book",
|
||||
"reversal_of",
|
||||
"tax_withholding_category",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
@ -515,13 +516,21 @@
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Tax Withholding Amount "
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus",
|
||||
"fieldname": "reversal_of",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reversal Of",
|
||||
"options": "Journal Entry",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-09 15:31:14.484029",
|
||||
"modified": "2022-01-04 13:39:36.485954",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
@ -407,13 +407,14 @@ class JournalEntry(AccountsController):
|
||||
debit_or_credit = 'Debit' if d.debit else 'Credit'
|
||||
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
|
||||
debit_or_credit)
|
||||
against_voucher = ['', against_voucher[1]]
|
||||
else:
|
||||
if d.reference_type == "Sales Invoice":
|
||||
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
|
||||
else:
|
||||
party_account = against_voucher[1]
|
||||
|
||||
if (against_voucher[0] != d.party or party_account != d.account):
|
||||
if (against_voucher[0] != cstr(d.party) or party_account != d.account):
|
||||
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
|
||||
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
|
||||
d.reference_type, d.reference_name))
|
||||
@ -478,13 +479,22 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def set_against_account(self):
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
|
||||
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
|
||||
if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
|
||||
for d in self.get('accounts'):
|
||||
if d.reference_type == 'Sales Invoice':
|
||||
field = 'customer'
|
||||
else:
|
||||
field = 'supplier'
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
|
||||
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
|
||||
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
|
||||
else:
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
|
||||
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
|
||||
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
|
||||
|
||||
def validate_debit_credit_amount(self):
|
||||
for d in self.get('accounts'):
|
||||
@ -1157,9 +1167,8 @@ def make_inter_company_journal_entry(name, voucher_type, company):
|
||||
def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
def update_accounts(source, target, source_parent):
|
||||
target.reference_type = "Journal Entry"
|
||||
target.reference_name = source_parent.name
|
||||
def post_process(source, target):
|
||||
target.reversal_of = source.name
|
||||
|
||||
doclist = get_mapped_doc("Journal Entry", source_name, {
|
||||
"Journal Entry": {
|
||||
@ -1177,9 +1186,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
"debit": "credit",
|
||||
"credit_in_account_currency": "debit_in_account_currency",
|
||||
"credit": "debit",
|
||||
},
|
||||
"postprocess": update_accounts,
|
||||
}
|
||||
},
|
||||
}, target_doc)
|
||||
}, target_doc, post_process)
|
||||
|
||||
return doclist
|
||||
|
128
erpnext/accounts/doctype/ledger_merge/ledger_merge.js
Normal file
128
erpnext/accounts/doctype/ledger_merge/ledger_merge.js
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Ledger Merge', {
|
||||
setup: function(frm) {
|
||||
frappe.realtime.on('ledger_merge_refresh', ({ ledger_merge }) => {
|
||||
if (ledger_merge !== frm.doc.name) return;
|
||||
frappe.model.clear_doc(frm.doc.doctype, frm.doc.name);
|
||||
frappe.model.with_doc(frm.doc.doctype, frm.doc.name).then(() => {
|
||||
frm.refresh();
|
||||
});
|
||||
});
|
||||
|
||||
frappe.realtime.on('ledger_merge_progress', data => {
|
||||
if (data.ledger_merge !== frm.doc.name) return;
|
||||
let message = __('Merging {0} of {1}', [data.current, data.total]);
|
||||
let percent = Math.floor((data.current * 100) / data.total);
|
||||
frm.dashboard.show_progress(__('Merge Progress'), percent, message);
|
||||
frm.page.set_indicator(__('In Progress'), 'orange');
|
||||
});
|
||||
|
||||
frm.set_query("account", function(doc) {
|
||||
if (!doc.company) frappe.throw(__('Please set Company'));
|
||||
if (!doc.root_type) frappe.throw(__('Please set Root Type'));
|
||||
return {
|
||||
filters: {
|
||||
root_type: doc.root_type,
|
||||
company: doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query('account', 'merge_accounts', function(doc) {
|
||||
if (!doc.company) frappe.throw(__('Please set Company'));
|
||||
if (!doc.root_type) frappe.throw(__('Please set Root Type'));
|
||||
if (!doc.account) frappe.throw(__('Please set Account'));
|
||||
let acc = [doc.account];
|
||||
frm.doc.merge_accounts.forEach((row) => {
|
||||
acc.push(row.account);
|
||||
});
|
||||
return {
|
||||
filters: {
|
||||
is_group: doc.is_group,
|
||||
root_type: doc.root_type,
|
||||
name: ["not in", acc],
|
||||
company: doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.page.hide_icon_group();
|
||||
frm.trigger('set_merge_status');
|
||||
frm.trigger('update_primary_action');
|
||||
},
|
||||
|
||||
after_save: function(frm) {
|
||||
setTimeout(() => {
|
||||
frm.trigger('update_primary_action');
|
||||
}, 500);
|
||||
},
|
||||
|
||||
update_primary_action: function(frm) {
|
||||
if (frm.is_dirty()) {
|
||||
frm.enable_save();
|
||||
return;
|
||||
}
|
||||
frm.disable_save();
|
||||
if (frm.doc.status !== 'Success') {
|
||||
if (!frm.is_new()) {
|
||||
let label = frm.doc.status === 'Pending' ? __('Start Merge') : __('Retry');
|
||||
frm.page.set_primary_action(label, () => frm.events.start_merge(frm));
|
||||
} else {
|
||||
frm.page.set_primary_action(__('Save'), () => frm.save());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
start_merge: function(frm) {
|
||||
frm.call({
|
||||
method: 'form_start_merge',
|
||||
args: { docname: frm.doc.name },
|
||||
btn: frm.page.btn_primary
|
||||
}).then(r => {
|
||||
if (r.message === true) {
|
||||
frm.disable_save();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
set_merge_status: function(frm) {
|
||||
if (frm.doc.status == "Pending") return;
|
||||
let successful_records = 0;
|
||||
frm.doc.merge_accounts.forEach((row) => {
|
||||
if (row.merged) successful_records += 1;
|
||||
});
|
||||
let message_args = [successful_records, frm.doc.merge_accounts.length];
|
||||
frm.dashboard.set_headline(__('Successfully merged {0} out of {1}.', message_args));
|
||||
},
|
||||
|
||||
root_type: function(frm) {
|
||||
frm.set_value('account', '');
|
||||
frm.set_value('merge_accounts', []);
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
frm.set_value('account', '');
|
||||
frm.set_value('merge_accounts', []);
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Ledger Merge Accounts', {
|
||||
merge_accounts_add: function(frm) {
|
||||
frm.trigger('update_primary_action');
|
||||
},
|
||||
|
||||
merge_accounts_remove: function(frm) {
|
||||
frm.trigger('update_primary_action');
|
||||
},
|
||||
|
||||
account: function(frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
row.account_name = row.account;
|
||||
frm.refresh_field('merge_accounts');
|
||||
frm.trigger('update_primary_action');
|
||||
}
|
||||
});
|
130
erpnext/accounts/doctype/ledger_merge/ledger_merge.json
Normal file
130
erpnext/accounts/doctype/ledger_merge/ledger_merge.json
Normal file
@ -0,0 +1,130 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:{account_name} merger on {creation}",
|
||||
"creation": "2021-12-09 15:38:04.556584",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_1",
|
||||
"root_type",
|
||||
"account",
|
||||
"account_name",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"status",
|
||||
"is_group",
|
||||
"section_break_5",
|
||||
"merge_accounts"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"depends_on": "root_type",
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_1",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "merge_accounts",
|
||||
"fieldtype": "Table",
|
||||
"label": "Accounts to Merge",
|
||||
"options": "Ledger Merge Accounts",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "account",
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nSuccess\nPartial Success\nError",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "root_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Root Type",
|
||||
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "account",
|
||||
"fetch_from": "account.account_name",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "account_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account Name",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "account",
|
||||
"fetch_from": "account.is_group",
|
||||
"fieldname": "is_group",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Group",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-12 21:34:55.155146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Merge",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
76
erpnext/accounts/doctype/ledger_merge/ledger_merge.py
Normal file
76
erpnext/accounts/doctype/ledger_merge/ledger_merge.py
Normal file
@ -0,0 +1,76 @@
|
||||
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.account.account import merge_account
|
||||
|
||||
|
||||
class LedgerMerge(Document):
|
||||
def start_merge(self):
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
frappe.throw(
|
||||
_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive")
|
||||
)
|
||||
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
|
||||
if self.name not in enqueued_jobs:
|
||||
enqueue(
|
||||
start_merge,
|
||||
queue="default",
|
||||
timeout=6000,
|
||||
event="ledger_merge",
|
||||
job_name=self.name,
|
||||
docname=self.name,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_merge(docname):
|
||||
return frappe.get_doc("Ledger Merge", docname).start_merge()
|
||||
|
||||
def start_merge(docname):
|
||||
ledger_merge = frappe.get_doc("Ledger Merge", docname)
|
||||
successful_merges = 0
|
||||
total = len(ledger_merge.merge_accounts)
|
||||
for row in ledger_merge.merge_accounts:
|
||||
if not row.merged:
|
||||
try:
|
||||
merge_account(
|
||||
row.account,
|
||||
ledger_merge.account,
|
||||
ledger_merge.is_group,
|
||||
ledger_merge.root_type,
|
||||
ledger_merge.company
|
||||
)
|
||||
row.db_set('merged', 1)
|
||||
frappe.db.commit()
|
||||
successful_merges += 1
|
||||
frappe.publish_realtime("ledger_merge_progress", {
|
||||
"ledger_merge": ledger_merge.name,
|
||||
"current": successful_merges,
|
||||
"total": total
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(title=ledger_merge.name)
|
||||
finally:
|
||||
if successful_merges == total:
|
||||
ledger_merge.db_set('status', 'Success')
|
||||
elif successful_merges > 0:
|
||||
ledger_merge.db_set('status', 'Partial Success')
|
||||
else:
|
||||
ledger_merge.db_set('status', 'Error')
|
||||
|
||||
frappe.publish_realtime("ledger_merge_refresh", {"ledger_merge": ledger_merge.name})
|
118
erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py
Normal file
118
erpnext/accounts/doctype/ledger_merge/test_ledger_merge.py
Normal file
@ -0,0 +1,118 @@
|
||||
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.accounts.doctype.ledger_merge.ledger_merge import start_merge
|
||||
|
||||
|
||||
class TestLedgerMerge(unittest.TestCase):
|
||||
def test_merge_success(self):
|
||||
if not frappe.db.exists("Account", "Indirect Expenses - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Indirect Expenses"
|
||||
acc.is_group = 1
|
||||
acc.parent_account = "Expenses - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
if not frappe.db.exists("Account", "Indirect Test Expenses - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Indirect Test Expenses"
|
||||
acc.is_group = 1
|
||||
acc.parent_account = "Expenses - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
if not frappe.db.exists("Account", "Administrative Test Expenses - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Administrative Test Expenses"
|
||||
acc.parent_account = "Indirect Test Expenses - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Ledger Merge",
|
||||
"company": "_Test Company",
|
||||
"root_type": frappe.db.get_value("Account", "Indirect Test Expenses - _TC", "root_type"),
|
||||
"account": "Indirect Expenses - _TC",
|
||||
"merge_accounts": [
|
||||
{
|
||||
"account": "Indirect Test Expenses - _TC",
|
||||
"account_name": "Indirect Expenses"
|
||||
}
|
||||
]
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
|
||||
self.assertEqual(parent, "Indirect Test Expenses - _TC")
|
||||
|
||||
start_merge(doc.name)
|
||||
|
||||
parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
|
||||
self.assertEqual(parent, "Indirect Expenses - _TC")
|
||||
|
||||
self.assertFalse(frappe.db.exists("Account", "Indirect Test Expenses - _TC"))
|
||||
|
||||
def test_partial_merge_success(self):
|
||||
if not frappe.db.exists("Account", "Indirect Income - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Indirect Income"
|
||||
acc.is_group = 1
|
||||
acc.parent_account = "Income - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
if not frappe.db.exists("Account", "Indirect Test Income - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Indirect Test Income"
|
||||
acc.is_group = 1
|
||||
acc.parent_account = "Income - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
if not frappe.db.exists("Account", "Administrative Test Income - _TC"):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "Administrative Test Income"
|
||||
acc.parent_account = "Indirect Test Income - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.insert()
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Ledger Merge",
|
||||
"company": "_Test Company",
|
||||
"root_type": frappe.db.get_value("Account", "Indirect Income - _TC", "root_type"),
|
||||
"account": "Indirect Income - _TC",
|
||||
"merge_accounts": [
|
||||
{
|
||||
"account": "Indirect Test Income - _TC",
|
||||
"account_name": "Indirect Test Income"
|
||||
},
|
||||
{
|
||||
"account": "Administrative Test Income - _TC",
|
||||
"account_name": "Administrative Test Income"
|
||||
}
|
||||
]
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
|
||||
self.assertEqual(parent, "Indirect Test Income - _TC")
|
||||
|
||||
start_merge(doc.name)
|
||||
|
||||
parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
|
||||
self.assertEqual(parent, "Indirect Income - _TC")
|
||||
|
||||
self.assertFalse(frappe.db.exists("Account", "Indirect Test Income - _TC"))
|
||||
self.assertTrue(frappe.db.exists("Account", "Administrative Test Income - _TC"))
|
||||
|
||||
def tearDown(self):
|
||||
for entry in frappe.db.get_all("Ledger Merge"):
|
||||
frappe.delete_doc("Ledger Merge", entry.name)
|
||||
|
||||
test_accounts = [
|
||||
"Indirect Test Expenses - _TC",
|
||||
"Administrative Test Expenses - _TC",
|
||||
"Indirect Test Income - _TC",
|
||||
"Administrative Test Income - _TC"
|
||||
]
|
||||
for account in test_accounts:
|
||||
frappe.delete_doc_if_exists("Account", account)
|
@ -0,0 +1,52 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-12-09 15:44:58.033398",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account",
|
||||
"account_name",
|
||||
"merged"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 4,
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"default": "0",
|
||||
"fieldname": "merged",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Merged",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 4,
|
||||
"fieldname": "account_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account Name",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-10 15:27:24.477139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Merge Accounts",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerMergeAccounts(Document):
|
||||
pass
|
@ -75,7 +75,7 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2019-07-25 14:57:33.187689",
|
||||
"modified": "2022-01-04 15:25:06.053187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool",
|
||||
|
@ -135,7 +135,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
|
||||
rate = flt(row.outstanding_amount) / flt(row.qty)
|
||||
|
||||
return frappe._dict({
|
||||
item_dict = frappe._dict({
|
||||
"uom": default_uom,
|
||||
"rate": rate or 0.0,
|
||||
"qty": row.qty,
|
||||
@ -146,6 +146,13 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"cost_center": cost_center
|
||||
})
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
item_dict.update({
|
||||
dimension: row.get(dimension)
|
||||
})
|
||||
|
||||
return item_dict
|
||||
|
||||
item = get_item_dict()
|
||||
|
||||
invoice = frappe._dict({
|
||||
@ -160,13 +167,14 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"is_pos": 0,
|
||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
||||
"update_stock": 0,
|
||||
"invoice_number": row.invoice_number
|
||||
"invoice_number": row.invoice_number,
|
||||
"disable_rounded_total": 1
|
||||
})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
for dimension in accounting_dimension:
|
||||
invoice.update({
|
||||
dimension: item.get(dimension)
|
||||
dimension: self.get(dimension) or item.get(dimension)
|
||||
})
|
||||
|
||||
return invoice
|
||||
|
@ -7,21 +7,26 @@ import frappe
|
||||
from frappe.cache_manager import clear_doctype_cache
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
create_dimension,
|
||||
disable_dimension,
|
||||
)
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
|
||||
test_dependencies = ["Customer", "Supplier"]
|
||||
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
|
||||
|
||||
class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||
make_company()
|
||||
create_dimension()
|
||||
|
||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
|
||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
|
||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
|
||||
party_1=party_1, party_2=party_2, invoice_number=invoice_number)
|
||||
party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department)
|
||||
doc.update(args)
|
||||
return doc.make_invoices()
|
||||
|
||||
@ -106,6 +111,19 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
doc = frappe.get_doc('Sales Invoice', inv)
|
||||
doc.cancel()
|
||||
|
||||
def test_opening_invoice_with_accounting_dimension(self):
|
||||
invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC')
|
||||
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status", "department"],
|
||||
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
|
||||
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
|
||||
|
||||
def tearDown(self):
|
||||
disable_dimension()
|
||||
|
||||
def get_opening_invoice_creation_dict(**args):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
company = args.get("company", "_Test Company")
|
||||
|
@ -2,7 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@ -12,6 +12,17 @@ class PartyLink(Document):
|
||||
frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."),
|
||||
title=_("Invalid Primary Role"))
|
||||
|
||||
existing_party_link = frappe.get_all('Party Link', {
|
||||
'primary_party': self.primary_party,
|
||||
'secondary_party': self.secondary_party
|
||||
}, pluck="primary_role")
|
||||
if existing_party_link:
|
||||
frappe.throw(_('{} {} is already linked with {} {}')
|
||||
.format(
|
||||
self.primary_role, bold(self.primary_party),
|
||||
self.secondary_role, bold(self.secondary_party)
|
||||
))
|
||||
|
||||
existing_party_link = frappe.get_all('Party Link', {
|
||||
'primary_party': self.secondary_party
|
||||
}, pluck="primary_role")
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import json
|
||||
from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, scrub, throw
|
||||
@ -1523,6 +1524,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
|
||||
if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']:
|
||||
pe.project = (doc.get('project') or
|
||||
reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items
|
||||
|
||||
if pe.party_type in ["Customer", "Supplier"]:
|
||||
bank_account = get_party_bank_account(pe.party_type, pe.party)
|
||||
pe.set("bank_account", bank_account)
|
||||
@ -1708,7 +1713,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
|
||||
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
total_discount = 0
|
||||
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
|
||||
eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
|
||||
has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
|
||||
|
||||
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||
for term in doc.payment_schedule:
|
||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
||||
if term.discount_type == 'Percentage':
|
||||
|
@ -291,7 +291,7 @@ class PaymentRequest(Document):
|
||||
if not status:
|
||||
return
|
||||
|
||||
shopping_cart_settings = frappe.get_doc("Shopping Cart Settings")
|
||||
shopping_cart_settings = frappe.get_doc("E Commerce Settings")
|
||||
|
||||
if status in ["Authorized", "Completed"]:
|
||||
redirect_to = None
|
||||
@ -435,13 +435,13 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
""", (ref_dt, ref_dn))
|
||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||
|
||||
def get_gateway_details(args):
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
"""return gateway and payment account of default payment gateway"""
|
||||
if args.get("payment_gateway_account"):
|
||||
return get_payment_gateway_account(args.get("payment_gateway_account"))
|
||||
|
||||
if args.order_type == "Shopping Cart":
|
||||
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account
|
||||
payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account
|
||||
return get_payment_gateway_account(payment_gateway_account)
|
||||
|
||||
gateway_account = get_payment_gateway_account({"is_default": 1})
|
||||
|
@ -15,6 +15,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_multi_mode_option,
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
|
||||
|
||||
|
||||
@ -41,7 +42,6 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_serialised_or_batched_item()
|
||||
self.validate_stock_availablility()
|
||||
self.validate_return_items_qty()
|
||||
self.validate_non_stock_items()
|
||||
self.set_status()
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.validate_pos()
|
||||
@ -124,9 +124,26 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
elif invalid_serial_nos:
|
||||
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
|
||||
def validate_pos_reserved_batch_qty(self, item):
|
||||
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
|
||||
|
||||
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
|
||||
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
|
||||
|
||||
bold_item_name = frappe.bold(item.item_name)
|
||||
bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
|
||||
bold_invalid_batch_no = frappe.bold(item.batch_no)
|
||||
|
||||
if (available_batch_qty - reserved_batch_qty) == 0:
|
||||
frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
|
||||
.format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
|
||||
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
|
||||
frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
|
||||
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
|
||||
|
||||
def validate_delivered_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
||||
@ -140,20 +157,40 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
|
||||
|
||||
def validate_invalid_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
error_msg = []
|
||||
invalid_serials, msg = "", ""
|
||||
for serial_no in serial_nos:
|
||||
if not frappe.db.exists('Serial No', serial_no):
|
||||
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
|
||||
msg = (_("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)))
|
||||
if invalid_serials:
|
||||
error_msg.append(msg)
|
||||
|
||||
if error_msg:
|
||||
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||
|
||||
if self.is_return or self.docstatus != 1:
|
||||
return
|
||||
|
||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||
for d in self.get('items'):
|
||||
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
|
||||
if is_service_item:
|
||||
return
|
||||
if d.serial_no:
|
||||
self.validate_pos_reserved_serial_nos(d)
|
||||
self.validate_delivered_serial_nos(d)
|
||||
self.validate_invalid_serial_nos(d)
|
||||
elif d.batch_no:
|
||||
self.validate_pos_reserved_batch_qty(d)
|
||||
else:
|
||||
if allow_negative_stock:
|
||||
if is_negative_stock_allowed(item_code=d.item_code):
|
||||
return
|
||||
|
||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
||||
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
|
||||
|
||||
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||
if flt(available_stock) <= 0:
|
||||
@ -224,14 +261,6 @@ class POSInvoice(SalesInvoice):
|
||||
.format(d.idx, bold_serial_no, bold_return_against)
|
||||
)
|
||||
|
||||
def validate_non_stock_items(self):
|
||||
for d in self.get("items"):
|
||||
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
|
||||
if not is_stock_item:
|
||||
if not frappe.db.exists('Product Bundle', d.item_code):
|
||||
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
|
||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||
|
||||
def validate_mode_of_payment(self):
|
||||
if len(self.payments) == 0:
|
||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||
@ -333,7 +362,6 @@ class POSInvoice(SalesInvoice):
|
||||
if not for_validate and not self.customer:
|
||||
self.customer = profile.customer
|
||||
|
||||
self.ignore_pricing_rule = profile.ignore_pricing_rule
|
||||
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
|
||||
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
||||
|
||||
@ -472,12 +500,18 @@ class POSInvoice(SalesInvoice):
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
||||
is_stock_item = True
|
||||
bin_qty = get_bin_qty(item_code, warehouse)
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
return bin_qty - pos_sales_qty
|
||||
return bin_qty - pos_sales_qty, is_stock_item
|
||||
else:
|
||||
is_stock_item = False
|
||||
if frappe.db.exists('Product Bundle', item_code):
|
||||
return get_bundle_availability(item_code, warehouse)
|
||||
return get_bundle_availability(item_code, warehouse), is_stock_item
|
||||
else:
|
||||
# Is a service item
|
||||
return 0, is_stock_item
|
||||
|
||||
|
||||
def get_bundle_availability(bundle_item_code, warehouse):
|
||||
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
|
||||
|
@ -354,6 +354,24 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos2.insert()
|
||||
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||
|
||||
def test_invalid_serial_no_validation(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
|
||||
se = make_serialized_item(company='_Test Company',
|
||||
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
|
||||
serial_nos = se.get("items")[0].serial_no + 'wrong'
|
||||
|
||||
pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
|
||||
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
|
||||
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
|
||||
item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1)
|
||||
|
||||
pos.get('items')[0].has_serial_no = 1
|
||||
pos.get('items')[0].serial_no = serial_nos
|
||||
pos.insert()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos.submit)
|
||||
|
||||
def test_loyalty_points(self):
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
get_loyalty_program_details_with_points,
|
||||
@ -521,6 +539,78 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 400)
|
||||
|
||||
def test_pos_batch_item_qty_validation(self):
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_batch_item_with_batch,
|
||||
)
|
||||
create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01')
|
||||
item = frappe.get_doc('Item', '_BATCH ITEM')
|
||||
batch = frappe.get_doc('Batch', 'TestBatch 01')
|
||||
batch.submit()
|
||||
item.batch_no = 'TestBatch 01'
|
||||
item.save()
|
||||
|
||||
se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01')
|
||||
|
||||
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
|
||||
pos_inv1.items[0].batch_no = 'TestBatch 01'
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
|
||||
pos_inv2.items[0].batch_no = 'TestBatch 01'
|
||||
pos_inv2.save()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos_inv2.submit)
|
||||
|
||||
#teardown
|
||||
pos_inv1.reload()
|
||||
pos_inv1.cancel()
|
||||
pos_inv1.delete()
|
||||
pos_inv2.reload()
|
||||
pos_inv2.delete()
|
||||
se.cancel()
|
||||
batch.reload()
|
||||
batch.cancel()
|
||||
batch.delete()
|
||||
|
||||
def test_ignore_pricing_rule(self):
|
||||
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
|
||||
|
||||
item_price = frappe.get_doc({
|
||||
'doctype': 'Item Price',
|
||||
'item_code': '_Test Item',
|
||||
'price_list': '_Test Price List',
|
||||
'price_list_rate': '450',
|
||||
})
|
||||
item_price.insert()
|
||||
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
|
||||
pr.save()
|
||||
|
||||
try:
|
||||
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
|
||||
# rate shouldn't change
|
||||
self.assertEquals(pos_inv.items[0].rate, 405)
|
||||
|
||||
pos_inv.ignore_pricing_rule = 1
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
|
||||
# rate should reset since pricing rules are ignored
|
||||
self.assertEquals(pos_inv.items[0].rate, 450)
|
||||
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.items[0].rate, 300)
|
||||
|
||||
finally:
|
||||
item_price.delete()
|
||||
pos_inv.delete()
|
||||
pr.delete()
|
||||
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
args = frappe._dict(args)
|
||||
pos_profile = None
|
||||
@ -557,7 +647,8 @@ def create_pos_invoice(**args):
|
||||
"income_account": args.income_account or "Sales - _TC",
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_no": args.serial_no
|
||||
"serial_no": args.serial_no,
|
||||
"batch_no": args.batch_no
|
||||
})
|
||||
|
||||
if not args.do_not_save:
|
||||
@ -570,3 +661,8 @@ def create_pos_invoice(**args):
|
||||
pos_inv.payment_schedule = []
|
||||
|
||||
return pos_inv
|
||||
|
||||
def make_batch_item(item_name):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
if not frappe.db.exists(item_name):
|
||||
return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1))
|
@ -84,12 +84,20 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.save()
|
||||
self.write_off_fractional_amount(sales_invoice, data)
|
||||
sales_invoice.submit()
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice.name
|
||||
|
||||
def write_off_fractional_amount(self, invoice, data):
|
||||
pos_invoice_grand_total = sum(d.grand_total for d in data)
|
||||
|
||||
if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
|
||||
invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
|
||||
invoice.save()
|
||||
|
||||
def process_merging_into_credit_note(self, data):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
@ -102,6 +110,7 @@ class POSInvoiceMergeLog(Document):
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
self.write_off_fractional_amount(credit_note, data)
|
||||
credit_note.submit()
|
||||
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
@ -135,9 +144,15 @@ class POSInvoiceMergeLog(Document):
|
||||
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
|
||||
found = True
|
||||
i.qty = i.qty + item.qty
|
||||
i.amount = i.amount + item.net_amount
|
||||
i.net_amount = i.amount
|
||||
i.base_amount = i.base_amount + item.base_net_amount
|
||||
i.base_net_amount = i.base_amount
|
||||
|
||||
if not found:
|
||||
item.rate = item.net_rate
|
||||
item.amount = item.net_amount
|
||||
item.base_amount = item.base_net_amount
|
||||
item.price_list_rate = 0
|
||||
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
|
||||
items.append(si_item)
|
||||
@ -169,6 +184,7 @@ class POSInvoiceMergeLog(Document):
|
||||
found = True
|
||||
if not found:
|
||||
payments.append(payment)
|
||||
|
||||
rounding_adjustment += doc.rounding_adjustment
|
||||
rounded_total += doc.rounded_total
|
||||
base_rounding_adjustment += doc.base_rounding_adjustment
|
||||
|
@ -12,6 +12,7 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
consolidate_pos_invoices,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
@ -150,3 +151,132 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
|
||||
def test_consolidation_round_off_error_1(self):
|
||||
'''
|
||||
Test round off error in consolidated invoice creation if POS Invoice has inclusive tax
|
||||
'''
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
make_stock_entry(
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
item_code="_Test Item",
|
||||
rate=8000,
|
||||
qty=10,
|
||||
)
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
|
||||
inv.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 7.5,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
|
||||
})
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
|
||||
inv2.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 7.5,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
|
||||
})
|
||||
inv2.insert()
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.outstanding_amount, 0)
|
||||
self.assertEqual(consolidated_invoice.status, 'Paid')
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
def test_consolidation_round_off_error_2(self):
|
||||
'''
|
||||
Test the same case as above but with an Unpaid POS Invoice
|
||||
'''
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
make_stock_entry(
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
item_code="_Test Item",
|
||||
rate=8000,
|
||||
qty=10,
|
||||
)
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
|
||||
inv.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 7.5,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
|
||||
})
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
|
||||
inv2.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 7.5,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
|
||||
})
|
||||
inv2.insert()
|
||||
inv2.submit()
|
||||
|
||||
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
|
||||
inv3.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000
|
||||
})
|
||||
inv3.insert()
|
||||
inv3.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
|
||||
self.assertNotEqual(consolidated_invoice.status, 'Paid')
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
@ -249,13 +249,17 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
||||
"free_item_data": [],
|
||||
"parent": args.parent,
|
||||
"parenttype": args.parenttype,
|
||||
"child_docname": args.get('child_docname')
|
||||
"child_docname": args.get('child_docname'),
|
||||
})
|
||||
|
||||
if args.ignore_pricing_rule or not args.item_code:
|
||||
if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"):
|
||||
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
|
||||
item_details, args.get('item_code'))
|
||||
item_details = remove_pricing_rule_for_item(
|
||||
args.get("pricing_rules"),
|
||||
item_details,
|
||||
item_code=args.get("item_code"),
|
||||
rate=args.get("price_list_rate"),
|
||||
)
|
||||
return item_details
|
||||
|
||||
update_args_for_pricing_rule(args)
|
||||
@ -308,8 +312,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
|
||||
if not doc: return item_details
|
||||
|
||||
elif args.get("pricing_rules"):
|
||||
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
|
||||
item_details, args.get('item_code'))
|
||||
item_details = remove_pricing_rule_for_item(
|
||||
args.get("pricing_rules"),
|
||||
item_details,
|
||||
item_code=args.get("item_code"),
|
||||
rate=args.get("price_list_rate"),
|
||||
)
|
||||
|
||||
return item_details
|
||||
|
||||
@ -390,7 +398,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
item_details[field] += (pricing_rule.get(field, 0)
|
||||
if pricing_rule else args.get(field, 0))
|
||||
|
||||
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
|
||||
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
get_applied_pricing_rules,
|
||||
get_pricing_rule_items,
|
||||
@ -403,6 +411,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
|
||||
if pricing_rule.rate_or_discount == 'Discount Percentage':
|
||||
item_details.discount_percentage = 0.0
|
||||
item_details.discount_amount = 0.0
|
||||
item_details.rate = rate or 0.0
|
||||
|
||||
if pricing_rule.rate_or_discount == 'Discount Amount':
|
||||
item_details.discount_amount = 0.0
|
||||
@ -421,6 +430,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
|
||||
item_details.applied_on_items = ','.join(items)
|
||||
|
||||
item_details.pricing_rules = ''
|
||||
item_details.pricing_rule_removed = True
|
||||
|
||||
return item_details
|
||||
|
||||
@ -432,9 +442,12 @@ def remove_pricing_rules(item_list):
|
||||
out = []
|
||||
for item in item_list:
|
||||
item = frappe._dict(item)
|
||||
if item.get('pricing_rules'):
|
||||
out.append(remove_pricing_rule_for_item(item.get("pricing_rules"),
|
||||
item, item.item_code))
|
||||
if item.get("pricing_rules"):
|
||||
out.append(
|
||||
remove_pricing_rule_for_item(
|
||||
item.get("pricing_rules"), item, item.item_code, item.get("price_list_rate")
|
||||
)
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
@ -166,7 +166,7 @@ class TestPricingRule(unittest.TestCase):
|
||||
"item_group": "Products",
|
||||
},
|
||||
{
|
||||
"item_group": "Seed",
|
||||
"item_group": "_Test Item Group",
|
||||
},
|
||||
],
|
||||
"selling": 1,
|
||||
@ -628,6 +628,67 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_remove_pricing_rule(self):
|
||||
item = make_item("Water Flask")
|
||||
make_item_price("Water Flask", "_Test Price List", 100)
|
||||
|
||||
pricing_rule_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Water Flask Rule",
|
||||
"apply_on": "Item Code",
|
||||
"price_or_product_discount": "Price",
|
||||
"items": [{
|
||||
"item_code": "Water Flask",
|
||||
}],
|
||||
"selling": 1,
|
||||
"currency": "INR",
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"discount_percentage": 20,
|
||||
"company": "_Test Company"
|
||||
}
|
||||
rule = frappe.get_doc(pricing_rule_record)
|
||||
rule.insert()
|
||||
|
||||
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
|
||||
si.selling_price_list = "_Test Price List"
|
||||
si.save()
|
||||
|
||||
self.assertEqual(si.items[0].price_list_rate, 100)
|
||||
self.assertEqual(si.items[0].discount_percentage, 20)
|
||||
self.assertEqual(si.items[0].rate, 80)
|
||||
|
||||
si.ignore_pricing_rule = 1
|
||||
si.save()
|
||||
|
||||
self.assertEqual(si.items[0].discount_percentage, 0)
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
si.delete()
|
||||
rule.delete()
|
||||
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
|
||||
item.delete()
|
||||
|
||||
def test_multiple_pricing_rules_with_min_qty(self):
|
||||
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
|
||||
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
|
||||
make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
|
||||
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
|
||||
item = si.items[0]
|
||||
item.stock_qty = 1
|
||||
si.save()
|
||||
self.assertFalse(item.discount_percentage)
|
||||
item.qty = 5
|
||||
item.stock_qty = 5
|
||||
si.save()
|
||||
self.assertEqual(item.discount_percentage, 30)
|
||||
si.delete()
|
||||
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
def make_pricing_rule(**args):
|
||||
@ -650,7 +711,7 @@ def make_pricing_rule(**args):
|
||||
"rate": args.rate or 0.0,
|
||||
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
|
||||
"condition": args.condition or '',
|
||||
"priority": 1,
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
|
||||
})
|
||||
@ -676,6 +737,8 @@ def make_pricing_rule(**args):
|
||||
if args.get(applicable_for):
|
||||
doc.db_set(applicable_for, args.get(applicable_for))
|
||||
|
||||
return doc
|
||||
|
||||
def setup_pricing_rule_data():
|
||||
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
||||
frappe.get_doc({
|
||||
|
@ -73,7 +73,7 @@ def sorted_by_priority(pricing_rules, args, doc=None):
|
||||
for key in sorted(pricing_rule_dict):
|
||||
pricing_rules_list.extend(pricing_rule_dict.get(key))
|
||||
|
||||
return pricing_rules_list or pricing_rules
|
||||
return pricing_rules_list
|
||||
|
||||
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
|
||||
filtered_pricing_rules = []
|
||||
|
@ -178,8 +178,8 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(
|
||||
_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
|
||||
.format(frappe.bold("Credit To")), title=_("Invalid Account")
|
||||
_("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.")
|
||||
.format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account")
|
||||
)
|
||||
|
||||
self.party_account_currency = account.account_currency
|
||||
@ -505,11 +505,11 @@ class PurchaseInvoice(BuyingController):
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
||||
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
|
||||
else self.base_grand_total, self.precision("base_grand_total"))
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
|
||||
self.precision("grand_total"))
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": self.credit_to,
|
||||
@ -517,8 +517,8 @@ class PurchaseInvoice(BuyingController):
|
||||
"party": self.supplier,
|
||||
"due_date": self.due_date,
|
||||
"against": self.against_expense_account,
|
||||
"credit": grand_total_in_company_currency,
|
||||
"credit_in_account_currency": grand_total_in_company_currency \
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_grand_total \
|
||||
if self.party_account_currency==self.company_currency else grand_total,
|
||||
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
@ -537,8 +537,11 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
voucher_wise_stock_value = {}
|
||||
if self.update_stock:
|
||||
for d in frappe.get_all('Stock Ledger Entry',
|
||||
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}):
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry",
|
||||
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"],
|
||||
filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}
|
||||
)
|
||||
for d in stock_ledger_entries:
|
||||
voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
|
||||
|
||||
valuation_tax_accounts = [d.account_head for d in self.get("taxes")
|
||||
@ -548,6 +551,10 @@ class PurchaseInvoice(BuyingController):
|
||||
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
|
||||
|
||||
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||
provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \
|
||||
'enable_provisional_accounting_for_non_stock_items'))
|
||||
|
||||
purchase_receipt_doc_map = {}
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
@ -643,19 +650,23 @@ class PurchaseInvoice(BuyingController):
|
||||
else:
|
||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
||||
|
||||
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
|
||||
|
||||
if auto_accounting_for_non_stock_items:
|
||||
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
provisional_account = self.get_company_default("default_provisional_account")
|
||||
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
|
||||
|
||||
if not purchase_receipt_doc:
|
||||
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
|
||||
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
|
||||
|
||||
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
|
||||
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
|
||||
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
|
||||
'account':service_received_but_not_billed_account}, ['name'])
|
||||
'account':provisional_account}, ['name'])
|
||||
|
||||
if expense_booked_in_pr:
|
||||
expense_account = service_received_but_not_billed_account
|
||||
# Intentionally passing purchase invoice item to handle partial billing
|
||||
purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
gl_entries.append(self.get_gl_dict({
|
||||
|
@ -56,4 +56,14 @@ frappe.listview_settings["Purchase Invoice"] = {
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(listview) {
|
||||
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
get_taxes,
|
||||
make_purchase_receipt,
|
||||
@ -986,7 +991,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = '2019-03-15'
|
||||
pi.posting_date = '2019-01-10'
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2019-01-10"
|
||||
pi.items[0].service_end_date = "2019-03-15"
|
||||
@ -1147,8 +1152,6 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
# create a new supplier to test
|
||||
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
|
||||
@ -1221,6 +1224,45 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
payment_entry.load_from_db()
|
||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||
|
||||
def test_provisional_accounting_entry(self):
|
||||
item = create_item("_Test Non Stock Item", is_stock_item=0)
|
||||
provisional_account = create_account(account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
|
||||
company = frappe.get_doc('Company', '_Test Company')
|
||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||
company.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
|
||||
pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, -1)
|
||||
pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
expected_gle = [
|
||||
['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
|
||||
['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 250, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.save()
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
@ -1236,7 +1278,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
def update_tax_witholding_category(company, account):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
fiscal_year = get_fiscal_year(fiscal_year='2021')
|
||||
fiscal_year = get_fiscal_year(date=nowdate())
|
||||
|
||||
if not frappe.db.get_value('Tax Withholding Rate',
|
||||
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
|
||||
|
@ -1,6 +1,8 @@
|
||||
{% include "erpnext/regional/india/taxes.js" %}
|
||||
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
|
||||
|
||||
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
||||
erpnext.setup_einvoice_actions('Sales Invoice')
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", {
|
||||
setup: function(frm) {
|
||||
|
@ -36,4 +36,139 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
|
||||
};
|
||||
|
||||
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
|
||||
|
||||
const generate_irns = () => {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
if (docnames && docnames.length) {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
|
||||
args: { docnames },
|
||||
freeze: true,
|
||||
freeze_message: __('Generating E-Invoices...')
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
message: __('Please select at least one sales invoice to generate IRN'),
|
||||
title: __('No Invoice Selected'),
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const cancel_irns = () => {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
|
||||
const fields = [
|
||||
{
|
||||
"label": "Reason",
|
||||
"fieldname": "reason",
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"default": "1-Duplicate",
|
||||
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||
},
|
||||
{
|
||||
"label": "Remark",
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1
|
||||
}
|
||||
];
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __("Cancel IRN"),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
const data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
|
||||
args: {
|
||||
doctype: list_view.doctype,
|
||||
docnames,
|
||||
reason: data.reason.split('-')[0],
|
||||
remark: data.remark
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Cancelling E-Invoices...'),
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
});
|
||||
d.show();
|
||||
};
|
||||
|
||||
let einvoicing_enabled = false;
|
||||
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
|
||||
einvoicing_enabled = enabled;
|
||||
});
|
||||
|
||||
list_view.$result.on("change", "input[type=checkbox]", () => {
|
||||
if (einvoicing_enabled) {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
// show/hide e-invoicing actions when no sales invoices are checked
|
||||
if (docnames && docnames.length) {
|
||||
// prevent adding actions twice if e-invoicing action group already exists
|
||||
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
|
||||
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
|
||||
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
|
||||
}
|
||||
} else {
|
||||
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
|
||||
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
|
||||
const { failures, user, invoices } = data;
|
||||
|
||||
if (invoices.length != failures.length) {
|
||||
frappe.msgprint({
|
||||
message: __('{0} e-invoices generated successfully', [invoices.length]),
|
||||
title: __('Bulk E-Invoice Generation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
|
||||
if (failures && failures.length && user == frappe.session.user) {
|
||||
let message = `
|
||||
Failed to generate IRNs for following ${failures.length} sales invoices:
|
||||
<ul style="padding-left: 20px; padding-top: 5px;">
|
||||
${failures.map(d => `<li>${d.docname}</li>`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
frappe.msgprint({
|
||||
message: message,
|
||||
title: __('Bulk E-Invoice Generation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
|
||||
const { failures, user, invoices } = data;
|
||||
|
||||
if (invoices.length != failures.length) {
|
||||
frappe.msgprint({
|
||||
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
|
||||
title: __('Bulk E-Invoice Cancellation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
|
||||
if (failures && failures.length && user == frappe.session.user) {
|
||||
let message = `
|
||||
Failed to cancel IRNs for following ${failures.length} sales invoices:
|
||||
<ul style="padding-left: 20px; padding-top: 5px;">
|
||||
${failures.map(d => `<li>${d.docname}</li>`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
frappe.msgprint({
|
||||
message: message,
|
||||
title: __('Bulk E-Invoice Cancellation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -469,7 +469,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
let row = frappe.get_doc(d.doctype, d.name)
|
||||
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
|
||||
});
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
this.frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -651,7 +651,7 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"permlevel": 0,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@ -2038,7 +2038,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-21 20:19:38.667508",
|
||||
"modified": "2021-12-23 20:19:38.667508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -43,6 +43,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s
|
||||
from erpnext.stock.doctype.batch.batch import set_batch_nos
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
|
||||
from erpnext.stock.utils import calculate_mapped_packed_items_return
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@ -284,7 +285,7 @@ class SalesInvoice(SellingController):
|
||||
filters={ invoice_or_credit_note: self.name },
|
||||
pluck="pos_closing_entry"
|
||||
)
|
||||
if pos_closing_entry:
|
||||
if pos_closing_entry and pos_closing_entry[0]:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
|
||||
@ -293,6 +294,8 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def before_cancel(self):
|
||||
self.check_if_consolidated_invoice()
|
||||
|
||||
super(SalesInvoice, self).before_cancel()
|
||||
self.update_time_sheet(None)
|
||||
|
||||
def on_cancel(self):
|
||||
@ -569,7 +572,10 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
|
||||
msg = _("Please ensure {} account {} is a Receivable account.").format(
|
||||
frappe.bold("Debit To"),
|
||||
frappe.bold(self.debit_to)
|
||||
) + " "
|
||||
msg += _("Change the account type to Receivable or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
@ -728,8 +734,11 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def update_packing_list(self):
|
||||
if cint(self.update_stock) == 1:
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
if cint(self.is_return) and self.return_against:
|
||||
calculate_mapped_packed_items_return(self)
|
||||
else:
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
else:
|
||||
self.set('packed_items', [])
|
||||
|
||||
@ -862,11 +871,11 @@ class SalesInvoice(SellingController):
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
||||
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
|
||||
else self.base_grand_total, self.precision("base_grand_total"))
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
# Didnot use base_grand_total to book rounding loss gle
|
||||
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
|
||||
self.precision("grand_total"))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": self.debit_to,
|
||||
@ -874,8 +883,8 @@ class SalesInvoice(SellingController):
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.against_income_account,
|
||||
"debit": grand_total_in_company_currency,
|
||||
"debit_in_account_currency": grand_total_in_company_currency \
|
||||
"debit": base_grand_total,
|
||||
"debit_in_account_currency": base_grand_total \
|
||||
if self.party_account_currency==self.company_currency else grand_total,
|
||||
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
@ -1243,14 +1252,14 @@ class SalesInvoice(SellingController):
|
||||
def update_billing_status_in_dn(self, update_modified=True):
|
||||
updated_delivery_notes = []
|
||||
for d in self.get("items"):
|
||||
if d.dn_detail:
|
||||
if d.so_detail:
|
||||
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
|
||||
elif d.dn_detail:
|
||||
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
|
||||
where dn_detail=%s and docstatus=1""", d.dn_detail)
|
||||
billed_amt = billed_amt and billed_amt[0][0] or 0
|
||||
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
|
||||
updated_delivery_notes.append(d.delivery_note)
|
||||
elif d.so_detail:
|
||||
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
|
||||
|
||||
for dn in set(updated_delivery_notes):
|
||||
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)
|
||||
|
@ -21,5 +21,15 @@ frappe.listview_settings['Sales Invoice'] = {
|
||||
};
|
||||
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
|
||||
},
|
||||
right_column: "grand_total"
|
||||
right_column: "grand_total",
|
||||
|
||||
onload: function(listview) {
|
||||
listview.page.add_action_item(__("Delivery Note"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
|
||||
from erpnext.accounts.utils import PaymentEntryUnlinkError
|
||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.controllers.accounts_controller import update_invoice_status
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
from erpnext.regional.india.utils import get_ewb_data
|
||||
@ -1780,47 +1781,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
||||
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
item.no_of_months = 12
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2019-01-10"
|
||||
si.items[0].service_end_date = "2019-03-15"
|
||||
si.items[0].deferred_revenue_account = deferred_account
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
|
||||
|
||||
pda1 = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company"
|
||||
))
|
||||
|
||||
pda1.insert()
|
||||
self.assertRaises(frappe.ValidationError, pda1.submit)
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
||||
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
@ -2140,6 +2100,54 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
|
||||
self.assertEqual(data['billLists'][0]['fromStateCode'],27)
|
||||
|
||||
def test_einvoice_submission_without_irn(self):
|
||||
# init
|
||||
einvoice_settings = frappe.get_doc('E Invoice Settings')
|
||||
einvoice_settings.enable = 1
|
||||
einvoice_settings.applicable_from = nowdate()
|
||||
einvoice_settings.append('credentials', {
|
||||
'company': '_Test Company',
|
||||
'gstin': '27AAECE4835E1ZR',
|
||||
'username': 'test',
|
||||
'password': 'test'
|
||||
})
|
||||
einvoice_settings.save()
|
||||
|
||||
country = frappe.flags.country
|
||||
frappe.flags.country = 'India'
|
||||
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
si.irn = 'test_irn'
|
||||
si.submit()
|
||||
|
||||
# reset
|
||||
einvoice_settings = frappe.get_doc('E Invoice Settings')
|
||||
einvoice_settings.enable = 0
|
||||
frappe.flags.country = country
|
||||
|
||||
def test_einvoice_json(self):
|
||||
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
|
||||
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.discount_amount = 100
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
self.assertTrue(einvoice['EwbDtls'])
|
||||
validate_totals(einvoice)
|
||||
|
||||
si.apply_discount_on = 'Net Total'
|
||||
si.save()
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
[d.set('included_in_print_rate', 1) for d in si.taxes]
|
||||
si.save()
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
@ -2232,9 +2240,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48],
|
||||
["2021-06-30", 20000.0, 21311.48],
|
||||
["2021-09-30", 5041.1, 26352.58]
|
||||
["2020-06-30", 1366.12, 1366.12],
|
||||
["2021-06-30", 20000.0, 21366.12],
|
||||
["2021-09-30", 5041.1, 26407.22]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@ -2282,12 +2290,12 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48, True],
|
||||
["2021-06-30", 20000.0, 21311.48, True],
|
||||
["2022-06-30", 20000.0, 41311.48, False],
|
||||
["2023-06-30", 20000.0, 61311.48, False],
|
||||
["2024-06-30", 20000.0, 81311.48, False],
|
||||
["2025-06-06", 18688.52, 100000.0, False]
|
||||
["2020-06-30", 1366.12, 1366.12, True],
|
||||
["2021-06-30", 20000.0, 21366.12, True],
|
||||
["2022-06-30", 20000.0, 41366.12, False],
|
||||
["2023-06-30", 20000.0, 61366.12, False],
|
||||
["2024-06-30", 20000.0, 81366.12, False],
|
||||
["2025-06-06", 18633.88, 100000.0, False]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@ -2385,6 +2393,41 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
|
||||
def test_update_invoice_status(self):
|
||||
today = nowdate()
|
||||
|
||||
# Sales Invoice without Payment Schedule
|
||||
si = create_sales_invoice(posting_date=add_days(today, -5))
|
||||
|
||||
# Sales Invoice with Payment Schedule
|
||||
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
|
||||
si_with_payment_schedule.extend("payment_schedule", [
|
||||
{
|
||||
"due_date": add_days(today, -5),
|
||||
"invoice_portion": 50,
|
||||
"payment_amount": si_with_payment_schedule.grand_total / 2
|
||||
},
|
||||
{
|
||||
"due_date": add_days(today, 5),
|
||||
"invoice_portion": 50,
|
||||
"payment_amount": si_with_payment_schedule.grand_total / 2
|
||||
}
|
||||
])
|
||||
si_with_payment_schedule.submit()
|
||||
|
||||
|
||||
for invoice in (si, si_with_payment_schedule):
|
||||
invoice.db_set("status", "Unpaid")
|
||||
update_invoice_status()
|
||||
invoice.reload()
|
||||
self.assertEqual(invoice.status, "Overdue")
|
||||
|
||||
invoice.db_set("status", "Unpaid and Discounted")
|
||||
update_invoice_status()
|
||||
invoice.reload()
|
||||
self.assertEqual(invoice.status, "Overdue and Discounted")
|
||||
|
||||
|
||||
def test_sales_commission(self):
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
item = copy.deepcopy(si.get('items')[0])
|
||||
@ -2446,6 +2489,74 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
|
||||
|
||||
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
|
||||
acc_settings = frappe.get_single('Accounts Settings')
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 1
|
||||
acc_settings.submit_journal_entries = 1
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
|
||||
item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
|
||||
|
||||
si.set_posting_time = 1
|
||||
si.posting_date = '2019-01-01'
|
||||
si.debit_to = '_Test Receivable USD - _TC'
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2019-01-01"
|
||||
si.items[0].service_end_date = "2019-03-30"
|
||||
si.items[0].deferred_expense_account = deferred_account
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
|
||||
|
||||
pda1 = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company"
|
||||
))
|
||||
|
||||
pda1.insert()
|
||||
pda1.submit()
|
||||
|
||||
expected_gle = [
|
||||
["Sales - _TC", 0.0, 2089.89, "2019-01-28"],
|
||||
[deferred_account, 2089.89, 0.0, "2019-01-28"],
|
||||
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
|
||||
[deferred_account, 1887.64, 0.0, "2019-02-28"],
|
||||
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
|
||||
[deferred_account, 2022.47, 0.0, "2019-03-15"]
|
||||
]
|
||||
|
||||
gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
|
||||
order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.credit)
|
||||
self.assertEqual(expected_gle[i][2], gle.debit)
|
||||
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
acc_settings = frappe.get_single('Accounts Settings')
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 0
|
||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
|
@ -71,7 +71,8 @@ class ShippingRule(Document):
|
||||
if doc.currency != doc.company_currency:
|
||||
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
|
||||
|
||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||
if shipping_amount:
|
||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||
|
||||
def get_shipping_amount_from_rules(self, value):
|
||||
for condition in self.get("conditions"):
|
||||
|
@ -2,12 +2,13 @@
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2018-11-22 23:38:39.668804",
|
||||
"creation": "2022-01-19 01:09:28.920486",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title"
|
||||
"title",
|
||||
"disabled"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -18,14 +19,21 @@
|
||||
"label": "Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-03 11:50:38.748872",
|
||||
"modified": "2022-01-18 21:13:41.161017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Category",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -65,5 +73,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -98,7 +98,7 @@ class TaxRule(Document):
|
||||
def validate_use_for_shopping_cart(self):
|
||||
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
|
||||
if (not self.use_for_shopping_cart
|
||||
and cint(frappe.db.get_single_value('Shopping Cart Settings', 'enabled'))
|
||||
and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled'))
|
||||
and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
|
||||
|
||||
self.use_for_shopping_cart = 1
|
||||
|
@ -28,14 +28,14 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "single_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Single Transaction Threshold"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"fieldname": "cumulative_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Cumulative Transaction Threshold"
|
||||
},
|
||||
@ -59,7 +59,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-31 11:42:12.213977",
|
||||
"modified": "2022-01-13 12:04:42.904263",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Rate",
|
||||
@ -68,5 +68,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -2,15 +2,17 @@
|
||||
"creation": "2021-08-24 12:28:18.044902",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"first_document": 0,
|
||||
"idx": 0,
|
||||
"include_name_field": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-08-24 12:28:18.044902",
|
||||
"modified": "2022-01-18 18:32:17.102330",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges Template",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Sales Taxes and Charges Template",
|
||||
"save_on_complete": 0,
|
||||
"save_on_complete": 1,
|
||||
"steps": [
|
||||
{
|
||||
"description": "A name by which you will identify this template. You can change this later.",
|
||||
|
@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
@ -51,49 +53,57 @@ def validate_accounting_period(gl_map):
|
||||
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
|
||||
|
||||
def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
if not gl_map:
|
||||
return []
|
||||
|
||||
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
|
||||
|
||||
if merge_entries:
|
||||
gl_map = merge_similar_entries(gl_map, precision)
|
||||
for entry in gl_map:
|
||||
# toggle debit, credit if negative entry
|
||||
if flt(entry.debit) < 0:
|
||||
entry.credit = flt(entry.credit) - flt(entry.debit)
|
||||
entry.debit = 0.0
|
||||
|
||||
if flt(entry.debit_in_account_currency) < 0:
|
||||
entry.credit_in_account_currency = \
|
||||
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
|
||||
entry.debit_in_account_currency = 0.0
|
||||
|
||||
if flt(entry.credit) < 0:
|
||||
entry.debit = flt(entry.debit) - flt(entry.credit)
|
||||
entry.credit = 0.0
|
||||
|
||||
if flt(entry.credit_in_account_currency) < 0:
|
||||
entry.debit_in_account_currency = \
|
||||
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
|
||||
entry.credit_in_account_currency = 0.0
|
||||
|
||||
update_net_values(entry)
|
||||
gl_map = toggle_debit_credit_if_negative(gl_map)
|
||||
|
||||
return gl_map
|
||||
|
||||
def update_net_values(entry):
|
||||
# In some scenarios net value needs to be shown in the ledger
|
||||
# This method updates net values as debit or credit
|
||||
if entry.post_net_value and entry.debit and entry.credit:
|
||||
if entry.debit > entry.credit:
|
||||
entry.debit = entry.debit - entry.credit
|
||||
entry.debit_in_account_currency = entry.debit_in_account_currency \
|
||||
- entry.credit_in_account_currency
|
||||
entry.credit = 0
|
||||
entry.credit_in_account_currency = 0
|
||||
else:
|
||||
entry.credit = entry.credit - entry.debit
|
||||
entry.credit_in_account_currency = entry.credit_in_account_currency \
|
||||
- entry.debit_in_account_currency
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
|
||||
if not cost_center_allocation:
|
||||
return gl_map
|
||||
|
||||
entry.debit = 0
|
||||
entry.debit_in_account_currency = 0
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
if cost_center and cost_center_allocation.get(cost_center):
|
||||
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
else:
|
||||
new_gl_map.append(d)
|
||||
|
||||
return new_gl_map
|
||||
|
||||
def get_cost_center_allocation_data(company, posting_date):
|
||||
par = frappe.qb.DocType("Cost Center Allocation")
|
||||
child = frappe.qb.DocType("Cost Center Allocation Percentage")
|
||||
|
||||
records = (
|
||||
frappe.qb.from_(par).inner_join(child).on(par.name == child.parent)
|
||||
.select(par.main_cost_center, child.cost_center, child.percentage)
|
||||
.where(par.docstatus == 1)
|
||||
.where(par.company == company)
|
||||
.where(par.valid_from <= posting_date)
|
||||
.orderby(par.valid_from, order=frappe.qb.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
cc_allocation = frappe._dict()
|
||||
for d in records:
|
||||
cc_allocation.setdefault(d.main_cost_center, frappe._dict())\
|
||||
.setdefault(d.cost_center, d.percentage)
|
||||
|
||||
return cc_allocation
|
||||
|
||||
def merge_similar_entries(gl_map, precision=None):
|
||||
merged_gl_map = []
|
||||
@ -145,6 +155,49 @@ def check_if_in_list(gle, gl_map, dimensions=None):
|
||||
if same_head:
|
||||
return e
|
||||
|
||||
def toggle_debit_credit_if_negative(gl_map):
|
||||
for entry in gl_map:
|
||||
# toggle debit, credit if negative entry
|
||||
if flt(entry.debit) < 0:
|
||||
entry.credit = flt(entry.credit) - flt(entry.debit)
|
||||
entry.debit = 0.0
|
||||
|
||||
if flt(entry.debit_in_account_currency) < 0:
|
||||
entry.credit_in_account_currency = \
|
||||
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
|
||||
entry.debit_in_account_currency = 0.0
|
||||
|
||||
if flt(entry.credit) < 0:
|
||||
entry.debit = flt(entry.debit) - flt(entry.credit)
|
||||
entry.credit = 0.0
|
||||
|
||||
if flt(entry.credit_in_account_currency) < 0:
|
||||
entry.debit_in_account_currency = \
|
||||
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
|
||||
entry.credit_in_account_currency = 0.0
|
||||
|
||||
update_net_values(entry)
|
||||
|
||||
return gl_map
|
||||
|
||||
def update_net_values(entry):
|
||||
# In some scenarios net value needs to be shown in the ledger
|
||||
# This method updates net values as debit or credit
|
||||
if entry.post_net_value and entry.debit and entry.credit:
|
||||
if entry.debit > entry.credit:
|
||||
entry.debit = entry.debit - entry.credit
|
||||
entry.debit_in_account_currency = entry.debit_in_account_currency \
|
||||
- entry.credit_in_account_currency
|
||||
entry.credit = 0
|
||||
entry.credit_in_account_currency = 0
|
||||
else:
|
||||
entry.credit = entry.credit - entry.debit
|
||||
entry.credit_in_account_currency = entry.credit_in_account_currency \
|
||||
- entry.debit_in_account_currency
|
||||
|
||||
entry.debit = 0
|
||||
entry.debit_in_account_currency = 0
|
||||
|
||||
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
if not from_repost:
|
||||
validate_cwip_accounts(gl_map)
|
||||
@ -266,13 +319,18 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
|
||||
"""
|
||||
|
||||
if not gl_entries:
|
||||
gl_entries = frappe.get_all("GL Entry",
|
||||
fields = ["*"],
|
||||
filters = {
|
||||
"voucher_type": voucher_type,
|
||||
"voucher_no": voucher_no,
|
||||
"is_cancelled": 0
|
||||
})
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (frappe.qb.from_(
|
||||
gl_entry
|
||||
).select(
|
||||
'*'
|
||||
).where(
|
||||
gl_entry.voucher_type == voucher_type
|
||||
).where(
|
||||
gl_entry.voucher_no == voucher_no
|
||||
).where(
|
||||
gl_entry.is_cancelled == 0
|
||||
).for_update()).run(as_dict=1)
|
||||
|
||||
if gl_entries:
|
||||
validate_accounting_period(gl_entries)
|
||||
@ -280,23 +338,24 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
|
||||
set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
|
||||
|
||||
for entry in gl_entries:
|
||||
entry['name'] = None
|
||||
debit = entry.get('debit', 0)
|
||||
credit = entry.get('credit', 0)
|
||||
new_gle = copy.deepcopy(entry)
|
||||
new_gle['name'] = None
|
||||
debit = new_gle.get('debit', 0)
|
||||
credit = new_gle.get('credit', 0)
|
||||
|
||||
debit_in_account_currency = entry.get('debit_in_account_currency', 0)
|
||||
credit_in_account_currency = entry.get('credit_in_account_currency', 0)
|
||||
debit_in_account_currency = new_gle.get('debit_in_account_currency', 0)
|
||||
credit_in_account_currency = new_gle.get('credit_in_account_currency', 0)
|
||||
|
||||
entry['debit'] = credit
|
||||
entry['credit'] = debit
|
||||
entry['debit_in_account_currency'] = credit_in_account_currency
|
||||
entry['credit_in_account_currency'] = debit_in_account_currency
|
||||
new_gle['debit'] = credit
|
||||
new_gle['credit'] = debit
|
||||
new_gle['debit_in_account_currency'] = credit_in_account_currency
|
||||
new_gle['credit_in_account_currency'] = debit_in_account_currency
|
||||
|
||||
entry['remarks'] = "On cancellation of " + entry['voucher_no']
|
||||
entry['is_cancelled'] = 1
|
||||
new_gle['remarks'] = "On cancellation of " + new_gle['voucher_no']
|
||||
new_gle['is_cancelled'] = 1
|
||||
|
||||
if entry['debit'] or entry['credit']:
|
||||
make_entry(entry, adv_adj, "Yes")
|
||||
if new_gle['debit'] or new_gle['credit']:
|
||||
make_entry(new_gle, adv_adj, "Yes")
|
||||
|
||||
|
||||
def check_freezing_date(posting_date, adv_adj=False):
|
||||
|
@ -13,15 +13,12 @@
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"modified": "2021-08-13 11:59:35.690443",
|
||||
"modified": "2022-01-18 18:35:52.326688",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts",
|
||||
"owner": "Administrator",
|
||||
"steps": [
|
||||
{
|
||||
"step": "Company"
|
||||
},
|
||||
{
|
||||
"step": "Chart of Accounts"
|
||||
},
|
||||
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"action": "Go to Page",
|
||||
"action_label": "Let's Review your Company",
|
||||
"creation": "2021-06-29 14:47:42.497318",
|
||||
"description": "# Company\n\nIn ERPNext, you can also create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company. \n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-08-13 11:43:35.767341",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Company",
|
||||
"owner": "Administrator",
|
||||
"path": "app/company",
|
||||
"reference_document": "Company",
|
||||
"show_form_tour": 0,
|
||||
"show_full_form": 0,
|
||||
"title": "Review Company",
|
||||
"validate_action": 1
|
||||
}
|
@ -58,7 +58,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
|
||||
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
|
||||
|
||||
party = frappe.get_doc(party_type, party)
|
||||
currency = party.default_currency if party.get("default_currency") else get_company_currency(company)
|
||||
currency = party.get("default_currency") or currency or get_company_currency(company)
|
||||
|
||||
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
|
||||
set_contact_details(party_details, party, party_type)
|
||||
|
173
erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
Normal file
173
erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
Normal file
@ -0,0 +1,173 @@
|
||||
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
|
||||
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
|
||||
|
||||
<div class="page-break">
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
{% if letter_head and not no_letterhead %}
|
||||
<div class="letter-head">{{ letter_head }}</div>
|
||||
{% endif %}
|
||||
<div class="print-heading">
|
||||
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
|
||||
</div>
|
||||
</div>
|
||||
{% if print_settings.repeat_header_footer %}
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if not no_letterhead and footer %}
|
||||
<div class="letter-head-footer">
|
||||
{{ footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-center small page-number visible-pdf">
|
||||
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
|
||||
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
|
||||
<div class="col-xs-8 column-break">
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>IRN</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Ack. No</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Ack. Date</label></div>
|
||||
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Category</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Document Type</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Document No</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-4 column-break">
|
||||
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
|
||||
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
|
||||
{%- set seller = einvoice.SellerDtls -%}
|
||||
<div class="col-xs-6 column-break">
|
||||
<h5 style="margin-bottom: 5px;">Seller</h5>
|
||||
<p>{{ seller.Gstin }}</p>
|
||||
<p>{{ seller.LglNm }}</p>
|
||||
<p>{{ seller.Addr1 }}</p>
|
||||
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
|
||||
<p>{{ seller.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
|
||||
|
||||
{%- if einvoice.ShipDtls -%}
|
||||
{%- set shipping = einvoice.ShipDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Shipped From</h5>
|
||||
<p>{{ shipping.Gstin }}</p>
|
||||
<p>{{ shipping.LglNm }}</p>
|
||||
<p>{{ shipping.Addr1 }}</p>
|
||||
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
|
||||
<p>{{ shipping.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{%- set buyer = einvoice.BuyerDtls -%}
|
||||
<div class="col-xs-6 column-break">
|
||||
<h5 style="margin-bottom: 5px;">Buyer</h5>
|
||||
<p>{{ buyer.Gstin }}</p>
|
||||
<p>{{ buyer.LglNm }}</p>
|
||||
<p>{{ buyer.Addr1 }}</p>
|
||||
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
|
||||
<p>{{ buyer.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
|
||||
|
||||
{%- if einvoice.DispDtls -%}
|
||||
{%- set dispatch = einvoice.DispDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Dispatched From</h5>
|
||||
{%- if dispatch.Gstin -%} <p>{{ dispatch.Gstin }}</p> {% endif %}
|
||||
<p>{{ dispatch.LglNm }}</p>
|
||||
<p>{{ dispatch.Addr1 }}</p>
|
||||
{%- if dispatch.Addr2 -%} <p>{{ dispatch.Addr2 }}</p> {% endif %}
|
||||
<p>{{ dispatch.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x: auto;">
|
||||
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left" style="width: 3%;">Sr. No.</th>
|
||||
<th class="text-left">Item</th>
|
||||
<th class="text-left" style="width: 10%;">HSN Code</th>
|
||||
<th class="text-left" style="width: 5%;">Qty</th>
|
||||
<th class="text-left" style="width: 5%;">UOM</th>
|
||||
<th class="text-left">Rate</th>
|
||||
<th class="text-left" style="width: 5%;">Discount</th>
|
||||
<th class="text-left">Taxable Amount</th>
|
||||
<th class="text-left" style="width: 7%;">Tax Rate</th>
|
||||
<th class="text-left" style="width: 5%;">Other Charges</th>
|
||||
<th class="text-left">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in einvoice.ItemList %}
|
||||
<tr>
|
||||
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
|
||||
<td class="text-left">{{ item.PrdDesc }}</td>
|
||||
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
|
||||
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
|
||||
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
|
||||
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
|
||||
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
|
||||
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="overflow-x: auto;">
|
||||
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Taxable Amount</th>
|
||||
<th class="text-left">CGST</th>
|
||||
<th class="text-left"">SGST</th>
|
||||
<th class="text-left">IGST</th>
|
||||
<th class="text-left">CESS</th>
|
||||
<th class="text-left" style="width: 10%;">State CESS</th>
|
||||
<th class="text-left">Discount</th>
|
||||
<th class="text-left" style="width: 10%;">Other Charges</th>
|
||||
<th class="text-left" style="width: 10%;">Round Off</th>
|
||||
<th class="text-left">Total Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- set value_details = einvoice.ValDtls -%}
|
||||
<tr>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
{
|
||||
"align_labels_right": 1,
|
||||
"creation": "2020-10-10 18:01:21.032914",
|
||||
"custom_format": 0,
|
||||
"default_print_language": "en-US",
|
||||
"disabled": 1,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Default",
|
||||
"html": "",
|
||||
"idx": 0,
|
||||
"line_breaks": 1,
|
||||
"modified": "2020-10-23 19:54:40.634936",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GST E-Invoice",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 1,
|
||||
"standard": "Yes"
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
{
|
||||
"align_labels_right": 0,
|
||||
"creation": "2017-08-08 12:33:04.773099",
|
||||
"custom_format": 1,
|
||||
"disabled": 0,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Default",
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"Serial No\") }}:</b> {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"modified": "2020-04-29 16:39:12.936215",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GST POS Invoice",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
"label": __("Show Future Payments"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname":"show_gl_balance",
|
||||
"label": __("Show GL Balance"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
],
|
||||
|
||||
onload: function(report) {
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.party import get_partywise_advanced_payment_amount
|
||||
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
|
||||
@ -36,6 +36,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
|
||||
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {}
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date)
|
||||
|
||||
for party, party_dict in self.party_total.items():
|
||||
if party_dict.outstanding == 0:
|
||||
continue
|
||||
@ -55,6 +58,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
# but in summary report advance shown in separate column
|
||||
row.paid -= row.advance
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
row.gl_balance = gl_balance_map.get(party)
|
||||
row.diff = flt(row.outstanding) - flt(row.gl_balance)
|
||||
|
||||
self.data.append(row)
|
||||
|
||||
def get_party_total(self, args):
|
||||
@ -114,6 +121,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(_(credit_debit_label), fieldname='credit_note')
|
||||
self.add_column(_('Outstanding Amount'), fieldname='outstanding')
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
self.add_column(_('GL Balance'), fieldname='gl_balance')
|
||||
self.add_column(_('Difference'), fieldname='diff')
|
||||
|
||||
self.setup_ageing_columns()
|
||||
|
||||
if self.party_type == "Customer":
|
||||
@ -140,3 +151,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
# Add column for total due amount
|
||||
self.add_column(label="Total Amount Due", fieldname='total_due')
|
||||
|
||||
def get_gl_balance(report_date):
|
||||
return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'],
|
||||
filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1))
|
||||
|
@ -120,11 +120,11 @@ def check_opening_balance(asset, liability, equity):
|
||||
opening_balance = 0
|
||||
float_precision = cint(frappe.db.get_default("float_precision")) or 2
|
||||
if asset:
|
||||
opening_balance = flt(asset[0].get("opening_balance", 0), float_precision)
|
||||
opening_balance = flt(asset[-1].get("opening_balance", 0), float_precision)
|
||||
if liability:
|
||||
opening_balance -= flt(liability[0].get("opening_balance", 0), float_precision)
|
||||
opening_balance -= flt(liability[-1].get("opening_balance", 0), float_precision)
|
||||
if equity:
|
||||
opening_balance -= flt(equity[0].get("opening_balance", 0), float_precision)
|
||||
opening_balance -= flt(equity[-1].get("opening_balance", 0), float_precision)
|
||||
|
||||
opening_balance = flt(opening_balance, float_precision)
|
||||
if opening_balance:
|
||||
|
@ -29,18 +29,6 @@ def execute(filters=None):
|
||||
dimension_items = cam_map.get(dimension)
|
||||
if dimension_items:
|
||||
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
|
||||
else:
|
||||
DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation
|
||||
FROM `tabDistributed Cost Center`
|
||||
WHERE cost_center IN %(dimension)s
|
||||
AND parent NOT IN %(dimension)s
|
||||
GROUP BY parent''',{'dimension':[dimension]})
|
||||
if DCC_allocation:
|
||||
filters['budget_against_filter'] = [DCC_allocation[0][0]]
|
||||
ddc_cam_map = get_dimension_account_month_map(filters)
|
||||
dimension_items = ddc_cam_map.get(DCC_allocation[0][0])
|
||||
if dimension_items:
|
||||
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
|
||||
|
||||
chart = get_chart_data(filters, columns, data)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user