Merge branch 'develop' into e-commerce-refactor-develop

This commit is contained in:
Marica 2022-01-12 02:52:03 +05:30 committed by GitHub
commit b5abf68b0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
675 changed files with 13321 additions and 20904 deletions

View File

@ -1,47 +0,0 @@
---
name: Bug report
about: Report a bug encountered while using ERPNext
labels: bug
---
<!--
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For documentation issues, refer to https://github.com/frappe/erpnext_com
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
-->
## Description of the issue
## Context information (for bug reports)
**Output of `bench version`**
```
(paste here)
```
## Steps to reproduce the issue
1.
2.
3.
### Observed result
### Expected result
### Stacktrace / full error message
```
(paste here)
```
## Additional information
OS version / distribution, `ERPNext` install method, etc.

89
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,89 @@
name: Bug Report
description: Report a bug encountered while using ERPNext
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
2. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
- type: textarea
id: bug-info
attributes:
label: Information about bug
description: Also tell us, what did you expect to happen?
placeholder: Please provide as much information as possible.
validations:
required: true
- type: dropdown
id: module
attributes:
label: Module
description: Select affected module of ERPNext.
multiple: true
options:
- accounts
- stock
- buying
- selling
- ecommerce
- manufacturing
- HR
- projects
- support
- CRM
- assets
- integrations
- quality
- regional
- portal
- agriculture
- education
- non-profit
- other
validations:
required: true
- type: textarea
id: exact-version
attributes:
label: Version
description: Share exact version number of Frappe and ERPNext you are using.
placeholder: |
Frappe version -
ERPNext Verion -
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Installation method
options:
- docker
- easy-install
- manual install
- FrappeCloud
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output / Stack trace / Full Error Message.
description: Please copy and paste any relevant log output. This will be automatically formatted.
render: shell
- type: markdown
attributes:
value: |
By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)

View File

@ -1,7 +1,10 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea to improve ERPNext about: Suggest an idea to improve ERPNext
title: ''
labels: feature-request labels: feature-request
assignees: ''
--- ---
<!-- <!--

View File

@ -1,17 +0,0 @@
---
name: Question about using ERPNext
about: This is not the appropriate channel
labels: invalid
---
Please post on our forums:
for questions about using `ERPNext`: https://discuss.erpnext.com
for questions about using the `Frappe Framework`: ~~https://discuss.frappe.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/frappe) tagged under `frappe`
for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench)
For documentation issues, use the [ERPNext Documentation](https://erpnext.com/docs/) or [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
> **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.**

View File

@ -12,17 +12,30 @@ git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site 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'" if [ "$DB" == "mariadb" ];then
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" 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'" if [ "$DB" == "mariadb" ];then
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES" 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 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 tar -xf /tmp/wkhtmltox.tar.xz -C /tmp

View 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
View 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']

56
.github/stale.yml vendored
View File

@ -1,34 +1,36 @@
# Configuration for probot-stale - https://github.com/probot/stale # Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 3
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- hotfix
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking as stale # Label to use when marking as stale
staleLabel: inactive staleLabel: inactive
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed within a week if no further activity occurs, but it
only takes a comment to keep a contribution alive :) Also, even if it is closed,
you can always reopen the PR when you're ready. Thank you for contributing.
# Limit the number of actions per hour, from 1-30. Default is 30 # Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30 limitPerRun: 10
# Limit to only `issues` or `pulls` # Set to true to ignore issues in a project (defaults to false)
only: pulls exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
pulls:
daysUntilStale: 15
daysUntilClose: 3
exemptLabels:
- hotfix
markComment: >
This pull request has been automatically marked as inactive because it has
not had recent activity. It will be closed within 3 days if no further
activity occurs, but it only takes a comment to keep a contribution alive
:) Also, even if it is closed, you can always reopen the PR when you're
ready. Thank you for contributing.
issues:
daysUntilStale: 60
daysUntilClose: 7
exemptLabels:
- valid
- to-validate
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
closed within a week if no further activity occurs.

View File

@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment' - name: 'Setup Environment'
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.6 python-version: 3.8
- name: 'Clone repo' - name: 'Clone repo'
uses: actions/checkout@v2 uses: actions/checkout@v2

12
.github/workflows/labeller.yml vendored Normal file
View 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 }}"

View File

@ -34,7 +34,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.7 python-version: 3.8
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -80,6 +80,9 @@ jobs:
- name: Install - name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
- name: Run Patch Tests - name: Run Patch Tests
run: | run: |

View File

@ -1,10 +1,11 @@
name: Server name: Server (Mariadb)
on: on:
pull_request: pull_request:
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.md' - '**.md'
- '**.html'
workflow_dispatch: workflow_dispatch:
push: push:
branches: [ develop ] branches: [ develop ]
@ -13,7 +14,7 @@ on:
- '**.md' - '**.md'
concurrency: concurrency:
group: server-develop-${{ github.event.number }} group: server-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@ -45,7 +46,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.7 python-version: 3.8
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
@ -92,6 +93,7 @@ jobs:
- name: Install - name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:
DB: mariadb
TYPE: server TYPE: server
- name: Run Tests - name: Run Tests

View 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

View File

@ -36,7 +36,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.7 python-version: 3.8
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:

View File

@ -23,13 +23,13 @@ erpnext/stock/ @marination @rohitwaghchaure @ankush
erpnext/crm/ @ruchamahabal @pateljannat erpnext/crm/ @ruchamahabal @pateljannat
erpnext/education/ @ruchamahabal @pateljannat erpnext/education/ @ruchamahabal @pateljannat
erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
erpnext/hr/ @ruchamahabal @pateljannat erpnext/hr/ @ruchamahabal @pateljannat
erpnext/non_profit/ @ruchamahabal
erpnext/payroll @ruchamahabal @pateljannat erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @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 requirements.txt @gavindsouza

View File

@ -8,6 +8,16 @@ coverage:
target: auto target: auto
threshold: 0.5% threshold: 0.5%
patch:
default:
target: 85%
threshold: 0%
base: auto
branches:
- develop
if_ci_failed: ignore
only_pulls: true
comment: comment:
layout: "diff, files" layout: "diff, files"
require_changes: true require_changes: true

1
dev-requirements.txt Normal file
View File

@ -0,0 +1 @@
hypothesis~=6.31.0

View File

@ -4,7 +4,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
__version__ = '13.9.0' __version__ = '14.0.0-dev'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''
@ -55,9 +55,9 @@ def set_perpetual_inventory(enable=1, company=None):
company.enable_perpetual_inventory = enable company.enable_perpetual_inventory = enable
company.save() company.save()
def encode_company_abbr(name, company): def encode_company_abbr(name, company=None, abbr=None):
'''Returns name encoded with company abbreviation''' '''Returns name encoded with company abbreviation'''
company_abbr = frappe.get_cached_value('Company', company, "abbr") company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr")
parts = name.rsplit(" - ", 1) parts = name.rsplit(" - ", 1)
if parts[-1].lower() != company_abbr.lower(): if parts[-1].lower() != company_abbr.lower():

View File

@ -374,12 +374,13 @@ def make_gl_entries(doc, credit_account, debit_account, against,
frappe.db.commit() frappe.db.commit()
except Exception as e: except Exception as e:
if frappe.flags.in_test: if frappe.flags.in_test:
traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
raise e raise e
else: else:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(message=traceback) frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process): def send_mail(deferred_process):
@ -446,10 +447,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
if submit: if submit:
journal_entry.submit() journal_entry.submit()
frappe.db.commit()
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(message=traceback) frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True

View File

@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
frm.trigger('add_toolbar_buttons'); frm.trigger('add_toolbar_buttons');
} }
if (frm.has_perm('write')) { 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.add_custom_button(__('Merge Account'), function () {
frm.trigger("merge_account"); 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) { add_toolbar_buttons: function(frm) {
frm.add_custom_button(__('Chart of Accounts'), frm.add_custom_button(__('Chart of Accounts'), () => {
function () { frappe.set_route("Tree", "Account"); }); frappe.set_route("Tree", "Account");
}, __('View'));
if (frm.doc.is_group == 1) { 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({ return frappe.call({
doc: frm.doc, doc: frm.doc,
method: 'convert_group_to_ledger', method: 'convert_group_to_ledger',
@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
frm.refresh(); frm.refresh();
} }
}); });
}); }, __('Actions'));
} else if (cint(frm.doc.is_group) == 0 } else if (cint(frm.doc.is_group) == 0
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) { && 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 = { frappe.route_options = {
"account": frm.doc.name, "account": frm.doc.name,
"from_date": frappe.sys_defaults.year_start_date, "from_date": frappe.sys_defaults.year_start_date,
@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
"company": frm.doc.company "company": frm.doc.company
}; };
frappe.set_route("query-report", "General Ledger"); 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({ return frappe.call({
doc: frm.doc, doc: frm.doc,
method: 'convert_ledger_to_group', method: 'convert_ledger_to_group',
@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
frm.refresh(); frm.refresh();
} }
}); });
}); }, __('Actions'));
} }
}, },

View File

@ -78,6 +78,7 @@ frappe.treeview_settings["Account"] = {
const format = (value, currency) => format_currency(Math.abs(value), currency); const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) { if (account.balance!==undefined) {
node.parent && node.parent.find('.balance-area').remove();
$('<span class="balance-area pull-right">' $('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ? + (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "") (format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
@ -175,7 +176,7 @@ frappe.treeview_settings["Account"] = {
&& node.expandable && !node.hide_add; && node.expandable && !node.hide_add;
}, },
click: function() { click: function() {
var me = frappe.treeview_settings['Account'].treeview; var me = frappe.views.trees['Account'];
me.new_node(); me.new_node();
}, },
btnClass: "hidden-xs" btnClass: "hidden-xs"

View File

@ -1,29 +0,0 @@
QUnit.module('accounts');
QUnit.test("test account", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => frappe.set_route('Tree', 'Account'),
() => frappe.timeout(3),
() => frappe.click_button('Expand All'),
() => frappe.timeout(1),
() => frappe.click_link('Debtors'),
() => frappe.click_button('Edit'),
() => frappe.timeout(1),
() => {
assert.ok(cur_frm.doc.root_type=='Asset');
assert.ok(cur_frm.doc.report_type=='Balance Sheet');
assert.ok(cur_frm.doc.account_type=='Receivable');
},
() => frappe.click_button('Ledger'),
() => frappe.timeout(1),
() => {
// check if general ledger report shown
assert.deepEqual(frappe.get_route(), ['query-report', 'General Ledger']);
window.history.back();
return frappe.timeout(1);
},
() => done()
]);
});

View File

@ -1,69 +0,0 @@
QUnit.module('accounts');
QUnit.test("test account with number", function(assert) {
assert.expect(7);
let done = assert.async();
frappe.run_serially([
() => frappe.set_route('Tree', 'Account'),
() => frappe.click_link('Income'),
() => frappe.click_button('Add Child'),
() => frappe.timeout(.5),
() => {
cur_dialog.fields_dict.account_name.$input.val("Test Income");
cur_dialog.fields_dict.account_number.$input.val("4010");
},
() => frappe.click_button('Create New'),
() => frappe.timeout(1),
() => {
assert.ok($('a:contains("4010 - Test Income"):visible').length!=0, "Account created with number");
},
() => frappe.click_link('4010 - Test Income'),
() => frappe.click_button('Edit'),
() => frappe.timeout(.5),
() => frappe.click_button('Update Account Number'),
() => frappe.timeout(.5),
() => {
cur_dialog.fields_dict.account_number.$input.val("4020");
},
() => frappe.timeout(1),
() => cur_dialog.primary_action(),
() => frappe.timeout(1),
() => cur_frm.refresh_fields(),
() => frappe.timeout(.5),
() => {
var abbr = frappe.get_abbr(frappe.defaults.get_default("Company"));
var new_account = "4020 - Test Income - " + abbr;
assert.ok(cur_frm.doc.name==new_account, "Account renamed");
assert.ok(cur_frm.doc.account_name=="Test Income", "account name remained same");
assert.ok(cur_frm.doc.account_number=="4020", "Account number updated to 4020");
},
() => frappe.timeout(1),
() => frappe.click_button('Menu'),
() => frappe.click_link('Rename'),
() => frappe.timeout(.5),
() => {
cur_dialog.fields_dict.new_name.$input.val("4030 - Test Income");
},
() => frappe.timeout(.5),
() => frappe.click_button("Rename"),
() => frappe.timeout(2),
() => {
assert.ok(cur_frm.doc.account_name=="Test Income", "account name remained same");
assert.ok(cur_frm.doc.account_number=="4030", "Account number updated to 4030");
},
() => frappe.timeout(.5),
() => frappe.click_button('Chart of Accounts'),
() => frappe.timeout(.5),
() => frappe.click_button('Menu'),
() => frappe.click_link('Refresh'),
() => frappe.click_button('Expand All'),
() => frappe.click_link('4030 - Test Income'),
() => frappe.click_button('Delete'),
() => frappe.click_button('Yes'),
() => frappe.timeout(.5),
() => {
assert.ok($('a:contains("4030 - Test Account"):visible').length==0, "Account deleted");
},
() => done()
]);
});

View File

@ -1,46 +0,0 @@
QUnit.module('accounts');
QUnit.test("test account", assert => {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
() => frappe.set_route('Tree', 'Account'),
() => frappe.click_button('Expand All'),
() => frappe.click_link('Duties and Taxes - '+ frappe.get_abbr(frappe.defaults.get_default("Company"))),
() => {
if($('a:contains("CGST"):visible').length == 0){
return frappe.map_tax.make('CGST', 9);
}
},
() => {
if($('a:contains("SGST"):visible').length == 0){
return frappe.map_tax.make('SGST', 9);
}
},
() => {
if($('a:contains("IGST"):visible').length == 0){
return frappe.map_tax.make('IGST', 18);
}
},
() => {
assert.ok($('a:contains("CGST"):visible').length!=0, "CGST Checked");
assert.ok($('a:contains("SGST"):visible').length!=0, "SGST Checked");
assert.ok($('a:contains("IGST"):visible').length!=0, "IGST Checked");
},
() => done()
]);
});
frappe.map_tax = {
make:function(text,rate){
return frappe.run_serially([
() => frappe.click_button('Add Child'),
() => frappe.timeout(0.2),
() => cur_dialog.set_value('account_name',text),
() => cur_dialog.set_value('account_type','Tax'),
() => cur_dialog.set_value('tax_rate',rate),
() => cur_dialog.set_value('account_currency','INR'),
() => frappe.click_button('Create New'),
]);
}
};

View File

@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document): class AccountsSettings(Document):
def on_update(self): def on_update(self):
@ -19,9 +21,13 @@ class AccountsSettings(Document):
frappe.db.set_default("add_taxes_from_item_tax_template", frappe.db.set_default("add_taxes_from_item_tax_template",
self.get("add_taxes_from_item_tax_template", 0)) self.get("add_taxes_from_item_tax_template", 0))
frappe.db.set_default("enable_common_party_accounting",
self.get("enable_common_party_accounting", 0))
self.validate_stale_days() self.validate_stale_days()
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields() self.toggle_discount_accounting_fields()
self.validate_pending_reposts()
def validate_stale_days(self): def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0: if not self.allow_stale and cint(self.stale_days) <= 0:
@ -53,3 +59,8 @@ class AccountsSettings(Document):
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False) make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False) make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)

View File

@ -1,35 +0,0 @@
QUnit.module('accounts');
QUnit.test("test: Accounts Settings doesn't allow negatives", function (assert) {
let done = assert.async();
assert.expect(2);
frappe.run_serially([
() => frappe.set_route('Form', 'Accounts Settings', 'Accounts Settings'),
() => frappe.timeout(2),
() => unchecked_if_checked(cur_frm, 'Allow Stale Exchange Rates', frappe.click_check),
() => cur_frm.set_value('stale_days', 0),
() => frappe.click_button('Save'),
() => frappe.timeout(2),
() => {
assert.ok(cur_dialog);
},
() => frappe.click_button('Close'),
() => cur_frm.set_value('stale_days', -1),
() => frappe.click_button('Save'),
() => frappe.timeout(2),
() => {
assert.ok(cur_dialog);
},
() => frappe.click_button('Close'),
() => done()
]);
});
const unchecked_if_checked = function(frm, field_name, fn){
if (frm.doc.allow_stale) {
return fn(field_name);
}
};

View File

@ -0,0 +1,56 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-25 10:24:39.836195",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"reference_detail",
"account_head",
"allocated_amount"
],
"fields": [
{
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"options": "DocType"
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"label": "Reference Name",
"options": "reference_type"
},
{
"fieldname": "reference_detail",
"fieldtype": "Data",
"label": "Reference Detail"
},
{
"fieldname": "account_head",
"fieldtype": "Link",
"label": "Account Head",
"options": "Account"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount",
"options": "party_account_currency"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-11-25 10:27:51.712286",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Tax",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

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

View File

@ -25,8 +25,7 @@
"allocated_amount", "allocated_amount",
"column_break_13", "column_break_13",
"base_tax_amount", "base_tax_amount",
"base_total", "base_total"
"base_allocated_amount"
], ],
"fields": [ "fields": [
{ {
@ -168,12 +167,6 @@
"label": "Allocated Amount", "label": "Allocated Amount",
"options": "currency" "options": "currency"
}, },
{
"fieldname": "base_allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount (Company Currency)",
"options": "Company:company:default_currency"
},
{ {
"fetch_from": "account_head.account_currency", "fetch_from": "account_head.account_currency",
"fieldname": "currency", "fieldname": "currency",
@ -186,7 +179,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-09 11:46:58.373170", "modified": "2021-11-25 11:10:10.945027",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Advance Taxes and Charges", "name": "Advance Taxes and Charges",

View File

@ -7,7 +7,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.set_query("bank_account", function () { frm.set_query("bank_account", function () {
return { return {
filters: { filters: {
company: ["in", frm.doc.company], company: frm.doc.company,
'is_company_account': 1 'is_company_account': 1
}, },
}; };

View File

@ -218,6 +218,8 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction # updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers) vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) 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: if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled")) frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0 total_amount = 0
@ -226,7 +228,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
total_amount += get_paid_amount(frappe._dict({ total_amount += get_paid_amount(frappe._dict({
'payment_document': voucher['payment_doctype'], 'payment_document': voucher['payment_doctype'],
'payment_entry': voucher['payment_name'], 'payment_entry': voucher['payment_name'],
}), transaction.currency) }), transaction.currency, company_account)
if total_amount > transaction.unallocated_amount: 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")) 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 return matching
def check_matching(bank_account, company, transaction, document_types): 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) subquery = get_queries(bank_account, company, transaction, document_types)
filters = { filters = {
"amount": transaction.unallocated_amount, "amount": transaction.unallocated_amount,
@ -343,12 +345,10 @@ def get_pe_matching_query(amount_condition, account_from_to, transaction):
def get_je_matching_query(amount_condition, transaction): def get_je_matching_query(amount_condition, transaction):
# get matching journal entry query # 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") 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""" return f"""
@ -434,7 +434,7 @@ def get_pi_matching_query(amount_condition):
def get_ec_matching_query(bank_account, company, amount_condition): def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query # get matching Expense Claim query
mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account", mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account",
filters={"default_account": bank_account}, fields=["parent"])] filters={"default_account": bank_account}, fields=["parent"])]
mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )' mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
company_currency = get_company_currency(company) company_currency = get_company_currency(company)

View File

@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
"withdrawal", "withdrawal",
"description", "description",
"reference_number", "reference_number",
"bank_account" "bank_account",
"currency"
], ],
}, },
}); });

View File

@ -102,7 +102,7 @@ def get_total_allocated_amount(payment_entry):
AND AND
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True) 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"]: if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount" paid_amount_field = "paid_amount"
@ -115,7 +115,7 @@ def get_paid_amount(payment_entry, currency):
payment_entry.payment_entry, paid_amount_field) payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry": 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": elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed") return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")

View File

@ -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();
}

View File

@ -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
}

View File

@ -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))

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -5,10 +5,10 @@
import unittest import unittest
import frappe import frappe
from frappe.utils import now_datetime
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"] test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase): class TestFiscalYear(unittest.TestCase):
@ -25,3 +25,29 @@ class TestFiscalYear(unittest.TestCase):
}) })
self.assertRaises(FiscalYearIncorrectDate, fy.insert) self.assertRaises(FiscalYearIncorrectDate, fy.insert)
def test_record_generator():
test_records = [
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
}
]
start = 2012
end = now_datetime().year + 5
for year in range(start, end):
test_records.append({
"doctype": "Fiscal Year",
"year": f"_Test Fiscal Year {year}",
"year_start_date": f"{year}-01-01",
"year_end_date": f"{year}-12-31"
})
return test_records
test_records = test_record_generator()

View File

@ -1,69 +0,0 @@
[
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2012",
"year_end_date": "2012-12-31",
"year_start_date": "2012-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2013",
"year_end_date": "2013-12-31",
"year_start_date": "2013-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2014",
"year_end_date": "2014-12-31",
"year_start_date": "2014-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2015",
"year_end_date": "2015-12-31",
"year_start_date": "2015-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2016",
"year_end_date": "2016-12-31",
"year_start_date": "2016-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2017",
"year_end_date": "2017-12-31",
"year_start_date": "2017-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2018",
"year_end_date": "2018-12-31",
"year_start_date": "2018-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2019",
"year_end_date": "2019-12-31",
"year_start_date": "2019-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2020",
"year_end_date": "2020-12-31",
"year_start_date": "2020-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2021",
"year_end_date": "2021-12-31",
"year_start_date": "2021-01-01"
}
]

View File

@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", {
if(frm.doc.docstatus==1) { if(frm.doc.docstatus==1) {
frm.add_custom_button(__('Reverse Journal Entry'), function() { frm.add_custom_button(__('Reverse Journal Entry'), function() {
return erpnext.journal_entry.reverse_journal_entry(frm); return erpnext.journal_entry.reverse_journal_entry(frm);
}, __('Make')); }, __('Actions'));
} }
if (frm.doc.__islocal) { if (frm.doc.__islocal) {

View File

@ -13,6 +13,7 @@
"voucher_type", "voucher_type",
"naming_series", "naming_series",
"finance_book", "finance_book",
"reversal_of",
"tax_withholding_category", "tax_withholding_category",
"column_break1", "column_break1",
"from_template", "from_template",
@ -515,13 +516,21 @@
"fieldname": "apply_tds", "fieldname": "apply_tds",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Apply Tax Withholding Amount " "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", "icon": "fa fa-file-text",
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-09-09 15:31:14.484029", "modified": "2022-01-04 13:39:36.485954",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@ -1157,9 +1157,8 @@ def make_inter_company_journal_entry(name, voucher_type, company):
def make_reverse_journal_entry(source_name, target_doc=None): def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
def update_accounts(source, target, source_parent): def post_process(source, target):
target.reference_type = "Journal Entry" target.reversal_of = source.name
target.reference_name = source_parent.name
doclist = get_mapped_doc("Journal Entry", source_name, { doclist = get_mapped_doc("Journal Entry", source_name, {
"Journal Entry": { "Journal Entry": {
@ -1177,9 +1176,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
"debit": "credit", "debit": "credit",
"credit_in_account_currency": "debit_in_account_currency", "credit_in_account_currency": "debit_in_account_currency",
"credit": "debit", "credit": "debit",
}
}, },
"postprocess": update_accounts, }, target_doc, post_process)
},
}, target_doc)
return doclist return doclist

View File

@ -1,39 +0,0 @@
QUnit.module('Journal Entry');
QUnit.test("test journal entry", function(assert) {
assert.expect(2);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Journal Entry', [
{posting_date:frappe.datetime.add_days(frappe.datetime.nowdate(), 0)},
{accounts: [
[
{'account':'Debtors - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
{'party_type':'Customer'},
{'party':'Test Customer 1'},
{'credit_in_account_currency':1000},
{'is_advance':'Yes'},
],
[
{'account':'HDFC - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
{'debit_in_account_currency':1000},
]
]},
{cheque_no:1234},
{cheque_date: frappe.datetime.add_days(frappe.datetime.nowdate(), -1)},
{user_remark: 'Test'},
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.total_debit==1000, "total debit correct");
assert.ok(cur_frm.doc.total_credit==1000, "total credit correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View 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');
}
});

View 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
}

View 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})

View 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)

View File

@ -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"
}

View File

@ -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

View File

@ -75,7 +75,7 @@
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"issingle": 1, "issingle": 1,
"modified": "2019-07-25 14:57:33.187689", "modified": "2022-01-04 15:25:06.053187",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Opening Invoice Creation Tool", "name": "Opening Invoice Creation Tool",

View File

@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party, frappe.scrub(row.party_type): row.party,
"is_pos": 0, "is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0 "update_stock": 0,
"invoice_number": row.invoice_number
}) })
accounting_dimension = get_accounting_dimensions() accounting_dimension = get_accounting_dimensions()
@ -200,10 +201,13 @@ def start_import(invoices):
names = [] names = []
for idx, d in enumerate(invoices): for idx, d in enumerate(invoices):
try: try:
invoice_number = None
if d.invoice_number:
invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype) publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d) doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.insert() doc.insert(set_name=invoice_number)
doc.submit() doc.submit()
frappe.db.commit() frappe.db.commit()
names.append(doc.name) names.append(doc.name)

View File

@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"): if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company() make_company()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None): def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
doc = frappe.get_single("Opening Invoice Creation Tool") doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company, args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2) party_1=party_1, party_2=party_2, invoice_number=invoice_number)
doc.update(args) doc.update(args)
return doc.make_invoices() return doc.make_invoices()
@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
# teardown # teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def test_renaming_of_invoice_using_invoice_number_field(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
#teardown
for inv in [sales_inv1, sales_inv2]:
doc = frappe.get_doc('Sales Invoice', inv)
doc.cancel()
def get_opening_invoice_creation_dict(**args): def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company") company = args.get("company", "_Test Company")
@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args):
"item_name": "Opening Item", "item_name": "Opening Item",
"due_date": "2016-09-10", "due_date": "2016-09-10",
"posting_date": "2016-09-05", "posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company) "temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": args.get("invoice_number")
}, },
{ {
"qty": 2.0, "qty": 2.0,
@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args):
"item_name": "Opening Item", "item_name": "Opening Item",
"due_date": "2016-09-10", "due_date": "2016-09-10",
"posting_date": "2016-09-05", "posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company) "temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": None
} }
] ]
}) })
@ -132,7 +148,7 @@ def make_company():
company.company_name = "_Test Opening Invoice Company" company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC" company.abbr = "_TOIC"
company.default_currency = "INR" company.default_currency = "INR"
company.country = "India" company.country = "Pakistan"
company.insert() company.insert()
return company return company

View File

@ -1,9 +1,11 @@
{ {
"actions": [],
"creation": "2017-08-29 04:26:36.159247", "creation": "2017-08-29 04:26:36.159247",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"invoice_number",
"party_type", "party_type",
"party", "party",
"temporary_opening_account", "temporary_opening_account",
@ -103,10 +105,18 @@
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"description": "Reference number of the invoice from the previous system",
"fieldname": "invoice_number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Invoice Number"
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-07-25 15:00:00.460695", "links": [],
"modified": "2021-12-17 19:25:06.053187",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Opening Invoice Creation Tool Item", "name": "Opening Invoice Creation Tool Item",

View File

@ -2,7 +2,7 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe import _ from frappe import _, bold
from frappe.model.document import Document 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."), frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."),
title=_("Invalid Primary Role")) 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', { existing_party_link = frappe.get_all('Party Link', {
'primary_party': self.secondary_party 'primary_party': self.secondary_party
}, pluck="primary_role") }, pluck="primary_role")
@ -25,3 +36,17 @@ class PartyLink(Document):
if existing_party_link: if existing_party_link:
frappe.throw(_('{} {} is already linked with another {}') frappe.throw(_('{} {} is already linked with another {}')
.format(self.primary_role, self.primary_party, existing_party_link[0])) .format(self.primary_role, self.primary_party, existing_party_link[0]))
@frappe.whitelist()
def create_party_link(primary_role, primary_party, secondary_party):
party_link = frappe.new_doc('Party Link')
party_link.primary_role = primary_role
party_link.primary_party = primary_party
party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier'
party_link.secondary_party = secondary_party
party_link.save(ignore_permissions=True)
return party_link

View File

@ -61,7 +61,6 @@
"taxes_and_charges_section", "taxes_and_charges_section",
"purchase_taxes_and_charges_template", "purchase_taxes_and_charges_template",
"sales_taxes_and_charges_template", "sales_taxes_and_charges_template",
"advance_tax_account",
"column_break_55", "column_break_55",
"apply_tax_withholding_amount", "apply_tax_withholding_amount",
"tax_withholding_category", "tax_withholding_category",
@ -685,15 +684,6 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1 "hide_border": 1
}, },
{
"depends_on": "eval:doc.apply_tax_withholding_amount",
"description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices",
"fieldname": "advance_tax_account",
"fieldtype": "Link",
"label": "Advance Tax Account",
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
"options": "Account"
},
{ {
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'", "depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax", "fieldname": "received_amount_after_tax",
@ -730,7 +720,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-10-22 17:50:24.632806", "modified": "2021-11-24 18:58:24.919764",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",

View File

@ -20,7 +20,7 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_ban
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details, get_party_tax_withholding_details,
) )
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
from erpnext.controllers.accounts_controller import ( from erpnext.controllers.accounts_controller import (
@ -339,7 +339,7 @@ class PaymentEntry(AccountsController):
for k, v in no_oustanding_refs.items(): for k, v in no_oustanding_refs.items():
frappe.msgprint( frappe.msgprint(
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.") _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
.format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount")) .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount")))
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."), + "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange") title=_("Warning"), indicator="orange")
@ -433,23 +433,12 @@ class PaymentEntry(AccountsController):
if not self.apply_tax_withholding_amount: if not self.apply_tax_withholding_amount:
return return
if not self.advance_tax_account:
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
net_total = self.paid_amount net_total = self.paid_amount
for reference in self.get("references"):
net_total_for_tds = 0
if reference.reference_doctype == 'Purchase Order':
net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total'))
if net_total_for_tds:
net_total = net_total_for_tds
# Adding args as purchase invoice to get TDS amount # Adding args as purchase invoice to get TDS amount
args = frappe._dict({ args = frappe._dict({
'company': self.company, 'company': self.company,
'doctype': 'Purchase Invoice', 'doctype': 'Payment Entry',
'supplier': self.party, 'supplier': self.party,
'posting_date': self.posting_date, 'posting_date': self.posting_date,
'net_total': net_total 'net_total': net_total
@ -461,7 +450,6 @@ class PaymentEntry(AccountsController):
return return
tax_withholding_details.update({ tax_withholding_details.update({
'add_deduct_tax': 'Add',
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company) 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
}) })
@ -623,7 +611,7 @@ class PaymentEntry(AccountsController):
if not total_negative_outstanding: if not total_negative_outstanding:
frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice") frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice")
.format(self.payment_type, ("to" if self.party_type=="Customer" else "from"), .format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")),
self.party_type), InvalidPaymentEntry) self.party_type), InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding: elif paid_amount - additional_charges > total_negative_outstanding:
@ -689,6 +677,7 @@ class PaymentEntry(AccountsController):
self.add_deductions_gl_entries(gl_entries) self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries) self.add_tax_gl_entries(gl_entries)
gl_entries = process_gl_map(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
def add_party_gl_entries(self, gl_entries): def add_party_gl_entries(self, gl_entries):
@ -752,7 +741,8 @@ class PaymentEntry(AccountsController):
"against": self.party if self.payment_type=="Pay" else self.paid_to, "against": self.party if self.payment_type=="Pay" else self.paid_to,
"credit_in_account_currency": self.paid_amount, "credit_in_account_currency": self.paid_amount,
"credit": self.base_paid_amount, "credit": self.base_paid_amount,
"cost_center": self.cost_center "cost_center": self.cost_center,
"post_net_value": True
}, item=self) }, item=self)
) )
if self.payment_type in ("Receive", "Internal Transfer"): if self.payment_type in ("Receive", "Internal Transfer"):
@ -782,14 +772,10 @@ class PaymentEntry(AccountsController):
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_to against = self.party or self.paid_to
payment_or_advance_account = self.get_party_account_for_taxes() payment_account = self.get_party_account_for_taxes()
tax_amount = d.tax_amount tax_amount = d.tax_amount
base_tax_amount = d.base_tax_amount base_tax_amount = d.base_tax_amount
if self.advance_tax_account:
tax_amount = -1 * tax_amount
base_tax_amount = -1 * base_tax_amount
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
"account": d.account_head, "account": d.account_head,
@ -798,19 +784,21 @@ class PaymentEntry(AccountsController):
dr_or_cr + "_in_account_currency": base_tax_amount dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency if account_currency==self.company_currency
else d.tax_amount, else d.tax_amount,
"cost_center": d.cost_center "cost_center": d.cost_center,
"post_net_value": True,
}, account_currency, item=d)) }, account_currency, item=d))
if not d.included_in_paid_amount or self.advance_tax_account: if not d.included_in_paid_amount:
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
"account": payment_or_advance_account, "account": payment_account,
"against": against, "against": against,
rev_dr_or_cr: tax_amount, rev_dr_or_cr: tax_amount,
rev_dr_or_cr + "_in_account_currency": base_tax_amount rev_dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency if account_currency==self.company_currency
else d.tax_amount, else d.tax_amount,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"post_net_value": True,
}, account_currency, item=d)) }, account_currency, item=d))
def add_deductions_gl_entries(self, gl_entries): def add_deductions_gl_entries(self, gl_entries):
@ -832,9 +820,7 @@ class PaymentEntry(AccountsController):
) )
def get_party_account_for_taxes(self): def get_party_account_for_taxes(self):
if self.advance_tax_account: if self.payment_type == 'Receive':
return self.advance_tax_account
elif self.payment_type == 'Receive':
return self.paid_to return self.paid_to
elif self.payment_type in ('Pay', 'Internal Transfer'): elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_from return self.paid_from
@ -1106,7 +1092,7 @@ def get_outstanding_reference_documents(args):
if not data: if not data:
frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.") frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
.format(args.get("party_type").lower(), frappe.bold(args.get("party")))) .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))))
return data return data
@ -1599,13 +1585,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
}) })
pe.set_difference_amount() pe.set_difference_amount()
if doc.doctype == 'Purchase Order' and doc.apply_tds:
pe.apply_tax_withholding_amount = 1
pe.tax_withholding_category = doc.tax_withholding_category
if not pe.advance_tax_account:
pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account')
return pe return pe
def get_bank_cash_account(doc, bank_account): def get_bank_cash_account(doc, bank_account):
@ -1729,7 +1708,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
def apply_early_payment_discount(paid_amount, received_amount, doc): def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0 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: for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage': if term.discount_type == 'Percentage':

View File

@ -1,55 +0,0 @@
QUnit.module('Payment Entry');
QUnit.test("test payment entry", function(assert) {
assert.expect(6);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'item_code': 'Test Product 1'},
{'qty': 1},
{'rate': 101},
]
]}
]);
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(1),
() => frappe.click_button('Make'),
() => frappe.timeout(1),
() => frappe.click_link('Payment'),
() => frappe.timeout(2),
() => {
assert.equal(frappe.get_route()[1], 'Payment Entry',
'made payment entry');
assert.equal(cur_frm.doc.party, 'Test Customer 1',
'customer set in payment entry');
assert.equal(cur_frm.doc.paid_amount, 101,
'paid amount set in payment entry');
assert.equal(cur_frm.doc.references[0].allocated_amount, 101,
'amount allocated against sales invoice');
},
() => frappe.timeout(1),
() => cur_frm.set_value('paid_amount', 100),
() => frappe.timeout(1),
() => {
frappe.model.set_value("Payment Entry Reference", cur_frm.doc.references[0].name,
"allocated_amount", 101);
},
() => frappe.timeout(1),
() => frappe.click_button('Write Off Difference Amount'),
() => frappe.timeout(1),
() => {
assert.equal(cur_frm.doc.difference_amount, 0, 'difference amount is zero');
assert.equal(cur_frm.doc.deductions[0].amount, 1, 'Write off amount = 1');
},
() => done()
]);
});

View File

@ -1,60 +0,0 @@
QUnit.module('Payment Entry');
QUnit.test("test payment entry", function(assert) {
assert.expect(7 );
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Purchase Invoice', [
{supplier: 'Test Supplier'},
{bill_no: 'in1234'},
{items: [
[
{'qty': 2},
{'item_code': 'Test Product 1'},
{'rate':1000},
]
]},
{update_stock:1},
{supplier_address: 'Test1-Billing'},
{contact_person: 'Contact 3-Test Supplier'},
{tc_name: 'Test Term 1'},
{terms: 'This is just a Test'}
]);
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1),
() => frappe.click_button('Make'),
() => frappe.timeout(2),
() => frappe.click_link('Payment'),
() => frappe.timeout(3),
() => cur_frm.set_value('mode_of_payment','Cash'),
() => frappe.timeout(3),
() => {
assert.equal(frappe.get_route()[1], 'Payment Entry',
'made payment entry');
assert.equal(cur_frm.doc.party, 'Test Supplier',
'supplier set in payment entry');
assert.equal(cur_frm.doc.paid_amount, 2000,
'paid amount set in payment entry');
assert.equal(cur_frm.doc.references[0].allocated_amount, 2000,
'amount allocated against purchase invoice');
assert.equal(cur_frm.doc.references[0].bill_no, 'in1234',
'invoice number allocated against purchase invoice');
assert.equal(cur_frm.get_field('total_allocated_amount').value, 2000,
'correct amount allocated in Write Off');
assert.equal(cur_frm.get_field('unallocated_amount').value, 0,
'correct amount unallocated in Write Off');
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(3),
() => done()
]);
});

View File

@ -1,28 +0,0 @@
QUnit.module('Accounts');
QUnit.test("test payment entry", function(assert) {
assert.expect(1);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Payment Entry', [
{payment_type:'Receive'},
{mode_of_payment:'Cash'},
{party_type:'Customer'},
{party:'Test Customer 3'},
{paid_amount:675},
{reference_no:123},
{reference_date: frappe.datetime.add_days(frappe.datetime.nowdate(), 0)},
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.total_allocated_amount==675, "Allocated AmountCorrect");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1,67 +0,0 @@
QUnit.module('Payment Entry');
QUnit.test("test payment entry", function(assert) {
assert.expect(8);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{company: 'For Testing'},
{currency: 'INR'},
{selling_price_list: '_Test Price List'},
{items: [
[
{'qty': 1},
{'item_code': 'Test Product 1'},
]
]}
]);
},
() => frappe.timeout(1),
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1.5),
() => frappe.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.click_button('Make'),
() => frappe.timeout(1),
() => frappe.click_link('Payment'),
() => frappe.timeout(2),
() => cur_frm.set_value("paid_to", "_Test Cash - FT"),
() => frappe.timeout(0.5),
() => {
assert.equal(frappe.get_route()[1], 'Payment Entry', 'made payment entry');
assert.equal(cur_frm.doc.party, 'Test Customer 1', 'customer set in payment entry');
assert.equal(cur_frm.doc.paid_from, 'Debtors - FT', 'customer account set in payment entry');
assert.equal(cur_frm.doc.paid_amount, 100, 'paid amount set in payment entry');
assert.equal(cur_frm.doc.references[0].allocated_amount, 100,
'amount allocated against sales invoice');
},
() => cur_frm.set_value('paid_amount', 95),
() => frappe.timeout(1),
() => {
frappe.model.set_value("Payment Entry Reference",
cur_frm.doc.references[0].name, "allocated_amount", 100);
},
() => frappe.timeout(.5),
() => {
assert.equal(cur_frm.doc.difference_amount, 5, 'difference amount is 5');
},
() => {
frappe.db.set_value("Company", "For Testing", "write_off_account", "_Test Write Off - FT");
frappe.timeout(1);
frappe.db.set_value("Company", "For Testing",
"exchange_gain_loss_account", "_Test Exchange Gain/Loss - FT");
},
() => frappe.timeout(1),
() => frappe.click_button('Write Off Difference Amount'),
() => frappe.timeout(2),
() => {
assert.equal(cur_frm.doc.difference_amount, 0, 'difference amount is zero');
assert.equal(cur_frm.doc.deductions[0].amount, 5, 'Write off amount = 5');
},
() => done()
]);
});

View File

@ -548,10 +548,14 @@ def make_payment_order(source_name, target_doc=None):
return doclist return doclist
def validate_payment(doc, method=""): def validate_payment(doc, method=None):
if not frappe.db.has_column(doc.reference_doctype, 'status'): if doc.reference_doctype != "Payment Request" or (
frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
!= "Paid"
):
return return
status = frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status') frappe.throw(
if status == 'Paid': _("The Payment Request {0} is already paid, cannot process payment twice")
frappe.throw(_("The Payment Request {0} is already paid, cannot process payment twice").format(doc.reference_docname)) .format(doc.reference_docname)
)

View File

@ -88,9 +88,10 @@ class PeriodClosingVoucher(AccountsController):
for acc in pl_accounts: for acc in pl_accounts:
if flt(acc.bal_in_company_currency): if flt(acc.bal_in_company_currency):
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
gl_entry = self.get_gl_dict({ gl_entry = self.get_gl_dict({
"account": self.closing_account_head, "account": self.closing_account_head,
"cost_center": acc.cost_center or company_cost_center, "cost_center": cost_center,
"finance_book": acc.finance_book, "finance_book": acc.finance_book,
"account_currency": acc.account_currency, "account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,

View File

@ -66,8 +66,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
company = create_company() company = create_company()
surplus_account = create_account() surplus_account = create_account()
cost_center1 = create_cost_center("Test Cost Center 1") cost_center1 = create_cost_center("Main")
cost_center2 = create_cost_center("Test Cost Center 2") cost_center2 = create_cost_center("Western Branch")
create_sales_invoice( create_sales_invoice(
company=company, company=company,
@ -86,7 +86,10 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC" debit_to="Debtors - TPC"
) )
pcv = self.make_period_closing_voucher() pcv = self.make_period_closing_voucher(submit=False)
pcv.cost_center_wise_pnl = 1
pcv.save()
pcv.submit()
surplus_account = pcv.closing_account_head surplus_account = pcv.closing_account_head
expected_gle = ( expected_gle = (
@ -149,7 +152,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(pcv_gle, expected_gle) self.assertEqual(pcv_gle, expected_gle)
def make_period_closing_voucher(self): def make_period_closing_voucher(self, submit=True):
surplus_account = create_account() surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1") cost_center = create_cost_center("Test Cost Center 1")
pcv = frappe.get_doc({ pcv = frappe.get_doc({
@ -163,6 +166,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
"remarks": "test" "remarks": "test"
}) })
pcv.insert() pcv.insert()
if submit:
pcv.submit() pcv.submit()
return pcv return pcv

View File

@ -171,6 +171,7 @@
"sales_team_section_break", "sales_team_section_break",
"sales_partner", "sales_partner",
"column_break10", "column_break10",
"amount_eligible_for_commission",
"commission_rate", "commission_rate",
"total_commission", "total_commission",
"section_break2", "section_break2",
@ -1561,16 +1562,23 @@
"label": "Coupon Code", "label": "Coupon Code",
"options": "Coupon Code", "options": "Coupon Code",
"print_hide": 1 "print_hide": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-08-27 20:12:57.306772", "modified": "2021-10-05 12:11:53.871828",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",
"name_case": "Title Case", "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@ -15,6 +15,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_multi_mode_option, update_multi_mode_option,
) )
from erpnext.accounts.party import get_due_date, get_party_account 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 from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@ -124,9 +125,26 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.") 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")) .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos: 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")) .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): def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no) serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', { delivered_serial_nos = frappe.db.get_list('Serial No', {
@ -149,6 +167,8 @@ class POSInvoice(SalesInvoice):
if d.serial_no: if d.serial_no:
self.validate_pos_reserved_serial_nos(d) self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d) self.validate_delivered_serial_nos(d)
elif d.batch_no:
self.validate_pos_reserved_batch_qty(d)
else: else:
if allow_negative_stock: if allow_negative_stock:
return return
@ -333,7 +353,6 @@ class POSInvoice(SalesInvoice):
if not for_validate and not self.customer: if not for_validate and not self.customer:
self.customer = profile.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.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 self.set_warehouse = profile.get('warehouse') or self.set_warehouse

View File

@ -521,6 +521,72 @@ class TestPOSInvoice(unittest.TestCase):
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400) 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()
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.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
# rate should change since pricing rules are ignored
self.assertEquals(pos_inv.items[0].rate, 300)
item_price.delete()
pos_inv.delete()
pr.delete()
def create_pos_invoice(**args): def create_pos_invoice(**args):
args = frappe._dict(args) args = frappe._dict(args)
pos_profile = None pos_profile = None
@ -557,7 +623,8 @@ def create_pos_invoice(**args):
"income_account": args.income_account or "Sales - _TC", "income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _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: if not args.do_not_save:
@ -570,3 +637,8 @@ def create_pos_invoice(**args):
pos_inv.payment_schedule = [] pos_inv.payment_schedule = []
return pos_inv 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))

View File

@ -46,6 +46,7 @@
"base_amount", "base_amount",
"pricing_rules", "pricing_rules",
"is_free_item", "is_free_item",
"grant_commission",
"section_break_21", "section_break_21",
"net_rate", "net_rate",
"net_amount", "net_amount",
@ -800,14 +801,22 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-04 17:34:49.924531", "modified": "2021-10-05 12:23:47.506290",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Item", "name": "POS Invoice Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",

View File

@ -3,7 +3,8 @@
{% include "erpnext/public/js/controllers/accounts.js" %} {% include "erpnext/public/js/controllers/accounts.js" %}
frappe.ui.form.on("POS Profile", "onload", function(frm) { frappe.ui.form.on('POS Profile', {
setup: function(frm) {
frm.set_query("selling_price_list", function() { frm.set_query("selling_price_list", function() {
return { filters: { selling: 1 } }; return { filters: { selling: 1 } };
}); });
@ -15,10 +16,7 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) {
erpnext.queries.setup_queries(frm, "Warehouse", function() { erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc); return erpnext.queries.warehouse(frm.doc);
}); });
});
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
frm.set_query("print_format", function() { frm.set_query("print_format", function() {
return { return {
filters: [ filters: [
@ -27,10 +25,16 @@ frappe.ui.form.on('POS Profile', {
}; };
}); });
frm.set_query("account_for_change_amount", function() { frm.set_query("account_for_change_amount", function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return { return {
filters: { filters: {
account_type: ['in', ["Cash", "Bank"]] account_type: ['in', ["Cash", "Bank"]],
is_group: 0,
company: doc.company
} }
}; };
}); });
@ -45,7 +49,7 @@ frappe.ui.form.on('POS Profile', {
}); });
frm.set_query('company_address', function(doc) { frm.set_query('company_address', function(doc) {
if(!doc.company) { if (!doc.company) {
frappe.throw(__('Please set Company')); frappe.throw(__('Please set Company'));
} }
@ -58,11 +62,79 @@ frappe.ui.form.on('POS Profile', {
}; };
}); });
frm.set_query('income_account', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
'is_group': 0,
'company': doc.company,
'account_type': "Income Account"
}
};
});
frm.set_query('cost_center', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
'company': doc.company,
'is_group': 0
}
};
});
frm.set_query('expense_account', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
"report_type": "Profit and Loss",
"company": doc.company,
"is_group": 0
}
};
});
frm.set_query("select_print_heading", function() {
return {
filters: [
['Print Heading', 'docstatus', '!=', 2]
]
};
});
frm.set_query("write_off_account", function(doc) {
return {
filters: {
'report_type': 'Profit and Loss',
'is_group': 0,
'company': doc.company
}
};
});
frm.set_query("write_off_cost_center", function(doc) {
return {
filters: {
'is_group': 0,
'company': doc.company
}
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}, },
refresh: function(frm) { refresh: function(frm) {
if(frm.doc.company) { if (frm.doc.company) {
frm.trigger("toggle_display_account_head"); frm.trigger("toggle_display_account_head");
} }
}, },
@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', {
frm.toggle_display('expense_account', frm.toggle_display('expense_account',
erpnext.is_perpetual_inventory_enabled(frm.doc.company)); erpnext.is_perpetual_inventory_enabled(frm.doc.company));
} }
}) });
// Income Account
// --------------------------------
cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) {
return{
filters:{
'is_group': 0,
'company': doc.company,
'account_type': "Income Account"
}
};
};
// Cost Center
// -----------------------------
cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) {
return{
filters:{
'company': doc.company,
'is_group': 0
}
};
};
// Expense Account
// -----------------------------
cur_frm.fields_dict["expense_account"].get_query = function(doc) {
return {
filters: {
"report_type": "Profit and Loss",
"company": doc.company,
"is_group": 0
}
};
};
// ------------------ Get Print Heading ------------------------------------
cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) {
return{
filters:[
['Print Heading', 'docstatus', '!=', 2]
]
};
};
cur_frm.fields_dict.write_off_account.get_query = function(doc) {
return{
filters:{
'report_type': 'Profit and Loss',
'is_group': 0,
'company': doc.company
}
};
};
// Write off cost center
// -----------------------
cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
return{
filters:{
'is_group': 0,
'company': doc.company
}
};
};

View File

@ -166,7 +166,7 @@ class TestPricingRule(unittest.TestCase):
"item_group": "Products", "item_group": "Products",
}, },
{ {
"item_group": "Seed", "item_group": "_Test Item Group",
}, },
], ],
"selling": 1, "selling": 1,
@ -543,6 +543,75 @@ class TestPricingRule(unittest.TestCase):
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete() item.delete()
def test_pricing_rule_for_different_currency(self):
make_item("Test Sanitizer Item")
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Sanitizer Rule",
"apply_on": "Item Code",
"items": [{
"item_code": "Test Sanitizer Item",
}],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
"rate": 0,
"priority": 2,
"margin_type": "Percentage",
"margin_rate_or_amount": 0.0,
"company": "_Test Company"
}
rule = frappe.get_doc(pricing_rule_record)
rule.rate_or_discount = 'Rate'
rule.rate = 100.0
rule.insert()
rule1 = frappe.get_doc(pricing_rule_record)
rule1.currency = 'USD'
rule1.rate_or_discount = 'Rate'
rule1.rate = 2.0
rule1.priority = 1
rule1.insert()
args = frappe._dict({
"item_code": "Test Sanitizer Item",
"company": "_Test Company",
"price_list": "_Test Price List",
"currency": "USD",
"doctype": "Sales Invoice",
"conversion_rate": 1,
"price_list_currency": "_Test Currency",
"plc_conversion_rate": 1,
"order_type": "Sales",
"customer": "_Test Customer",
"name": None,
"transaction_date": frappe.utils.nowdate()
})
details = get_item_details(args)
self.assertEqual(details.price_list_rate, 2.0)
args = frappe._dict({
"item_code": "Test Sanitizer Item",
"company": "_Test Company",
"price_list": "_Test Price List",
"currency": "INR",
"doctype": "Sales Invoice",
"conversion_rate": 1,
"price_list_currency": "_Test Currency",
"plc_conversion_rate": 1,
"order_type": "Sales",
"customer": "_Test Customer",
"name": None,
"transaction_date": frappe.utils.nowdate()
})
details = get_item_details(args)
self.assertEqual(details.price_list_rate, 100.0)
def test_pricing_rule_for_transaction(self): def test_pricing_rule_for_transaction(self):
make_item("Water Flask 1") make_item("Water Flask 1")
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
@ -581,7 +650,7 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0, "rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '', "condition": args.condition or '',
"priority": 1, "priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0, "discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
}) })
@ -607,6 +676,8 @@ def make_pricing_rule(**args):
if args.get(applicable_for): if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for)) doc.db_set(applicable_for, args.get(applicable_for))
return doc
def setup_pricing_rule_data(): def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'): if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({ frappe.get_doc({

View File

@ -1,28 +0,0 @@
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule", function(assert) {
assert.expect(2);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Pricing Rule", [
{title: 'Test Pricing Rule'},
{item_code:'Test Product 2'},
{selling:1},
{applicable_for:'Customer'},
{customer:'Test Customer 3'},
{currency: frappe.defaults.get_default("currency")}
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{discount_percentage:10},
{for_price_list:'Standard Selling'}
]);
},
() => {
assert.ok(cur_frm.doc.item_code=='Test Product 2');
assert.ok(cur_frm.doc.customer=='Test Customer 3');
},
() => done()
]);
});

View File

@ -1,58 +0,0 @@
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule with different currency", function(assert) {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Pricing Rule", [
{title: 'Test Pricing Rule 2'},
{apply_on: 'Item Code'},
{item_code:'Test Product 4'},
{selling:1},
{priority: 1},
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{margin_type: 'Amount'},
{margin_rate_or_amount: 20},
{rate_or_discount: 'Rate'},
{rate:200},
{currency:'USD'}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
assert.ok(cur_frm.doc.item_code=='Test Product 4');
},
() => {
return frappe.tests.make('Sales Order', [
{customer: 'Test Customer 1'},
{currency: 'INR'},
{items: [
[
{'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{'qty': 5},
{'item_code': "Test Product 4"}
]
]}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].pricing_rule=='Test Pricing Rule 2', "Pricing rule correct");
// margin not applied because different currency in pricing rule
assert.ok(cur_frm.doc.items[0].margin_type==null, "Margin correct");
},
() => frappe.timeout(0.3),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1,56 +0,0 @@
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule with same currency", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Pricing Rule", [
{title: 'Test Pricing Rule 1'},
{apply_on: 'Item Code'},
{item_code:'Test Product 4'},
{selling:1},
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{rate_or_discount: 'Rate'},
{rate:200},
{currency:'USD'}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
assert.ok(cur_frm.doc.item_code=='Test Product 4');
},
() => {
return frappe.tests.make('Sales Order', [
{customer: 'Test Customer 1'},
{currency: 'USD'},
{items: [
[
{'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{'qty': 5},
{'item_code': "Test Product 4"}
]
]}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].pricing_rule=='Test Pricing Rule 1', "Pricing rule correct");
assert.ok(cur_frm.doc.items[0].price_list_rate==200, "Item rate correct");
// get_total
assert.ok(cur_frm.doc.total== 1000, "Total correct");
},
() => frappe.timeout(0.3),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -264,6 +264,11 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
else: else:
p.variant_of = None p.variant_of = None
if len(pricing_rules) > 1:
filtered_rules = list(filter(lambda x: x.currency==args.get('currency'), pricing_rules))
if filtered_rules:
pricing_rules = filtered_rules
# find pricing rule with highest priority # find pricing rule with highest priority
if pricing_rules: if pricing_rules:
max_priority = max(cint(p.priority) for p in pricing_rules) max_priority = max(cint(p.priority) for p in pricing_rules)

View File

@ -130,6 +130,7 @@
"allocate_advances_automatically", "allocate_advances_automatically",
"get_advances", "get_advances",
"advances", "advances",
"advance_tax",
"payment_schedule_section", "payment_schedule_section",
"payment_terms_template", "payment_terms_template",
"ignore_default_payment_terms_template", "ignore_default_payment_terms_template",
@ -1408,13 +1409,21 @@
{ {
"fieldname": "column_break_147", "fieldname": "column_break_147",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "advance_tax",
"fieldtype": "Table",
"hidden": 1,
"label": "Advance Tax",
"options": "Advance Tax",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-10-12 20:55:16.145651", "modified": "2021-11-25 13:31:02.716727",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -114,6 +114,9 @@ class PurchaseInvoice(BuyingController):
self.set_status() self.set_status()
self.validate_purchase_receipt_if_update_stock() self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference) validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_release_date(self): def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date): if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@ -294,8 +297,15 @@ class PurchaseInvoice(BuyingController):
item.expense_account = stock_not_billed_account item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code, asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
company = self.company) company = self.company)
if not asset_category_account:
form_link = get_link_to_form('Asset Category', asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
title=_("Missing Account")
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail: elif item.is_fixed_asset and item.pr_detail:
item.expense_account = asset_received_but_not_billed item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate: elif not item.expense_account and for_validate:
@ -427,6 +437,7 @@ class PurchaseInvoice(BuyingController):
self.update_project() self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.update_advance_tax_references()
self.process_common_party_accounting() self.process_common_party_accounting()
@ -472,8 +483,6 @@ class PurchaseInvoice(BuyingController):
self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self) gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries) gl_entries = merge_similar_entries(gl_entries)
@ -496,11 +505,11 @@ class PurchaseInvoice(BuyingController):
# Checked both rounding_adjustment and rounded_total # Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on 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 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(): if grand_total and not self.is_internal_transfer():
# Did not use base_grand_total to book rounding loss gle # 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( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
"account": self.credit_to, "account": self.credit_to,
@ -508,8 +517,8 @@ class PurchaseInvoice(BuyingController):
"party": self.supplier, "party": self.supplier,
"due_date": self.due_date, "due_date": self.due_date,
"against": self.against_expense_account, "against": self.against_expense_account,
"credit": grand_total_in_company_currency, "credit": base_grand_total,
"credit_in_account_currency": grand_total_in_company_currency \ "credit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else 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": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype, "against_voucher_type": self.doctype,
@ -729,7 +738,7 @@ class PurchaseInvoice(BuyingController):
"account": self.stock_received_but_not_billed, "account": self.stock_received_but_not_billed,
"against": self.supplier, "against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"remarks": self.remarks or "Accounting Entry for Stock", "remarks": self.remarks or _("Accounting Entry for Stock"),
"cost_center": self.cost_center, "cost_center": self.cost_center,
"project": item.project or self.project "project": item.project or self.project
}, item=item) }, item=item)
@ -937,7 +946,7 @@ class PurchaseInvoice(BuyingController):
"cost_center": tax.cost_center, "cost_center": tax.cost_center,
"against": self.supplier, "against": self.supplier,
"credit": valuation_tax[tax.name], "credit": valuation_tax[tax.name],
"remarks": self.remarks or "Accounting Entry for Stock" "remarks": self.remarks or _("Accounting Entry for Stock")
}, item=tax)) }, item=tax))
@property @property
@ -1074,6 +1083,7 @@ class PurchaseInvoice(BuyingController):
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.update_advance_tax_references(cancel=1)
def update_project(self): def update_project(self):
project_list = [] project_list = []
@ -1150,7 +1160,10 @@ class PurchaseInvoice(BuyingController):
if not self.tax_withholding_category: if not self.tax_withholding_category:
return return
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category) tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category)
# Adjust TDS paid on advances
self.allocate_advance_tds(tax_withholding_details, advance_taxes)
if not tax_withholding_details: if not tax_withholding_details:
return return
@ -1174,6 +1187,39 @@ class PurchaseInvoice(BuyingController):
# calculate totals again after applying TDS # calculate totals again after applying TDS
self.calculate_taxes_and_totals() self.calculate_taxes_and_totals()
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
self.set('advance_tax', [])
for tax in advance_taxes:
allocated_amount = 0
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
if flt(tax_withholding_details.get('tax_amount')) >= pending_amount:
tax_withholding_details['tax_amount'] -= pending_amount
allocated_amount = pending_amount
elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount:
allocated_amount = tax_withholding_details['tax_amount']
tax_withholding_details['tax_amount'] = 0
self.append('advance_tax', {
'reference_type': 'Payment Entry',
'reference_name': tax.parent,
'reference_detail': tax.name,
'account_head': tax.account_head,
'allocated_amount': allocated_amount
})
def update_advance_tax_references(self, cancel=0):
for tax in self.get('advance_tax'):
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
if cancel:
frappe.qb.update(at).set(
at.allocated_amount, at.allocated_amount - tax.allocated_amount
).where(at.name == tax.reference_detail).run()
else:
frappe.qb.update(at).set(
at.allocated_amount, at.allocated_amount + tax.allocated_amount
).where(at.name == tax.reference_detail).run()
def set_status(self, update=False, status=None, update_modified=True): def set_status(self, update=False, status=None, update_modified=True):
if self.is_new(): if self.is_new():
if self.get('amended_from'): if self.get('amended_from'):

View File

@ -1,74 +0,0 @@
QUnit.module('Purchase Invoice');
QUnit.test("test purchase invoice", function(assert) {
assert.expect(9);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Purchase Invoice', [
{supplier: 'Test Supplier'},
{bill_no: 'in123'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
{'rate':100},
]
]},
{update_stock:1},
{supplier_address: 'Test1-Billing'},
{contact_person: 'Contact 3-Test Supplier'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'},
{payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
},
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
},
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.tests.set_form_values(cur_frm, [{'payment_terms_schedule': ''}]),
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
},
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.tests.set_form_values(cur_frm, [{'payment_schedule': []}]),
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(!cur_dialog, 'Message is not shown');
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1),
() => done()
]);
});

View File

@ -13,6 +13,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
@ -35,6 +36,27 @@ class TestPurchaseInvoice(unittest.TestCase):
def tearDownClass(self): def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0) unlink_payment_on_cancel_of_invoice(0)
def test_purchase_invoice_received_qty(self):
"""
1. Test if received qty is validated against accepted + rejected
2. Test if received qty is auto set on save
"""
pi = make_purchase_invoice(
qty=1,
rejected_qty=1,
received_qty=3,
item_code="_Test Item Home Desktop 200",
rejected_warehouse = "_Test Rejected Warehouse - _TC",
update_stock=True, do_not_save=True)
self.assertRaises(QtyMismatchError, pi.save)
pi.items[0].received_qty = 0
pi.save()
self.assertEqual(pi.items[0].received_qty, 2)
# teardown
pi.delete()
def test_gl_entries_without_perpetual_inventory(self): def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
pi = frappe.copy_doc(test_records[0]) pi = frappe.copy_doc(test_records[0])
@ -811,29 +833,12 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.shipping_rule = shipping_rule.name pi.shipping_rule = shipping_rule.name
pi.insert() pi.insert()
shipping_amount = 0.0
for condition in shipping_rule.get("conditions"):
if not condition.to_value or (flt(condition.from_value) <= pi.net_total <= flt(condition.to_value)):
shipping_amount = condition.shipping_amount
shipping_charge = {
"doctype": "Purchase Taxes and Charges",
"category": "Valuation and Total",
"charge_type": "Actual",
"account_head": shipping_rule.account,
"cost_center": shipping_rule.cost_center,
"tax_amount": shipping_amount,
"description": shipping_rule.name,
"add_deduct_tax": "Add"
}
pi.append("taxes", shipping_charge)
pi.save() pi.save()
self.assertEqual(pi.net_total, 1250) self.assertEqual(pi.net_total, 1250)
self.assertEqual(pi.total_taxes_and_charges, 462.3) self.assertEqual(pi.total_taxes_and_charges, 354.1)
self.assertEqual(pi.grand_total, 1712.3) self.assertEqual(pi.grand_total, 1604.1)
def test_make_pi_without_terms(self): def test_make_pi_without_terms(self):
pi = make_purchase_invoice(do_not_save=1) pi = make_purchase_invoice(do_not_save=1)
@ -981,7 +986,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True) pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
pi.set_posting_time = 1 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].enable_deferred_expense = 1
pi.items[0].service_start_date = "2019-01-10" pi.items[0].service_start_date = "2019-01-10"
pi.items[0].service_end_date = "2019-03-15" pi.items[0].service_end_date = "2019-03-15"
@ -1155,25 +1160,21 @@ class TestPurchaseInvoice(unittest.TestCase):
# Create Purchase Order with TDS applied # Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item', po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
posting_date='2021-09-15') posting_date='2021-09-15')
po.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save() po.save()
po.submit() po.submit()
# Update Unrealized Profit / Loss Account which is used as default advance tax account
frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC')
# Create Payment Entry Against the order # Create Payment Entry Against the order
payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name) payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
payment_entry.paid_from = 'Cash - _TC' payment_entry.paid_from = 'Cash - _TC'
payment_entry.apply_tax_withholding_amount = 1
payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
payment_entry.save() payment_entry.save()
payment_entry.submit() payment_entry.submit()
# Check GLE for Payment Entry # Check GLE for Payment Entry
expected_gle = [ expected_gle = [
['_Test Account Excise Duty - _TC', 3000, 0],
['Cash - _TC', 0, 27000], ['Cash - _TC', 0, 27000],
['Creditors - _TC', 27000, 0], ['Creditors - _TC', 30000, 0],
['TDS Payable - _TC', 0, 3000], ['TDS Payable - _TC', 0, 3000],
] ]
@ -1199,9 +1200,7 @@ class TestPurchaseInvoice(unittest.TestCase):
# Zero net effect on final TDS Payable on invoice # Zero net effect on final TDS Payable on invoice
expected_gle = [ expected_gle = [
['_Test Account Cost for Goods Sold - _TC', 30000], ['_Test Account Cost for Goods Sold - _TC', 30000],
['_Test Account Excise Duty - _TC', -3000], ['Creditors - _TC', -30000]
['Creditors - _TC', -27000],
['TDS Payable - _TC', 0]
] ]
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
@ -1214,6 +1213,14 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.amount) self.assertEqual(expected_gle[i][1], gle.amount)
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000)
purchase_invoice.cancel()
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry` from `tabGL Entry`
@ -1229,7 +1236,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
def update_tax_witholding_category(company, account): def update_tax_witholding_category(company, account):
from erpnext.accounts.utils import get_fiscal_year 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', if not frappe.db.get_value('Tax Withholding Rate',
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),

View File

@ -22,10 +22,10 @@
"received_qty", "received_qty",
"qty", "qty",
"rejected_qty", "rejected_qty",
"stock_uom",
"col_break2", "col_break2",
"uom", "uom",
"conversion_factor", "conversion_factor",
"stock_uom",
"stock_qty", "stock_qty",
"sec_break1", "sec_break1",
"price_list_rate", "price_list_rate",
@ -175,7 +175,8 @@
{ {
"fieldname": "received_qty", "fieldname": "received_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Received Qty" "label": "Received Qty",
"read_only": 1
}, },
{ {
"bold": 1, "bold": 1,
@ -223,7 +224,7 @@
{ {
"fieldname": "stock_qty", "fieldname": "stock_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Stock Qty", "label": "Accepted Qty in Stock UOM",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"reqd": 1 "reqd": 1
@ -870,10 +871,11 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-09-01 16:04:03.538643", "modified": "2021-11-15 17:04:07.191013",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",

View File

@ -1,28 +0,0 @@
QUnit.module('Sales Taxes and Charges Template');
QUnit.test("test sales taxes and charges template", function(assert) {
assert.expect(2);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Purchase Taxes and Charges Template', [
{title: "TEST In State GST"},
{taxes:[
[
{charge_type:"On Net Total"},
{account_head:"CGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
],
[
{charge_type:"On Net Total"},
{account_head:"SGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
]
]}
]);
},
() => {
assert.ok(cur_frm.doc.title=='TEST In State GST');
assert.ok(cur_frm.doc.name=='TEST In State GST - FT');
},
() => done()
]);
});

View File

@ -516,15 +516,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
} }
} }
// project name
//--------------------------
cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) {
return{
query: "erpnext.controllers.queries.get_project_name",
filters: {'customer': doc.customer}
}
}
// Income Account in Details Table // Income Account in Details Table
// -------------------------------- // --------------------------------
cur_frm.set_query("income_account", "items", function(doc) { cur_frm.set_query("income_account", "items", function(doc) {
@ -978,7 +969,7 @@ frappe.ui.form.on('Sales Invoice', {
} }
if (frm.doc.is_debit_note) { if (frm.doc.is_debit_note) {
frm.set_df_property('return_against', 'label', 'Adjustment Against'); frm.set_df_property('return_against', 'label', __('Adjustment Against'));
} }
if (frappe.boot.active_domains.includes("Healthcare")) { if (frappe.boot.active_domains.includes("Healthcare")) {
@ -988,10 +979,10 @@ frappe.ui.form.on('Sales Invoice', {
if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) { if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) {
frm.add_custom_button(__('Healthcare Services'), function() { frm.add_custom_button(__('Healthcare Services'), function() {
get_healthcare_services_to_invoice(frm); get_healthcare_services_to_invoice(frm);
},"Get Items From"); },__("Get Items From"));
frm.add_custom_button(__('Prescriptions'), function() { frm.add_custom_button(__('Prescriptions'), function() {
get_drugs_to_invoice(frm); get_drugs_to_invoice(frm);
},"Get Items From"); },__("Get Items From"));
} }
} }
else { else {

View File

@ -182,6 +182,7 @@
"sales_team_section_break", "sales_team_section_break",
"sales_partner", "sales_partner",
"column_break10", "column_break10",
"amount_eligible_for_commission",
"commission_rate", "commission_rate",
"total_commission", "total_commission",
"section_break2", "section_break2",
@ -650,7 +651,7 @@
"hide_seconds": 1, "hide_seconds": 1,
"label": "Ignore Pricing Rule", "label": "Ignore Pricing Rule",
"no_copy": 1, "no_copy": 1,
"permlevel": 1, "permlevel": 0,
"print_hide": 1 "print_hide": 1
}, },
{ {
@ -2019,6 +2020,12 @@
"label": "Total Billing Hours", "label": "Total Billing Hours",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -2031,7 +2038,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-10-11 20:19:38.667508", "modified": "2021-12-23 20:19:38.667508",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -155,6 +155,8 @@ class SalesInvoice(SellingController):
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated: if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
validate_loyalty_points(self, self.loyalty_points) validate_loyalty_points(self, self.loyalty_points)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_fixed_asset(self): def validate_fixed_asset(self):
for d in self.get("items"): for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
@ -842,8 +844,6 @@ class SalesInvoice(SellingController):
self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
self.make_item_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries)
self.make_discount_gl_entries(gl_entries) self.make_discount_gl_entries(gl_entries)
@ -862,11 +862,11 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total # Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on 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 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(): if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle # 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( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
"account": self.debit_to, "account": self.debit_to,
@ -874,8 +874,8 @@ class SalesInvoice(SellingController):
"party": self.customer, "party": self.customer,
"due_date": self.due_date, "due_date": self.due_date,
"against": self.against_income_account, "against": self.against_income_account,
"debit": grand_total_in_company_currency, "debit": base_grand_total,
"debit_in_account_currency": grand_total_in_company_currency \ "debit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else 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": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype, "against_voucher_type": self.doctype,
@ -1049,6 +1049,8 @@ class SalesInvoice(SellingController):
frappe.flags.is_reverse_depr_entry = False frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None schedule.journal_entry = None
depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
asset.finance_books[0].value_after_depreciation += depreciation_amount
asset.save() asset.save()
def get_posting_date_of_sales_invoice(self): def get_posting_date_of_sales_invoice(self):
@ -1071,6 +1073,12 @@ class SalesInvoice(SellingController):
return False return False
def get_depreciation_amount_in_je(self, journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
@property @property
def enable_discount_accounting(self): def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"): if not hasattr(self, "_enable_discount_accounting"):

View File

@ -1,73 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice", function(assert) {
assert.expect(9);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'},
{payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grand Total correct");
assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
},
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
},
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.tests.set_form_values(cur_frm, [{'payment_terms_schedule': ''}]),
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
},
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.tests.set_form_values(cur_frm, [{'payment_schedule': []}]),
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(!cur_dialog, 'Message is not shown');
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -20,6 +20,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
from erpnext.accounts.utils import PaymentEntryUnlinkError from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries 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.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.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.regional.india.utils import get_ewb_data from erpnext.regional.india.utils import get_ewb_data
@ -1603,28 +1604,12 @@ class TestSalesInvoice(unittest.TestCase):
si.shipping_rule = shipping_rule.name si.shipping_rule = shipping_rule.name
si.insert() si.insert()
shipping_amount = 0.0
for condition in shipping_rule.get("conditions"):
if not condition.to_value or (flt(condition.from_value) <= si.net_total <= flt(condition.to_value)):
shipping_amount = condition.shipping_amount
shipping_charge = {
"doctype": "Sales Taxes and Charges",
"category": "Valuation and Total",
"charge_type": "Actual",
"account_head": shipping_rule.account,
"cost_center": shipping_rule.cost_center,
"tax_amount": shipping_amount,
"description": shipping_rule.name
}
si.append("taxes", shipping_charge)
si.save() si.save()
self.assertEqual(si.net_total, 1250) self.assertEqual(si.net_total, 1250)
self.assertEqual(si.total_taxes_and_charges, 577.05) self.assertEqual(si.total_taxes_and_charges, 468.85)
self.assertEqual(si.grand_total, 1827.05) self.assertEqual(si.grand_total, 1718.85)
@ -2316,6 +2301,7 @@ class TestSalesInvoice(unittest.TestCase):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer, make_customer,
) )
from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
# create a customer # create a customer
@ -2324,13 +2310,7 @@ class TestSalesInvoice(unittest.TestCase):
supplier = create_supplier(supplier_name="_Test Common Supplier").name supplier = create_supplier(supplier_name="_Test Common Supplier").name
# create a party link between customer & supplier # create a party link between customer & supplier
# set primary role as supplier party_link = create_party_link("Supplier", supplier, customer)
party_link = frappe.new_doc("Party Link")
party_link.primary_role = "Supplier"
party_link.primary_party = supplier
party_link.secondary_role = "Customer"
party_link.secondary_party = customer
party_link.save()
# enable common party accounting # enable common party accounting
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1)
@ -2406,6 +2386,64 @@ class TestSalesInvoice(unittest.TestCase):
si.reload() si.reload()
self.assertEqual(si.status, "Paid") 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])
item.update({
"qty": 1,
"rate": 500,
"grant_commission": 1
})
si.append("items", item)
# Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
si.commission_rate = commission_rate
si.save()
self.assertEqual(si.amount_eligible_for_commission, 500)
self.assertEqual(si.total_commission, total_commission)
# Test invalid values
for commission_rate in (101, -1):
si.reload()
si.commission_rate = commission_rate
self.assertRaises(frappe.ValidationError, si.save)
def test_sales_invoice_submission_post_account_freezing_date(self): def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True) si = create_sales_invoice(do_not_save=True)
@ -2418,6 +2456,32 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def test_over_billing_case_against_delivery_note(self):
'''
Test a case where duplicating the item with qty = 1 in the invoice
allows overbilling even if it is disabled
'''
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance')
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0)
dn = create_delivery_note()
dn.submit()
si = make_sales_invoice(dn.name)
# make a copy of first item and add it to invoice
item_copy = frappe.copy_doc(si.items[0])
si.append('items', item_copy)
si.save()
with self.assertRaises(frappe.ValidationError) as err:
si.submit()
self.assertTrue("cannot overbill" in str(err.exception).lower())
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
def get_sales_invoice_for_e_invoice(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'

View File

@ -1,42 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1,35 +0,0 @@
QUnit.module('Accounts');
QUnit.test("test sales invoice with margin", function(assert) {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{selling_price_list: 'Test-Selling-USD'},
{currency: 'USD'},
{items: [
[
{'item_code': 'Test Product 4'},
{'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{'qty': 1},
{'margin_type': 'Percentage'},
{'margin_rate_or_amount': 20}
]
]}
]);
},
() => cur_frm.save(),
() => {
assert.ok(cur_frm.doc.items[0].rate_with_margin == 240, "Margin rate correct");
assert.ok(cur_frm.doc.items[0].base_rate_with_margin == cur_frm.doc.conversion_rate * 240, "Base margin rate correct");
assert.ok(cur_frm.doc.total == 240, "Amount correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1,56 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice with payment", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'},
{payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(2),
() => frappe.tests.click_button('Close'),
() => frappe.tests.click_button('Make'),
() => frappe.tests.click_link('Payment'),
() => frappe.timeout(0.2),
() => { cur_frm.set_value('mode_of_payment','Cash');},
() => { cur_frm.set_value('paid_to','Cash - '+frappe.get_abbr(frappe.defaults.get_default('Company')));},
() => {cur_frm.set_value('reference_no','TEST1234');},
() => {cur_frm.set_value('reference_date',frappe.datetime.add_days(frappe.datetime.nowdate(), 0));},
() => cur_frm.save(),
() => {
// get payment details
assert.ok(cur_frm.doc.paid_amount==590, "Paid Amount Correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => done()
]);
});

View File

@ -1,51 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice with payment request", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(2),
() => frappe.tests.click_button('Close'),
() => frappe.tests.click_button('Make'),
() => frappe.tests.click_link('Payment Request'),
() => frappe.timeout(0.2),
() => { cur_frm.set_value('print_format','GST Tax Invoice');},
() => { cur_frm.set_value('email_to','test@gmail.com');},
() => cur_frm.save(),
() => {
// get payment details
assert.ok(cur_frm.doc.grand_total==590, "grand total Correct");
},
() => done()
]);
});

View File

@ -1,44 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice with serialize item", function(assert) {
assert.expect(5);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 2},
{'item_code': 'Test Product 4'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// get batch number
assert.ok(cur_frm.doc.items[0].batch_no=='TEST-BATCH-001', " Batch Details correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==218, "Grad Total correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

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