mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-09 00:01:18 +00:00
Merge pull request #25820 from deepeshgarg007/update-staging
chore: Update branch
This commit is contained in:
46
.github/helper/install.sh
vendored
Normal file
46
.github/helper/install.sh
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
sudo apt-get install redis-server
|
||||
|
||||
sudo apt install nodejs
|
||||
|
||||
sudo apt install npm
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" --depth 1
|
||||
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
|
||||
|
||||
mkdir ~/frappe-bench/sites/test_site
|
||||
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/
|
||||
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
|
||||
sudo chmod o+x /usr/local/bin/wkhtmltopdf
|
||||
sudo apt-get install libcups2-dev
|
||||
|
||||
cd ~/frappe-bench || exit
|
||||
|
||||
sed -i 's/watch:/# watch:/g' Procfile
|
||||
sed -i 's/schedule:/# schedule:/g' Procfile
|
||||
sed -i 's/socketio:/# socketio:/g' Procfile
|
||||
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
||||
|
||||
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
||||
bench start &
|
||||
bench --site test_site reinstall --yes
|
||||
38
.github/helper/semgrep_rules/README.md
vendored
Normal file
38
.github/helper/semgrep_rules/README.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Semgrep linting
|
||||
|
||||
## What is semgrep?
|
||||
Semgrep or "semantic grep" is language agnostic static analysis tool. In simple terms semgrep is syntax-aware `grep`, so unlike regex it doesn't get confused by different ways of writing same thing or whitespaces or code split in multiple lines etc.
|
||||
|
||||
Example:
|
||||
|
||||
To check if a translate function is using f-string or not the regex would be `r"_\(\s*f[\"']"` while equivalent rule in semgrep would be `_(f"...")`. As semgrep knows grammer of language it takes care of unnecessary whitespace, type of quotation marks etc.
|
||||
|
||||
You can read more such examples in `.github/helper/semgrep_rules` directory.
|
||||
|
||||
# Why/when to use this?
|
||||
We want to maintain quality of contributions, at the same time remembering all the good practices can be pain to deal with while evaluating contributions. Using semgrep if you can translate "best practice" into a rule then it can automate the task for us.
|
||||
|
||||
## Running locally
|
||||
|
||||
Install semgrep using homebrew `brew install semgrep` or pip `pip install semgrep`.
|
||||
|
||||
To run locally use following command:
|
||||
|
||||
`semgrep --config=.github/helper/semgrep_rules [file/folder names]`
|
||||
|
||||
## Testing
|
||||
semgrep allows testing the tests. Refer to this page: https://semgrep.dev/docs/writing-rules/testing-rules/
|
||||
|
||||
When writing new rules you should write few positive and few negative cases as shown in the guide and current tests.
|
||||
|
||||
To run current tests: `semgrep --test --test-ignore-todo .github/helper/semgrep_rules`
|
||||
|
||||
|
||||
## Reference
|
||||
|
||||
If you are new to Semgrep read following pages to get started on writing/modifying rules:
|
||||
|
||||
- https://semgrep.dev/docs/getting-started/
|
||||
- https://semgrep.dev/docs/writing-rules/rule-syntax
|
||||
- https://semgrep.dev/docs/writing-rules/pattern-examples/
|
||||
- https://semgrep.dev/docs/writing-rules/rule-ideas/#common-use-cases
|
||||
28
.github/helper/semgrep_rules/frappe_correctness.py
vendored
Normal file
28
.github/helper/semgrep_rules/frappe_correctness.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import frappe
|
||||
from frappe import _, flt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
def on_submit(self):
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
# ruleid: frappe-modifying-after-submit
|
||||
self.status = 'Submitted'
|
||||
|
||||
def on_submit(self):
|
||||
if flt(self.per_billed) < 100:
|
||||
self.update_billing_status()
|
||||
else:
|
||||
# todook: frappe-modifying-after-submit
|
||||
self.status = "Completed"
|
||||
self.db_set("status", "Completed")
|
||||
|
||||
class TestDoc(Document):
|
||||
pass
|
||||
|
||||
def validate(self):
|
||||
#ruleid: frappe-modifying-child-tables-while-iterating
|
||||
for item in self.child_table:
|
||||
if item.value < 0:
|
||||
self.remove(item)
|
||||
74
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
Normal file
74
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# This file specifies rules for correctness according to how frappe doctype data model works.
|
||||
|
||||
rules:
|
||||
- id: frappe-modifying-after-submit
|
||||
patterns:
|
||||
- pattern: self.$ATTR = ...
|
||||
- pattern-inside: |
|
||||
def on_submit(self, ...):
|
||||
...
|
||||
- metavariable-regex:
|
||||
metavariable: '$ATTR'
|
||||
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
|
||||
regex: '^(?!status_updater)(.*)$'
|
||||
message: |
|
||||
Doctype modified after submission. Please check if modification of self.$ATTR is commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-modifying-after-cancel
|
||||
patterns:
|
||||
- pattern: self.$ATTR = ...
|
||||
- pattern-inside: |
|
||||
def on_cancel(self, ...):
|
||||
...
|
||||
- metavariable-regex:
|
||||
metavariable: '$ATTR'
|
||||
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
|
||||
message: |
|
||||
Doctype modified after cancellation. Please check if modification of self.$ATTR is commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-print-function-in-doctypes
|
||||
pattern: print(...)
|
||||
message: |
|
||||
Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
paths:
|
||||
exclude:
|
||||
- test_*.py
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
- id: frappe-modifying-child-tables-while-iterating
|
||||
pattern-either:
|
||||
- pattern: |
|
||||
for $ROW in self.$TABLE:
|
||||
...
|
||||
self.remove(...)
|
||||
- pattern: |
|
||||
for $ROW in self.$TABLE:
|
||||
...
|
||||
self.append(...)
|
||||
message: |
|
||||
Child table being modified while iterating on it.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
paths:
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
- id: frappe-same-key-assigned-twice
|
||||
pattern-either:
|
||||
- pattern: |
|
||||
{..., $X: $A, ..., $X: $B, ...}
|
||||
- pattern: |
|
||||
dict(..., ($X, $A), ..., ($X, $B), ...)
|
||||
- pattern: |
|
||||
_dict(..., ($X, $A), ..., ($X, $B), ...)
|
||||
message: |
|
||||
key `$X` is uselessly assigned twice. This could be a potential bug.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
6
.github/helper/semgrep_rules/security.py
vendored
Normal file
6
.github/helper/semgrep_rules/security.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
def function_name(input):
|
||||
# ruleid: frappe-codeinjection-eval
|
||||
eval(input)
|
||||
|
||||
# ok: frappe-codeinjection-eval
|
||||
eval("1 + 1")
|
||||
25
.github/helper/semgrep_rules/security.yml
vendored
Normal file
25
.github/helper/semgrep_rules/security.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
rules:
|
||||
- id: frappe-codeinjection-eval
|
||||
patterns:
|
||||
- pattern-not: eval("...")
|
||||
- pattern: eval(...)
|
||||
message: |
|
||||
Detected the use of eval(). eval() can be dangerous if used to evaluate
|
||||
dynamic content. Avoid it or use safe_eval().
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-sqli-format-strings
|
||||
patterns:
|
||||
- pattern-inside: |
|
||||
@frappe.whitelist()
|
||||
def $FUNC(...):
|
||||
...
|
||||
- pattern-either:
|
||||
- pattern: frappe.db.sql("..." % ...)
|
||||
- pattern: frappe.db.sql(f"...", ...)
|
||||
- pattern: frappe.db.sql("...".format(...), ...)
|
||||
message: |
|
||||
Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
37
.github/helper/semgrep_rules/translate.js
vendored
Normal file
37
.github/helper/semgrep_rules/translate.js
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
// ruleid: frappe-translation-empty-string
|
||||
__("")
|
||||
// ruleid: frappe-translation-empty-string
|
||||
__('')
|
||||
|
||||
// ok: frappe-translation-js-formatting
|
||||
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
|
||||
|
||||
// ruleid: frappe-translation-js-formatting
|
||||
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
|
||||
|
||||
// ok: frappe-translation-js-formatting
|
||||
__('This is fine');
|
||||
|
||||
|
||||
// ok: frappe-translation-trailing-spaces
|
||||
__('This is fine');
|
||||
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__(' this is not ok ');
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__('this is not ok ');
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__(' this is not ok');
|
||||
|
||||
// ok: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers in your mailing list.', [subscribers.length])
|
||||
|
||||
// todoruleid: frappe-translation-js-splitting
|
||||
__('You have') + subscribers.length + __('subscribers in your mailing list.')
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have' + 'subscribers in your mailing list.')
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers' +
|
||||
'in your mailing list', [subscribers.length])
|
||||
53
.github/helper/semgrep_rules/translate.py
vendored
Normal file
53
.github/helper/semgrep_rules/translate.py
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# Examples taken from https://frappeframework.com/docs/user/en/translations
|
||||
# This file is used for testing the tests.
|
||||
|
||||
from frappe import _
|
||||
|
||||
full_name = "Jon Doe"
|
||||
# ok: frappe-translation-python-formatting
|
||||
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
|
||||
|
||||
|
||||
subscribers = ["Jon", "Doe"]
|
||||
# ok: frappe-translation-python-formatting
|
||||
_('You have {0} subscribers in your mailing list.').format(len(subscribers))
|
||||
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_('You have') + len(subscribers) + _('subscribers in your mailing list.')
|
||||
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_('You have {0} subscribers \
|
||||
in your mailing list').format(len(subscribers))
|
||||
|
||||
# ok: frappe-translation-python-splitting
|
||||
_('You have {0} subscribers') \
|
||||
+ 'in your mailing list'
|
||||
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _(" You have {0} pending invoice ")
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _("You have {0} pending invoice ")
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _(" You have {0} pending invoice")
|
||||
|
||||
# ok: frappe-translation-trailing-spaces
|
||||
msg = ' ' + _("You have {0} pending invoices") + ' '
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_(f"can not format like this - {subscribers}")
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_(f"what" + f"this is also not cool")
|
||||
|
||||
|
||||
# ruleid: frappe-translation-empty-string
|
||||
_("")
|
||||
# ruleid: frappe-translation-empty-string
|
||||
_('')
|
||||
63
.github/helper/semgrep_rules/translate.yml
vendored
Normal file
63
.github/helper/semgrep_rules/translate.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
rules:
|
||||
- id: frappe-translation-empty-string
|
||||
pattern-either:
|
||||
- pattern: _("")
|
||||
- pattern: __("")
|
||||
message: |
|
||||
Empty string is useless for translation.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python, javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-trailing-spaces
|
||||
pattern-either:
|
||||
- pattern: _("=~/(^[ \t]+|[ \t]+$)/")
|
||||
- pattern: __("=~/(^[ \t]+|[ \t]+$)/")
|
||||
message: |
|
||||
Trailing or leading whitespace not allowed in translate strings.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python, javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-python-formatting
|
||||
pattern-either:
|
||||
- pattern: _("..." % ...)
|
||||
- pattern: _("...".format(...))
|
||||
- pattern: _(f"...")
|
||||
message: |
|
||||
Only positional formatters are allowed and formatting should not be done before translating.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-js-formatting
|
||||
patterns:
|
||||
- pattern: __(`...`)
|
||||
- pattern-not: __("...")
|
||||
message: |
|
||||
Template strings are not allowed for text formatting.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-python-splitting
|
||||
pattern-either:
|
||||
- pattern: _(...) + ... + _(...)
|
||||
- pattern: _("..." + "...")
|
||||
- pattern-regex: '_\([^\)]*\\\s*'
|
||||
message: |
|
||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-js-splitting
|
||||
pattern-either:
|
||||
- pattern-regex: '__\([^\)]*[\+\\]\s*'
|
||||
- pattern: __('...' + '...')
|
||||
- pattern: __('...') + __('...')
|
||||
message: |
|
||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [javascript, json]
|
||||
severity: ERROR
|
||||
31
.github/helper/semgrep_rules/ux.py
vendored
Normal file
31
.github/helper/semgrep_rules/ux.py
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import frappe
|
||||
from frappe import msgprint, throw, _
|
||||
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
throw("Error Occured")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
frappe.throw("Error Occured")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
frappe.msgprint("Useful message")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
msgprint("Useful message")
|
||||
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
translatedmessage = _("Hello")
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
throw(translatedmessage)
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
msgprint(translatedmessage)
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
msgprint(_("Helpful message"))
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
frappe.throw(_("Error occured"))
|
||||
15
.github/helper/semgrep_rules/ux.yml
vendored
Normal file
15
.github/helper/semgrep_rules/ux.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
rules:
|
||||
- id: frappe-missing-translate-function
|
||||
pattern-either:
|
||||
- patterns:
|
||||
- pattern: frappe.msgprint("...", ...)
|
||||
- pattern-not: frappe.msgprint(_("..."), ...)
|
||||
- pattern-not: frappe.msgprint(__("..."), ...)
|
||||
- patterns:
|
||||
- pattern: frappe.throw("...", ...)
|
||||
- pattern-not: frappe.throw(_("..."), ...)
|
||||
- pattern-not: frappe.throw(__("..."), ...)
|
||||
message: |
|
||||
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
|
||||
languages: [python, javascript, json]
|
||||
severity: ERROR
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"db_host": "127.0.0.1",
|
||||
"db_port": 3306,
|
||||
"db_name": "test_frappe",
|
||||
"db_password": "test_frappe",
|
||||
"auto_email_id": "test@example.com",
|
||||
108
.github/workflows/ci-tests.yml
vendored
Normal file
108
.github/workflows/ci-tests.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request, workflow_dispatch, push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- TYPE: "server"
|
||||
JOB_NAME: "Server"
|
||||
RUN_COMMAND: cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --coverage
|
||||
- TYPE: "patch"
|
||||
JOB_NAME: "Patch"
|
||||
RUN_COMMAND: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate
|
||||
|
||||
name: ${{ matrix.JOB_NAME }}
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mariadb:10.3
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: YES
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
|
||||
- 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
|
||||
|
||||
- name: Run Tests
|
||||
run: ${{ matrix.RUN_COMMAND }}
|
||||
env:
|
||||
TYPE: ${{ matrix.TYPE }}
|
||||
|
||||
- name: Coverage - Pull Request
|
||||
if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip install coveralls==2.2.0
|
||||
pip install coverage==4.5.4
|
||||
coveralls --service=github
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
COVERALLS_SERVICE_NAME: github
|
||||
|
||||
- name: Coverage - Push
|
||||
if: matrix.TYPE == 'server' && github.event_name == 'push'
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip install coveralls==2.2.0
|
||||
pip install coverage==4.5.4
|
||||
coveralls --service=github-actions
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
COVERALLS_SERVICE_NAME: github-actions
|
||||
|
||||
24
.github/workflows/semgrep.yml
vendored
Normal file
24
.github/workflows/semgrep.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Run semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
|
||||
semgrep --config="r/python.lang.correctness" --quiet --error $files
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
|
||||
69
.travis.yml
69
.travis.yml
@@ -1,69 +0,0 @@
|
||||
language: python
|
||||
dist: trusty
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
cache:
|
||||
- pip
|
||||
|
||||
addons:
|
||||
hosts: test_site
|
||||
mariadb: 10.3
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- name: "Python 3.6 Server Side Test"
|
||||
python: 3.6
|
||||
script: bench --site test_site run-tests --app erpnext --coverage
|
||||
|
||||
- name: "Python 3.6 Patch Test"
|
||||
python: 3.6
|
||||
before_script:
|
||||
- wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz
|
||||
- bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz
|
||||
script: bench --site test_site migrate
|
||||
|
||||
install:
|
||||
- cd ~
|
||||
- nvm install 10
|
||||
|
||||
- pip install frappe-bench
|
||||
|
||||
- git clone https://github.com/frappe/frappe --branch $TRAVIS_BRANCH --depth 1
|
||||
- bench init --skip-assets --frappe-path ~/frappe --python $(which python) frappe-bench
|
||||
|
||||
- mkdir ~/frappe-bench/sites/test_site
|
||||
- cp -r $TRAVIS_BUILD_DIR/.travis/site_config.json ~/frappe-bench/sites/test_site/
|
||||
|
||||
- mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||
- mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||
|
||||
- mysql -u root -e "CREATE DATABASE test_frappe"
|
||||
- mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
||||
- mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
||||
|
||||
- mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
|
||||
- mysql -u root -e "FLUSH PRIVILEGES"
|
||||
|
||||
- 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
|
||||
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
|
||||
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
|
||||
- sudo apt-get install libcups2-dev
|
||||
|
||||
- cd ~/frappe-bench
|
||||
|
||||
- sed -i 's/watch:/# watch:/g' Procfile
|
||||
- sed -i 's/schedule:/# schedule:/g' Procfile
|
||||
- sed -i 's/socketio:/# socketio:/g' Procfile
|
||||
- sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
||||
|
||||
- bench get-app erpnext $TRAVIS_BUILD_DIR
|
||||
- bench start &
|
||||
- bench --site test_site reinstall --yes
|
||||
|
||||
after_script:
|
||||
- pip install coverage==4.5.4
|
||||
- pip install python-coveralls
|
||||
- coveralls -b apps/erpnext -d ../../sites/.coverage
|
||||
@@ -5,7 +5,7 @@
|
||||
<p>ERP made simple</p>
|
||||
</p>
|
||||
|
||||
[](https://travis-ci.com/frappe/erpnext)
|
||||
[](https://github.com/frappe/erpnext/actions/workflows/ci-tests.yml)
|
||||
[](https://www.codetriage.com/frappe/erpnext)
|
||||
[](https://coveralls.io/github/frappe/erpnext?branch=develop)
|
||||
|
||||
@@ -39,6 +39,10 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a
|
||||
|
||||
---
|
||||
|
||||
### Containerized Installation
|
||||
|
||||
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
|
||||
|
||||
### Full Install
|
||||
|
||||
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.
|
||||
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.0.2'
|
||||
__version__ = '13.3.1'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
||||
@@ -13,7 +13,7 @@ class BalanceMismatchError(frappe.ValidationError): pass
|
||||
class Account(NestedSet):
|
||||
nsm_parent_field = 'parent_account'
|
||||
def on_update(self):
|
||||
if frappe.local.flags.ignore_on_update:
|
||||
if frappe.local.flags.ignore_update_nsm:
|
||||
return
|
||||
else:
|
||||
super(Account, self).on_update()
|
||||
@@ -214,6 +214,7 @@ class Account(NestedSet):
|
||||
if parent_value_changed:
|
||||
doc.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_group_to_ledger(self):
|
||||
if self.check_if_child_exists():
|
||||
throw(_("Account with child nodes cannot be converted to ledger"))
|
||||
@@ -224,6 +225,7 @@ class Account(NestedSet):
|
||||
self.save()
|
||||
return 1
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_ledger_to_group(self):
|
||||
if self.check_gle_exists():
|
||||
throw(_("Account with existing transaction can not be converted to group."))
|
||||
|
||||
@@ -57,10 +57,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
|
||||
|
||||
# Rebuild NestedSet HSM tree for Account Doctype
|
||||
# after all accounts are already inserted.
|
||||
frappe.local.flags.ignore_on_update = True
|
||||
frappe.local.flags.ignore_update_nsm = True
|
||||
_import_accounts(chart, None, None, root_account=True)
|
||||
rebuild_tree("Account", "parent_account")
|
||||
frappe.local.flags.ignore_on_update = False
|
||||
frappe.local.flags.ignore_update_nsm = False
|
||||
|
||||
def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||
if account_number:
|
||||
|
||||
@@ -39,6 +39,7 @@ class AccountingPeriod(Document):
|
||||
frappe.throw(_("Accounting Period overlaps with {0}")
|
||||
.format(existing_accounting_period[0].get("name")), OverlapError)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_closing(self):
|
||||
docs_for_closing = []
|
||||
doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \
|
||||
|
||||
@@ -11,36 +11,36 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
class TestAccountingPeriod(unittest.TestCase):
|
||||
def test_overlap(self):
|
||||
ap1 = create_accounting_period(start_date = "2018-04-01",
|
||||
end_date = "2018-06-30", company = "Wind Power LLC")
|
||||
ap1.save()
|
||||
def test_overlap(self):
|
||||
ap1 = create_accounting_period(start_date = "2018-04-01",
|
||||
end_date = "2018-06-30", company = "Wind Power LLC")
|
||||
ap1.save()
|
||||
|
||||
ap2 = create_accounting_period(start_date = "2018-06-30",
|
||||
end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
|
||||
self.assertRaises(OverlapError, ap2.save)
|
||||
ap2 = create_accounting_period(start_date = "2018-06-30",
|
||||
end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
|
||||
self.assertRaises(OverlapError, ap2.save)
|
||||
|
||||
def test_accounting_period(self):
|
||||
ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
|
||||
ap1.save()
|
||||
def test_accounting_period(self):
|
||||
ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
|
||||
ap1.save()
|
||||
|
||||
doc = create_sales_invoice(do_not_submit=1, cost_center = "_Test Company - _TC", warehouse = "Stores - _TC")
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.submit)
|
||||
doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.submit)
|
||||
|
||||
def tearDown(self):
|
||||
for d in frappe.get_all("Accounting Period"):
|
||||
frappe.delete_doc("Accounting Period", d.name)
|
||||
def tearDown(self):
|
||||
for d in frappe.get_all("Accounting Period"):
|
||||
frappe.delete_doc("Accounting Period", d.name)
|
||||
|
||||
def create_accounting_period(**args):
|
||||
args = frappe._dict(args)
|
||||
args = frappe._dict(args)
|
||||
|
||||
accounting_period = frappe.new_doc("Accounting Period")
|
||||
accounting_period.start_date = args.start_date or nowdate()
|
||||
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
|
||||
accounting_period.company = args.company or "_Test Company"
|
||||
accounting_period.period_name =args.period_name or "_Test_Period_Name_1"
|
||||
accounting_period.append("closed_documents", {
|
||||
"document_type": 'Sales Invoice', "closed": 1
|
||||
})
|
||||
accounting_period = frappe.new_doc("Accounting Period")
|
||||
accounting_period.start_date = args.start_date or nowdate()
|
||||
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
|
||||
accounting_period.company = args.company or "_Test Company"
|
||||
accounting_period.period_name =args.period_name or "_Test_Period_Name_1"
|
||||
accounting_period.append("closed_documents", {
|
||||
"document_type": 'Sales Invoice', "closed": 1
|
||||
})
|
||||
|
||||
return accounting_period
|
||||
return accounting_period
|
||||
|
||||
@@ -7,25 +7,30 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"auto_accounting_for_stock",
|
||||
"acc_frozen_upto",
|
||||
"frozen_accounts_modifier",
|
||||
"determine_address_tax_category_from",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
"column_break_4",
|
||||
"credit_controller",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"role_allowed_to_over_bill",
|
||||
"make_payment_via_journal_entry",
|
||||
"column_break_11",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
"unlink_payment_on_cancellation_of_invoice",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"automatically_fetch_payment_terms",
|
||||
"delete_linked_ledger_entries",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"tax_settings_section",
|
||||
"determine_address_tax_category_from",
|
||||
"column_break_19",
|
||||
"add_taxes_from_item_tax_template",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"frozen_accounts_modifier",
|
||||
"column_break_4",
|
||||
"credit_controller",
|
||||
"deferred_accounting_settings_section",
|
||||
"automatically_process_deferred_accounting_entry",
|
||||
"book_deferred_entries_based_on",
|
||||
"column_break_18",
|
||||
"automatically_process_deferred_accounting_entry",
|
||||
"book_deferred_entries_via_journal_entry",
|
||||
"submit_journal_entries",
|
||||
"print_settings",
|
||||
@@ -39,15 +44,6 @@
|
||||
"use_custom_cash_flow"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, the system will post accounting entries for inventory automatically",
|
||||
"fieldname": "auto_accounting_for_stock",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Make Accounting Entry For Every Stock Movement"
|
||||
},
|
||||
{
|
||||
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||
"fieldname": "acc_frozen_upto",
|
||||
@@ -93,6 +89,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "make_payment_via_journal_entry",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Make Payment via Journal Entry"
|
||||
},
|
||||
{
|
||||
@@ -226,6 +223,36 @@
|
||||
"fieldname": "delete_linked_ledger_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to over bill above the allowance percentage",
|
||||
"fieldname": "role_allowed_to_over_bill",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Over Bill ",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"fieldname": "period_closing_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Period Closing Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts_transactions_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transactions Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tax Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -233,7 +260,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-05 13:04:00.118892",
|
||||
"modified": "2021-04-30 15:25:10.381008",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -30,5 +30,5 @@ class AccountsSettings(Document):
|
||||
def enable_payment_schedule_in_print(self):
|
||||
show_in_print = cint(self.show_payment_schedule_in_print)
|
||||
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
|
||||
make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check")
|
||||
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check")
|
||||
make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False)
|
||||
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False)
|
||||
|
||||
@@ -42,10 +42,9 @@ let add_fields_to_mapping_table = function (frm) {
|
||||
});
|
||||
});
|
||||
|
||||
frappe.meta.get_docfield("Bank Transaction Mapping", "bank_transaction_field",
|
||||
frm.doc.name).options = options;
|
||||
|
||||
frm.fields_dict.bank_transaction_mapping.grid.refresh();
|
||||
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
|
||||
'bank_transaction_field', 'options', options
|
||||
);
|
||||
};
|
||||
|
||||
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||
|
||||
@@ -12,6 +12,7 @@ form_grid_templates = {
|
||||
}
|
||||
|
||||
class BankClearance(Document):
|
||||
@frappe.whitelist()
|
||||
def get_payment_entries(self):
|
||||
if not (self.from_date and self.to_date):
|
||||
frappe.throw(_("From Date and To Date are Mandatory"))
|
||||
@@ -108,6 +109,7 @@ class BankClearance(Document):
|
||||
row.update(d)
|
||||
self.total_amount += flt(amount)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_clearance_date(self):
|
||||
clearance_date_updated = False
|
||||
for d in self.get('payment_entries'):
|
||||
|
||||
@@ -8,6 +8,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
return {
|
||||
filters: {
|
||||
company: ["in", frm.doc.company],
|
||||
'is_company_account': 1
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -77,8 +78,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
if (
|
||||
frm.doc.bank_account &&
|
||||
frm.doc.bank_statement_from_date &&
|
||||
frm.doc.bank_statement_to_date &&
|
||||
frm.doc.bank_statement_closing_balance
|
||||
frm.doc.bank_statement_to_date
|
||||
) {
|
||||
frm.trigger("render_chart");
|
||||
frm.trigger("render");
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
"depends_on": "eval: doc.bank_account",
|
||||
"fieldname": "bank_statement_from_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Bank Statement From Date"
|
||||
"label": "From Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bank_statement_from_date",
|
||||
"fieldname": "bank_statement_to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Bank Statement To Date"
|
||||
"label": "To Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
@@ -63,11 +63,10 @@
|
||||
"depends_on": "eval: doc.bank_statement_to_date",
|
||||
"fieldname": "bank_statement_closing_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Bank Statement Closing Balance",
|
||||
"label": "Closing Balance",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bank_statement_closing_balance",
|
||||
"fieldname": "section_break_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reconcile"
|
||||
@@ -90,7 +89,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-02 01:35:53.043578",
|
||||
"modified": "2021-04-21 11:13:49.831769",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Reconciliation Tool",
|
||||
|
||||
@@ -239,6 +239,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
"withdrawal",
|
||||
"description",
|
||||
"reference_number",
|
||||
"bank_account"
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -532,43 +533,4 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
</table>
|
||||
`);
|
||||
},
|
||||
|
||||
show_missing_link_values(frm, missing_link_values) {
|
||||
let can_be_created_automatically = missing_link_values.every(
|
||||
(d) => d.has_one_mandatory_field
|
||||
);
|
||||
|
||||
let html = missing_link_values
|
||||
.map((d) => {
|
||||
let doctype = d.doctype;
|
||||
let values = d.missing_values;
|
||||
return `
|
||||
<h5>${doctype}</h5>
|
||||
<ul>${values.map((v) => `<li>${v}</li>`).join("")}</ul>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
if (can_be_created_automatically) {
|
||||
// prettier-ignore
|
||||
let message = __('There are some linked records which needs to be created before we can import your file. Do you want to create the following missing records automatically?');
|
||||
frappe.confirm(message + html, () => {
|
||||
frm.call("create_missing_link_values", {
|
||||
missing_link_values,
|
||||
}).then((r) => {
|
||||
let records = r.message;
|
||||
frappe.msgprint(__(
|
||||
"Created {0} records successfully.", [
|
||||
records.length,
|
||||
]
|
||||
));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint(
|
||||
// prettier-ignore
|
||||
__('The following records needs to be created before we can import your file.') + html
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
|
||||
"description": "Must be a publicly accessible Google Sheets URL",
|
||||
"description": "Must be a publicly accessible Google Sheets URL and adding Bank Account column is necessary for importing via Google Sheets",
|
||||
"fieldname": "google_sheets_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Import from Google Sheets"
|
||||
@@ -202,7 +202,7 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-10 19:29:59.027325",
|
||||
"modified": "2021-05-12 14:17:37.777246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
@@ -224,4 +224,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,13 @@ class BankStatementImport(DataImport):
|
||||
|
||||
def start_import(self):
|
||||
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
|
||||
if 'Bank Account' not in json.dumps(preview):
|
||||
frappe.throw(_("Please add the Bank Account column"))
|
||||
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
@@ -67,6 +74,7 @@ class BankStatementImport(DataImport):
|
||||
data_import=self.name,
|
||||
bank_account=self.bank_account,
|
||||
import_file_path=self.import_file,
|
||||
google_sheets_url=self.google_sheets_url,
|
||||
bank=self.bank,
|
||||
template_options=self.template_options,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||
@@ -90,18 +98,20 @@ def download_errored_template(data_import_name):
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
data_import.export_errored_rows()
|
||||
|
||||
def start_import(data_import, bank_account, import_file_path, bank, template_options):
|
||||
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
|
||||
"""This method runs in background job"""
|
||||
|
||||
update_mapping_db(bank, template_options)
|
||||
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import)
|
||||
file = import_file_path if import_file_path else google_sheets_url
|
||||
|
||||
import_file = ImportFile("Bank Transaction", file = import_file_path, import_type="Insert New Records")
|
||||
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
|
||||
data = import_file.raw_data
|
||||
|
||||
add_bank_account(data, bank_account)
|
||||
write_files(import_file, data)
|
||||
if import_file_path:
|
||||
add_bank_account(data, bank_account)
|
||||
write_files(import_file, data)
|
||||
|
||||
try:
|
||||
i = Importer(data_import.reference_doctype, data_import=data_import)
|
||||
|
||||
@@ -175,22 +175,24 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "deposit",
|
||||
"oldfieldname": "debit",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Deposit"
|
||||
"label": "Deposit",
|
||||
"oldfieldname": "debit",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "withdrawal",
|
||||
"oldfieldname": "credit",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Withdrawal"
|
||||
"label": "Withdrawal",
|
||||
"oldfieldname": "credit",
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-30 19:40:54.221070",
|
||||
"modified": "2021-04-14 17:31:58.963529",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -15,12 +15,14 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
|
||||
test_dependencies = ["Item", "Cost Center"]
|
||||
|
||||
class TestBankTransaction(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_pos_profile()
|
||||
add_transactions()
|
||||
add_vouchers()
|
||||
|
||||
def tearDown(self):
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
for bt in frappe.get_all("Bank Transaction"):
|
||||
doc = frappe.get_doc("Bank Transaction", bt.name)
|
||||
doc.cancel()
|
||||
@@ -33,9 +35,6 @@ class TestBankTransaction(unittest.TestCase):
|
||||
# Delete POS Profile
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
frappe.flags.test_bank_transactions_created = False
|
||||
frappe.flags.test_payments_created = False
|
||||
|
||||
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
|
||||
def test_linked_payments(self):
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
|
||||
@@ -44,8 +43,8 @@ class TestBankTransaction(unittest.TestCase):
|
||||
|
||||
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
|
||||
def test_reconcile(self):
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
|
||||
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
|
||||
vouchers = json.dumps([{
|
||||
"payment_doctype":"Payment Entry",
|
||||
"payment_name":payment.name,
|
||||
@@ -62,7 +61,6 @@ class TestBankTransaction(unittest.TestCase):
|
||||
def test_debit_credit_output(self):
|
||||
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
|
||||
linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
|
||||
print(linked_payments)
|
||||
self.assertTrue(linked_payments[0][3])
|
||||
|
||||
# Check error if already reconciled
|
||||
@@ -116,10 +114,6 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
|
||||
pass
|
||||
|
||||
def add_transactions():
|
||||
if frappe.flags.test_bank_transactions_created:
|
||||
return
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
create_bank_account()
|
||||
|
||||
doc = frappe.get_doc({
|
||||
@@ -172,14 +166,8 @@ def add_transactions():
|
||||
}).insert()
|
||||
doc.submit()
|
||||
|
||||
frappe.flags.test_bank_transactions_created = True
|
||||
|
||||
def add_vouchers():
|
||||
if frappe.flags.test_payments_created:
|
||||
return
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
try:
|
||||
frappe.get_doc({
|
||||
"doctype": "Supplier",
|
||||
@@ -272,13 +260,6 @@ def add_vouchers():
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "Fayva Oct 18"
|
||||
pe.reference_date = "2018-10-29"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
mode_of_payment = frappe.get_doc({
|
||||
"doctype": "Mode of Payment",
|
||||
"name": "Cash"
|
||||
@@ -291,14 +272,12 @@ def add_vouchers():
|
||||
})
|
||||
mode_of_payment.save()
|
||||
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_submit=1)
|
||||
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
|
||||
si.is_pos = 1
|
||||
si.append("payments", {
|
||||
"mode_of_payment": "Cash",
|
||||
"account": "_Test Bank - _TC",
|
||||
"amount": 109080
|
||||
})
|
||||
si.save()
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
frappe.flags.test_payments_created = True
|
||||
|
||||
@@ -57,6 +57,7 @@ class CForm(Document):
|
||||
total = sum([flt(d.grand_total) for d in self.get('invoices')])
|
||||
frappe.db.set(self, 'total_invoiced_amount', total)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_invoice_details(self, invoice_no):
|
||||
""" Pull details from invoices for referrence """
|
||||
if invoice_no:
|
||||
|
||||
@@ -293,6 +293,11 @@ def validate_accounts(file_name):
|
||||
accounts_dict = {}
|
||||
for account in accounts:
|
||||
accounts_dict.setdefault(account["account_name"], account)
|
||||
if not hasattr(account, "parent_account"):
|
||||
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
|
||||
msg += "<br><br>"
|
||||
msg += _("Alternatively, you can download the template and fill your data in.")
|
||||
frappe.throw(msg, title=_("Parent Account Missing"))
|
||||
if account["parent_account"] and accounts_dict.get(account["parent_account"]):
|
||||
accounts_dict[account["parent_account"]]["is_group"] = 1
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ class CostCenter(NestedSet):
|
||||
frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format(
|
||||
frappe.bold(self.parent_cost_center)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_group_to_ledger(self):
|
||||
if self.check_if_child_exists():
|
||||
frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))
|
||||
@@ -60,6 +61,7 @@ class CostCenter(NestedSet):
|
||||
self.save()
|
||||
return 1
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_ledger_to_group(self):
|
||||
if cint(self.enable_distributed_cost_center):
|
||||
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
|
||||
|
||||
@@ -42,9 +42,9 @@ class TestDunning(unittest.TestCase):
|
||||
['Sales - _TC', 0.0, 20.44]
|
||||
])
|
||||
for gle in gl_entries:
|
||||
self.assertEquals(expected_values[gle.account][0], gle.account)
|
||||
self.assertEquals(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEquals(expected_values[gle.account][2], gle.credit)
|
||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
|
||||
def test_payment_entry(self):
|
||||
dunning = create_dunning()
|
||||
|
||||
@@ -21,21 +21,17 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.docstatus==1) {
|
||||
frappe.db.get_value("Journal Entry Account", {
|
||||
'reference_type': 'Exchange Rate Revaluation',
|
||||
'reference_name': frm.doc.name,
|
||||
'docstatus': 1
|
||||
}, "sum(debit) as sum", (r) =>{
|
||||
let total_amt = 0;
|
||||
frm.doc.accounts.forEach(d=> {
|
||||
total_amt = total_amt + d['new_balance_in_base_currency'];
|
||||
});
|
||||
if(total_amt !== r.sum) {
|
||||
frm.add_custom_button(__('Journal Entry'), function() {
|
||||
return frm.events.make_jv(frm);
|
||||
}, __('Create'));
|
||||
frappe.call({
|
||||
method: 'check_journal_entry_condition',
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frm.add_custom_button(__('Journal Entry'), function() {
|
||||
return frm.events.make_jv(frm);
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
}, 'Journal Entry');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -27,6 +27,24 @@ class ExchangeRateRevaluation(Document):
|
||||
if not (self.company and self.posting_date):
|
||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_journal_entry_condition(self):
|
||||
total_debit = frappe.db.get_value("Journal Entry Account", {
|
||||
'reference_type': 'Exchange Rate Revaluation',
|
||||
'reference_name': self.name,
|
||||
'docstatus': 1
|
||||
}, "sum(debit) as sum")
|
||||
|
||||
total_amt = 0
|
||||
for d in self.accounts:
|
||||
total_amt = total_amt + d.new_balance_in_base_currency
|
||||
|
||||
if total_amt != total_debit:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_accounts_data(self, account=None):
|
||||
accounts = []
|
||||
self.validate_mandatory()
|
||||
@@ -95,6 +113,7 @@ class ExchangeRateRevaluation(Document):
|
||||
message = _("No outstanding invoices found")
|
||||
frappe.msgprint(message)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_jv_entry(self):
|
||||
if self.total_gain_loss == 0:
|
||||
return
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.model.document import Document
|
||||
class FiscalYearIncorrectDate(frappe.ValidationError): pass
|
||||
|
||||
class FiscalYear(Document):
|
||||
@frappe.whitelist()
|
||||
def set_as_default(self):
|
||||
frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name)
|
||||
global_defaults = frappe.get_doc("Global Defaults")
|
||||
@@ -54,7 +55,7 @@ class FiscalYear(Document):
|
||||
def on_update(self):
|
||||
check_duplicate_fiscal_year(self)
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
|
||||
def on_trash(self):
|
||||
global_defaults = frappe.get_doc("Global Defaults")
|
||||
if global_defaults.current_fiscal_year == self.name:
|
||||
|
||||
@@ -290,4 +290,8 @@ def rename_temporarily_named_docs(doctype):
|
||||
oldname = doc.name
|
||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql("""UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s""".format(doctype), (newname, oldname))
|
||||
frappe.db.sql(
|
||||
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
|
||||
(newname, oldname),
|
||||
auto_commit=True
|
||||
)
|
||||
|
||||
@@ -54,4 +54,4 @@ class TestGLEntry(unittest.TestCase):
|
||||
self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries)))
|
||||
|
||||
new_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0]
|
||||
self.assertEquals(old_naming_series_current_value + 2, new_naming_series_current_value)
|
||||
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
|
||||
|
||||
@@ -125,6 +125,7 @@ class InvoiceDiscounting(AccountsController):
|
||||
|
||||
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No')
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_disbursement_entry(self):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = 'Journal Entry'
|
||||
@@ -174,6 +175,7 @@ class InvoiceDiscounting(AccountsController):
|
||||
|
||||
return je
|
||||
|
||||
@frappe.whitelist()
|
||||
def close_loan(self):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = 'Journal Entry'
|
||||
|
||||
@@ -327,18 +327,16 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
|
||||
},
|
||||
|
||||
setup_balance_formatter: function() {
|
||||
var me = this;
|
||||
$.each(["balance", "party_balance"], function(i, field) {
|
||||
var df = frappe.meta.get_docfield("Journal Entry Account", field, me.frm.doc.name);
|
||||
df.formatter = function(value, df, options, doc) {
|
||||
var currency = frappe.meta.get_field_currency(df, doc);
|
||||
var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
|
||||
return "<div style='text-align: right'>"
|
||||
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
|
||||
+ " " + dr_or_cr
|
||||
+ "</div>";
|
||||
}
|
||||
})
|
||||
const formatter = function(value, df, options, doc) {
|
||||
var currency = frappe.meta.get_field_currency(df, doc);
|
||||
var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
|
||||
return "<div style='text-align: right'>"
|
||||
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
|
||||
+ " " + dr_or_cr
|
||||
+ "</div>";
|
||||
};
|
||||
this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter);
|
||||
this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter);
|
||||
},
|
||||
|
||||
reference_name: function(doc, cdt, cdn) {
|
||||
@@ -431,15 +429,6 @@ cur_frm.cscript.validate = function(doc,cdt,cdn) {
|
||||
cur_frm.cscript.update_totals(doc);
|
||||
}
|
||||
|
||||
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
|
||||
if(doc.select_print_heading){
|
||||
// print heading
|
||||
cur_frm.pformat.print_heading = doc.select_print_heading;
|
||||
}
|
||||
else
|
||||
cur_frm.pformat.print_heading = __("Journal Entry");
|
||||
}
|
||||
|
||||
frappe.ui.form.on("Journal Entry Account", {
|
||||
party: function(frm, cdt, cdn) {
|
||||
var d = frappe.get_doc(cdt, cdn);
|
||||
@@ -511,8 +500,11 @@ $.extend(erpnext.journal_entry, {
|
||||
};
|
||||
|
||||
$.each(field_label_map, function (fieldname, label) {
|
||||
var df = frappe.meta.get_docfield("Journal Entry Account", fieldname, frm.doc.name);
|
||||
df.label = frm.doc.multi_currency ? (label + " in Account Currency") : label;
|
||||
frm.fields_dict.accounts.grid.update_docfield_property(
|
||||
fieldname,
|
||||
'label',
|
||||
frm.doc.multi_currency ? (label + " in Account Currency") : label
|
||||
);
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -564,6 +564,7 @@ class JournalEntry(AccountsController):
|
||||
if gl_map:
|
||||
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance(self):
|
||||
if not self.get('accounts'):
|
||||
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
|
||||
@@ -591,6 +592,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_outstanding_invoices(self):
|
||||
self.set('accounts', [])
|
||||
total = 0
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import (flt, add_months)
|
||||
from frappe.model.document import Document
|
||||
|
||||
class MonthlyDistribution(Document):
|
||||
@frappe.whitelist()
|
||||
def get_months(self):
|
||||
month_list = ['January','February','March','April','May','June','July','August','September',
|
||||
'October','November','December']
|
||||
|
||||
@@ -167,6 +167,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
|
||||
return invoice
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_invoices(self):
|
||||
self.validate_company()
|
||||
invoices = self.get_invoices()
|
||||
|
||||
@@ -6,10 +6,12 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
test_dependencies = ["Customer", "Supplier"]
|
||||
from frappe.cache_manager import clear_doctype_cache
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
|
||||
|
||||
test_dependencies = ["Customer", "Supplier"]
|
||||
|
||||
class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||
@@ -24,22 +26,25 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
|
||||
def test_opening_sales_invoice_creation(self):
|
||||
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
|
||||
invoices = self.make_invoices(company="_Test Opening Invoice Company")
|
||||
try:
|
||||
invoices = self.make_invoices(company="_Test Opening Invoice Company")
|
||||
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
|
||||
si = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
si = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
|
||||
# Check if update stock is not enabled
|
||||
self.assertEqual(si.update_stock, 0)
|
||||
# Check if update stock is not enabled
|
||||
self.assertEqual(si.update_stock, 0)
|
||||
|
||||
property_setter.delete()
|
||||
finally:
|
||||
property_setter.delete()
|
||||
clear_doctype_cache("Sales Invoice")
|
||||
|
||||
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
|
||||
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
|
||||
@@ -143,4 +148,4 @@ def make_customer(customer=None):
|
||||
customer.insert(ignore_permissions=True)
|
||||
return customer.name
|
||||
else:
|
||||
return frappe.db.exists("Customer", customer_name)
|
||||
return frappe.db.exists("Customer", customer_name)
|
||||
|
||||
@@ -1,87 +1,39 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2014-08-29 16:02:39.740505",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2014-08-29 16:02:39.740505",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"field_order": [
|
||||
"company",
|
||||
"account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-07-11 03:28:03.348246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Party Account",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-07 18:13:08.833822",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Party Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
@@ -561,7 +561,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate));
|
||||
|
||||
if(frm.doc.payment_type == "Pay")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount);
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
|
||||
else
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
|
||||
@@ -582,7 +582,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
if(frm.doc.payment_type == "Receive")
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount);
|
||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
|
||||
else
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
@@ -606,9 +606,9 @@ frappe.ui.form.on('Payment Entry', {
|
||||
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
|
||||
{fieldtype:"Section Break"},
|
||||
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {"company": frm.doc.company}
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {"company": frm.doc.company}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -743,7 +743,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
});
|
||||
},
|
||||
|
||||
allocate_party_amount_against_ref_docs: function(frm, paid_amount) {
|
||||
allocate_party_amount_against_ref_docs: function(frm, paid_amount, paid_amount_change) {
|
||||
var total_positive_outstanding_including_order = 0;
|
||||
var total_negative_outstanding = 0;
|
||||
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
|
||||
@@ -800,22 +800,15 @@ frappe.ui.form.on('Payment Entry', {
|
||||
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
|
||||
row.allocated_amount = 0;
|
||||
|
||||
} else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
|
||||
if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
|
||||
if (row.outstanding_amount >= allocated_positive_outstanding) {
|
||||
row.allocated_amount = allocated_positive_outstanding;
|
||||
} else {
|
||||
row.allocated_amount = row.outstanding_amount;
|
||||
}
|
||||
|
||||
} else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) {
|
||||
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
|
||||
row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ?
|
||||
allocated_positive_outstanding : row.outstanding_amount;
|
||||
allocated_positive_outstanding -= flt(row.allocated_amount);
|
||||
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
|
||||
if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
|
||||
row.allocated_amount = -1*allocated_negative_outstanding;
|
||||
} else {
|
||||
row.allocated_amount = row.outstanding_amount;
|
||||
};
|
||||
|
||||
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
|
||||
row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ?
|
||||
-1*allocated_negative_outstanding : row.outstanding_amount;
|
||||
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ class TestPaymentOrder(unittest.TestCase):
|
||||
|
||||
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
|
||||
reference_doc = doc.get("references")[0]
|
||||
self.assertEquals(reference_doc.reference_name, payment_entry.name)
|
||||
self.assertEquals(reference_doc.reference_doctype, "Payment Entry")
|
||||
self.assertEquals(reference_doc.supplier, "_Test Supplier")
|
||||
self.assertEquals(reference_doc.amount, 250)
|
||||
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
||||
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
||||
self.assertEqual(reference_doc.supplier, "_Test Supplier")
|
||||
self.assertEqual(reference_doc.amount, 250)
|
||||
|
||||
def create_payment_order_against_payment_entry(ref_doc, order_type):
|
||||
payment_order = frappe.get_doc(dict(
|
||||
|
||||
@@ -234,8 +234,9 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
});
|
||||
|
||||
if (invoices) {
|
||||
frappe.meta.get_docfield("Payment Reconciliation Payment", "invoice_number",
|
||||
me.frm.doc.name).options = "\n" + invoices.join("\n");
|
||||
this.frm.fields_dict.payments.grid.update_docfield_property(
|
||||
'invoice_number', 'options', "\n" + invoices.join("\n")
|
||||
);
|
||||
|
||||
$.each(me.frm.doc.payments || [], function(i, p) {
|
||||
if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null;
|
||||
|
||||
@@ -11,6 +11,7 @@ from erpnext.accounts.utils import (get_outstanding_invoices,
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
|
||||
class PaymentReconciliation(Document):
|
||||
@frappe.whitelist()
|
||||
def get_unreconciled_entries(self):
|
||||
self.get_nonreconciled_payment_entries()
|
||||
self.get_invoice_entries()
|
||||
@@ -113,7 +114,7 @@ class PaymentReconciliation(Document):
|
||||
'party_type': self.party_type,
|
||||
'voucher_type': voucher_type,
|
||||
'account': self.receivable_payable_account
|
||||
}, as_dict=1, debug=1)
|
||||
}, as_dict=1)
|
||||
|
||||
def add_payment_entries(self, entries):
|
||||
self.set('payments', [])
|
||||
@@ -147,6 +148,7 @@ class PaymentReconciliation(Document):
|
||||
ent.currency = e.get('currency')
|
||||
ent.outstanding_amount = e.get('outstanding_amount')
|
||||
|
||||
@frappe.whitelist()
|
||||
def reconcile(self, args):
|
||||
for e in self.get('payments'):
|
||||
e.invoice_type = None
|
||||
@@ -197,6 +199,7 @@ class PaymentReconciliation(Document):
|
||||
'difference_account': row.difference_account
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_difference_amount(self, child_row):
|
||||
if child_row.get("reference_type") != 'Payment Entry': return
|
||||
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
"discount",
|
||||
"section_break_9",
|
||||
"payment_amount",
|
||||
"outstanding",
|
||||
"paid_amount",
|
||||
"discounted_amount",
|
||||
"column_break_3",
|
||||
"outstanding",
|
||||
"paid_amount"
|
||||
"base_payment_amount"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -78,7 +79,8 @@
|
||||
"depends_on": "paid_amount",
|
||||
"fieldname": "paid_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Paid Amount"
|
||||
"label": "Paid Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
@@ -97,6 +99,7 @@
|
||||
"fieldname": "outstanding",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Outstanding",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -145,12 +148,18 @@
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_payment_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Payment Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-15 21:03:12.540546",
|
||||
"modified": "2021-04-28 05:41:35.084233",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
|
||||
@@ -22,7 +22,43 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
});
|
||||
|
||||
if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime());
|
||||
if (frm.doc.docstatus === 1) set_html_data(frm);
|
||||
|
||||
frappe.realtime.on('closing_process_complete', async function(data) {
|
||||
await frm.reload_doc();
|
||||
if (frm.doc.status == 'Failed' && frm.doc.error_message && data.user == frappe.session.user) {
|
||||
frappe.msgprint({
|
||||
title: __('POS Closing Failed'),
|
||||
message: frm.doc.error_message,
|
||||
indicator: 'orange',
|
||||
clear: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
set_html_data(frm);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.status == 'Failed') {
|
||||
const issue = '<a id="jump_to_error" style="text-decoration: underline;">issue</a>';
|
||||
frm.dashboard.set_headline(
|
||||
__('POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.', [issue]));
|
||||
|
||||
$('#jump_to_error').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
frappe.utils.scroll_to(
|
||||
cur_frm.get_field("error_message").$wrapper,
|
||||
true,
|
||||
30
|
||||
);
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Retry'), function () {
|
||||
frm.call('retry', {}, () => {
|
||||
frm.reload_doc();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
pos_opening_entry(frm) {
|
||||
@@ -61,48 +97,37 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
before_save: function(frm) {
|
||||
frm.set_value("grand_total", 0);
|
||||
frm.set_value("net_total", 0);
|
||||
frm.set_value("total_quantity", 0);
|
||||
frm.set_value("taxes", []);
|
||||
|
||||
for (let row of frm.doc.payment_reconciliation) {
|
||||
row.expected_amount = 0;
|
||||
}
|
||||
|
||||
for (let row of frm.doc.pos_transactions) {
|
||||
frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) {
|
||||
const removed_row = locals[cdt][cdn];
|
||||
|
||||
if (!removed_row.pos_invoice) return;
|
||||
|
||||
frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => {
|
||||
cur_frm.doc.grand_total -= flt(doc.grand_total);
|
||||
cur_frm.doc.net_total -= flt(doc.net_total);
|
||||
cur_frm.doc.total_quantity -= flt(doc.total_qty);
|
||||
refresh_payments(doc, cur_frm, 1);
|
||||
refresh_taxes(doc, cur_frm, 1);
|
||||
refresh_fields(cur_frm);
|
||||
set_html_data(cur_frm);
|
||||
});
|
||||
}
|
||||
|
||||
frappe.ui.form.on('POS Invoice Reference', {
|
||||
pos_invoice(frm, cdt, cdn) {
|
||||
const added_row = locals[cdt][cdn];
|
||||
|
||||
if (!added_row.pos_invoice) return;
|
||||
|
||||
frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
frappe.ui.form.on('POS Closing Entry Detail', {
|
||||
closing_amount: (frm, cdt, cdn) => {
|
||||
const row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount))
|
||||
frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount));
|
||||
}
|
||||
})
|
||||
|
||||
@@ -126,28 +151,28 @@ function add_to_pos_transaction(d, frm) {
|
||||
})
|
||||
}
|
||||
|
||||
function refresh_payments(d, frm, remove) {
|
||||
function refresh_payments(d, frm) {
|
||||
d.payments.forEach(p => {
|
||||
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
|
||||
if (payment) {
|
||||
if (!remove) payment.expected_amount += flt(p.amount);
|
||||
else payment.expected_amount -= flt(p.amount);
|
||||
payment.expected_amount += flt(p.amount);
|
||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||
} else {
|
||||
frm.add_child("payment_reconciliation", {
|
||||
mode_of_payment: p.mode_of_payment,
|
||||
opening_amount: 0,
|
||||
expected_amount: p.amount
|
||||
expected_amount: p.amount,
|
||||
closing_amount: 0
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function refresh_taxes(d, frm, remove) {
|
||||
function refresh_taxes(d, frm) {
|
||||
d.taxes.forEach(t => {
|
||||
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
|
||||
if (tax) {
|
||||
if (!remove) tax.amount += flt(t.tax_amount);
|
||||
else tax.amount -= flt(t.tax_amount);
|
||||
tax.amount += flt(t.tax_amount);
|
||||
} else {
|
||||
frm.add_child("taxes", {
|
||||
account_head: t.account_head,
|
||||
@@ -177,11 +202,13 @@ function refresh_fields(frm) {
|
||||
}
|
||||
|
||||
function set_html_data(frm) {
|
||||
frappe.call({
|
||||
method: "get_payment_reconciliation_details",
|
||||
doc: frm.doc,
|
||||
callback: (r) => {
|
||||
frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
|
||||
}
|
||||
})
|
||||
if (frm.doc.docstatus === 1 && frm.doc.status == 'Submitted') {
|
||||
frappe.call({
|
||||
method: "get_payment_reconciliation_details",
|
||||
doc: frm.doc,
|
||||
callback: (r) => {
|
||||
frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
"total_quantity",
|
||||
"column_break_16",
|
||||
"taxes",
|
||||
"failure_description_section",
|
||||
"error_message",
|
||||
"section_break_14",
|
||||
"amended_from"
|
||||
],
|
||||
@@ -195,7 +197,7 @@
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Status",
|
||||
"options": "Draft\nSubmitted\nQueued\nCancelled",
|
||||
"options": "Draft\nSubmitted\nQueued\nFailed\nCancelled",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -203,6 +205,21 @@
|
||||
"fieldname": "period_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Period Details"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "error_message",
|
||||
"depends_on": "error_message",
|
||||
"fieldname": "failure_description_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Failure Description"
|
||||
},
|
||||
{
|
||||
"depends_on": "error_message",
|
||||
"fieldname": "error_message",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Error",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
@@ -212,7 +229,7 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2021-02-01 13:47:20.722104",
|
||||
"modified": "2021-05-05 16:59:49.723261",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
|
||||
@@ -16,33 +16,13 @@ class POSClosingEntry(StatusUpdater):
|
||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||
|
||||
self.validate_pos_closing()
|
||||
self.validate_pos_invoices()
|
||||
|
||||
def validate_pos_closing(self):
|
||||
user = frappe.db.sql("""
|
||||
SELECT name FROM `tabPOS Closing Entry`
|
||||
WHERE
|
||||
user = %(user)s AND docstatus = 1 AND pos_profile = %(profile)s AND
|
||||
(period_start_date between %(start)s and %(end)s OR period_end_date between %(start)s and %(end)s)
|
||||
""", {
|
||||
'user': self.user,
|
||||
'profile': self.pos_profile,
|
||||
'start': self.period_start_date,
|
||||
'end': self.period_end_date
|
||||
})
|
||||
|
||||
if user:
|
||||
bold_already_exists = frappe.bold(_("already exists"))
|
||||
bold_user = frappe.bold(self.user)
|
||||
frappe.throw(_("POS Closing Entry {} against {} between selected period")
|
||||
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
|
||||
|
||||
def validate_pos_invoices(self):
|
||||
invalid_rows = []
|
||||
for d in self.pos_transactions:
|
||||
invalid_row = {'idx': d.idx}
|
||||
pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
|
||||
pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
|
||||
["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
|
||||
if pos_invoice.consolidated_invoice:
|
||||
invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
|
||||
@@ -68,17 +48,22 @@ class POSClosingEntry(StatusUpdater):
|
||||
|
||||
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_reconciliation_details(self):
|
||||
currency = frappe.get_cached_value('Company', self.company, "default_currency")
|
||||
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
|
||||
{"data": self, "currency": currency})
|
||||
|
||||
|
||||
def on_submit(self):
|
||||
consolidate_pos_invoices(closing_entry=self)
|
||||
|
||||
|
||||
def on_cancel(self):
|
||||
unconsolidate_pos_invoices(closing_entry=self)
|
||||
|
||||
@frappe.whitelist()
|
||||
def retry(self):
|
||||
consolidate_pos_invoices(closing_entry=self)
|
||||
|
||||
def update_opening_entry(self, for_cancel=False):
|
||||
opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
|
||||
opening_entry.pos_closing_entry = self.name if not for_cancel else None
|
||||
@@ -88,8 +73,8 @@ class POSClosingEntry(StatusUpdater):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
|
||||
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'])
|
||||
return [c['user'] for c in cashiers_list]
|
||||
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'], as_list=1)
|
||||
return [c for c in cashiers_list]
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_invoices(start, end, pos_profile, user):
|
||||
|
||||
@@ -8,6 +8,7 @@ frappe.listview_settings['POS Closing Entry'] = {
|
||||
"Draft": "red",
|
||||
"Submitted": "blue",
|
||||
"Queued": "orange",
|
||||
"Failed": "red",
|
||||
"Cancelled": "red"
|
||||
|
||||
};
|
||||
|
||||
@@ -5,12 +5,21 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.utils import nowdate
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening
|
||||
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
|
||||
class TestPOSClosingEntry(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Make stock available for POS Sales
|
||||
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def test_pos_closing_entry(self):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
@@ -41,9 +50,6 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
self.assertEqual(pcv_doc.total_quantity, 2)
|
||||
self.assertEqual(pcv_doc.net_total, 6700)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def test_cancelling_of_pos_closing_entry(self):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
@@ -84,8 +90,6 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
self.assertEqual(si_doc.docstatus, 2)
|
||||
self.assertEqual(pos_inv1.status, 'Paid')
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def init_user_and_profile(**args):
|
||||
user = 'test@example.com'
|
||||
@@ -103,4 +107,4 @@ def init_user_and_profile(**args):
|
||||
|
||||
pos_profile.save()
|
||||
|
||||
return test_user, pos_profile
|
||||
return test_user, pos_profile
|
||||
|
||||
@@ -96,30 +96,45 @@ class POSInvoice(SalesInvoice):
|
||||
if paid_amt and pay.amount != paid_amt:
|
||||
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
|
||||
|
||||
def validate_pos_reserved_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
filters = {"item_code": item.item_code, "warehouse": item.warehouse}
|
||||
if item.batch_no:
|
||||
filters["batch_no"] = item.batch_no
|
||||
|
||||
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
|
||||
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
|
||||
|
||||
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
|
||||
if len(invalid_serial_nos) == 1:
|
||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
elif invalid_serial_nos:
|
||||
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
|
||||
def validate_delivered_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
||||
'item_code': item.item_code,
|
||||
'name': ['in', serial_nos],
|
||||
'sales_invoice': ['is', 'set']
|
||||
}, pluck='name')
|
||||
|
||||
if delivered_serial_nos:
|
||||
bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
|
||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
||||
error_msg = []
|
||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||
for d in self.get('items'):
|
||||
msg = ""
|
||||
if d.serial_no:
|
||||
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
|
||||
if d.batch_no:
|
||||
filters["batch_no"] = d.batch_no
|
||||
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
|
||||
|
||||
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
|
||||
if len(invalid_serial_nos) == 1:
|
||||
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(d.idx, bold_invalid_serial_nos))
|
||||
elif invalid_serial_nos:
|
||||
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(d.idx, bold_invalid_serial_nos))
|
||||
|
||||
self.validate_pos_reserved_serial_nos(d)
|
||||
self.validate_delivered_serial_nos(d)
|
||||
else:
|
||||
if allow_negative_stock:
|
||||
return
|
||||
@@ -127,15 +142,11 @@ class POSInvoice(SalesInvoice):
|
||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
||||
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||
if flt(available_stock) <= 0:
|
||||
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
|
||||
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
|
||||
.format(d.idx, item_code, warehouse), title=_("Item Unavailable"))
|
||||
elif flt(available_stock) < flt(d.qty):
|
||||
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
|
||||
.format(d.idx, item_code, warehouse, qty))
|
||||
if msg:
|
||||
error_msg.append(msg)
|
||||
|
||||
if error_msg:
|
||||
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
|
||||
frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
|
||||
.format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable"))
|
||||
|
||||
def validate_serialised_or_batched_item(self):
|
||||
error_msg = []
|
||||
@@ -202,9 +213,8 @@ class POSInvoice(SalesInvoice):
|
||||
for d in self.get("items"):
|
||||
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
|
||||
if not is_stock_item:
|
||||
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format(
|
||||
d.idx, frappe.bold(d.item_code)
|
||||
), title=_("Invalid Item"))
|
||||
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ")
|
||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||
|
||||
def validate_mode_of_payment(self):
|
||||
if len(self.payments) == 0:
|
||||
@@ -220,7 +230,7 @@ class POSInvoice(SalesInvoice):
|
||||
base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total)
|
||||
if not flt(self.change_amount) and grand_total < flt(self.paid_amount):
|
||||
self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount))
|
||||
self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount))
|
||||
self.base_change_amount = flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount)
|
||||
|
||||
if flt(self.change_amount) and not self.account_for_change_amount:
|
||||
frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
|
||||
@@ -354,6 +364,7 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
return profile
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_missing_values(self, for_validate=False):
|
||||
profile = self.set_pos_fields(for_validate)
|
||||
|
||||
@@ -376,12 +387,20 @@ class POSInvoice(SalesInvoice):
|
||||
"allow_print_before_pay": profile.get("allow_print_before_pay")
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def reset_mode_of_payments(self):
|
||||
if self.pos_profile:
|
||||
pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile)
|
||||
update_multi_mode_option(self, pos_profile)
|
||||
self.paid_amount = 0
|
||||
|
||||
def set_account_for_mode_of_payment(self):
|
||||
self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
|
||||
for pay in self.payments:
|
||||
if not pay.account:
|
||||
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_payment_request(self):
|
||||
for pay in self.payments:
|
||||
if pay.type == "Phone":
|
||||
@@ -442,7 +461,17 @@ def get_stock_availability(item_code, warehouse):
|
||||
order by posting_date desc, posting_time desc
|
||||
limit 1""", (item_code, warehouse), as_dict=1)
|
||||
|
||||
pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
|
||||
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
|
||||
|
||||
if sle_qty and pos_sales_qty:
|
||||
return sle_qty - pos_sales_qty
|
||||
else:
|
||||
return sle_qty
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
|
||||
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
||||
where p.name = p_item.parent
|
||||
and p.consolidated_invoice is NULL
|
||||
@@ -451,14 +480,8 @@ def get_stock_availability(item_code, warehouse):
|
||||
and p_item.item_code = %s
|
||||
and p_item.warehouse = %s
|
||||
""", (item_code, warehouse), as_dict=1)
|
||||
|
||||
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
|
||||
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
|
||||
|
||||
if sle_qty and pos_sales_qty:
|
||||
return sle_qty - pos_sales_qty
|
||||
else:
|
||||
return sle_qty
|
||||
|
||||
return reserved_qty[0].qty or 0 if reserved_qty else 0
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_return(source_name, target_doc=None):
|
||||
|
||||
@@ -9,8 +9,22 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
class TestPOSInvoice(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def tearDown(self):
|
||||
if frappe.session.user != "Administrator":
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
if frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
|
||||
frappe.db.set_value("Selling Settings", None, "validate_selling_price", 0)
|
||||
|
||||
def test_timestamp_change(self):
|
||||
w = create_pos_invoice(do_not_save=1)
|
||||
w.docstatus = 0
|
||||
@@ -308,6 +322,34 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos2.insert)
|
||||
|
||||
def test_delivered_serialized_item_transaction(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
se = make_serialized_item(company='_Test Company',
|
||||
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
|
||||
|
||||
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||
|
||||
si = create_sales_invoice(company='_Test Company', debit_to='Debtors - _TC',
|
||||
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
|
||||
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
|
||||
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||
|
||||
si.get("items")[0].serial_no = serial_nos[0]
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
|
||||
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
|
||||
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
|
||||
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||
|
||||
pos2.get("items")[0].serial_no = serial_nos[0]
|
||||
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos2.insert)
|
||||
|
||||
def test_loyalty_points(self):
|
||||
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
|
||||
@@ -370,7 +412,6 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv.load_from_db()
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 3470)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
@@ -412,7 +453,6 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv.load_from_db()
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 840)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_merging_with_validate_selling_price(self):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
@@ -421,10 +461,12 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
|
||||
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
|
||||
|
||||
make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=1, rate=300)
|
||||
item = "Test Selling Price Validation"
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
|
||||
pos_inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
|
||||
})
|
||||
@@ -438,7 +480,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
})
|
||||
self.assertRaises(frappe.ValidationError, pos_inv.submit)
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=400, do_not_submit=1)
|
||||
pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1)
|
||||
pos_inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400
|
||||
})
|
||||
@@ -457,8 +499,6 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
pos_inv2.load_from_db()
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 400)
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 0)
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
args = frappe._dict(args)
|
||||
@@ -508,4 +548,4 @@ def create_pos_invoice(**args):
|
||||
else:
|
||||
pos_inv.payment_schedule = []
|
||||
|
||||
return pos_inv
|
||||
return pos_inv
|
||||
|
||||
@@ -12,8 +12,8 @@ from frappe.utils.background_jobs import enqueue
|
||||
from frappe.model.mapper import map_doc, map_child_doc
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
|
||||
from six import iteritems
|
||||
import json
|
||||
import six
|
||||
|
||||
class POSInvoiceMergeLog(Document):
|
||||
def validate(self):
|
||||
@@ -78,8 +78,11 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
|
||||
|
||||
sales_invoice.is_consolidated = 1
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.save()
|
||||
sales_invoice.submit()
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice.name
|
||||
@@ -91,10 +94,13 @@ class POSInvoiceMergeLog(Document):
|
||||
credit_note = self.merge_pos_invoice_into(credit_note, data)
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
|
||||
return credit_note.name
|
||||
@@ -131,12 +137,14 @@ class POSInvoiceMergeLog(Document):
|
||||
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
||||
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
||||
update_item_wise_tax_detail(t, tax)
|
||||
found = True
|
||||
if not found:
|
||||
tax.charge_type = 'Actual'
|
||||
tax.included_in_print_rate = 0
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
||||
tax.item_wise_tax_detail = tax.item_wise_tax_detail
|
||||
taxes.append(tax)
|
||||
|
||||
for payment in doc.get('payments'):
|
||||
@@ -168,11 +176,9 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice = frappe.new_doc('Sales Invoice')
|
||||
sales_invoice.customer = self.customer
|
||||
sales_invoice.is_pos = 1
|
||||
# date can be pos closing date?
|
||||
sales_invoice.posting_date = getdate(nowdate())
|
||||
|
||||
return sales_invoice
|
||||
|
||||
|
||||
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
|
||||
for doc in invoice_docs:
|
||||
doc.load_from_db()
|
||||
@@ -187,6 +193,26 @@ class POSInvoiceMergeLog(Document):
|
||||
si.flags.ignore_validate = True
|
||||
si.cancel()
|
||||
|
||||
def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
|
||||
consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
|
||||
tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
|
||||
|
||||
if not consolidated_tax_detail:
|
||||
consolidated_tax_detail = {}
|
||||
|
||||
for item_code, tax_data in tax_row_detail.items():
|
||||
if consolidated_tax_detail.get(item_code):
|
||||
consolidated_tax_data = consolidated_tax_detail.get(item_code)
|
||||
consolidated_tax_detail.update({
|
||||
item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]
|
||||
})
|
||||
else:
|
||||
consolidated_tax_detail.update({
|
||||
item_code: [tax_data[0], tax_data[1]]
|
||||
})
|
||||
|
||||
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':'))
|
||||
|
||||
def get_all_unconsolidated_invoices():
|
||||
filters = {
|
||||
'consolidated_invoice': [ 'in', [ '', None ]],
|
||||
@@ -208,13 +234,13 @@ def get_invoice_customer_map(pos_invoices):
|
||||
|
||||
return pos_invoice_customer_map
|
||||
|
||||
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
|
||||
invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
|
||||
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
|
||||
invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
|
||||
invoice_by_customer = get_invoice_customer_map(invoices)
|
||||
|
||||
if len(invoices) >= 5 and closing_entry:
|
||||
if len(invoices) >= 10 and closing_entry:
|
||||
closing_entry.set_status(update=True, status='Queued')
|
||||
enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
|
||||
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
|
||||
else:
|
||||
create_merge_logs(invoice_by_customer, closing_entry)
|
||||
|
||||
@@ -225,50 +251,83 @@ def unconsolidate_pos_invoices(closing_entry):
|
||||
pluck='name'
|
||||
)
|
||||
|
||||
if len(merge_logs) >= 5:
|
||||
if len(merge_logs) >= 10:
|
||||
closing_entry.set_status(update=True, status='Queued')
|
||||
enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
|
||||
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
|
||||
else:
|
||||
cancel_merge_logs(merge_logs, closing_entry)
|
||||
|
||||
def create_merge_logs(invoice_by_customer, closing_entry={}):
|
||||
for customer, invoices in iteritems(invoice_by_customer):
|
||||
merge_log = frappe.new_doc('POS Invoice Merge Log')
|
||||
merge_log.posting_date = getdate(nowdate())
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get('name', None)
|
||||
def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
try:
|
||||
for customer, invoices in six.iteritems(invoice_by_customer):
|
||||
merge_log = frappe.new_doc('POS Invoice Merge Log')
|
||||
merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
|
||||
|
||||
merge_log.set('pos_invoices', invoices)
|
||||
merge_log.save(ignore_permissions=True)
|
||||
merge_log.submit()
|
||||
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status='Submitted')
|
||||
closing_entry.update_opening_entry()
|
||||
merge_log.set('pos_invoices', invoices)
|
||||
merge_log.save(ignore_permissions=True)
|
||||
merge_log.submit()
|
||||
|
||||
def cancel_merge_logs(merge_logs, closing_entry={}):
|
||||
for log in merge_logs:
|
||||
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
|
||||
merge_log.flags.ignore_permissions = True
|
||||
merge_log.cancel()
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status='Submitted')
|
||||
closing_entry.db_set('error_message', '')
|
||||
closing_entry.update_opening_entry()
|
||||
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status='Cancelled')
|
||||
closing_entry.update_opening_entry(for_cancel=True)
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
message_log = frappe.message_log.pop()
|
||||
error_message = safe_load_json(message_log)
|
||||
|
||||
def enqueue_job(job, invoice_by_customer, closing_entry):
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status='Failed')
|
||||
closing_entry.db_set('error_message', error_message)
|
||||
raise
|
||||
|
||||
finally:
|
||||
frappe.db.commit()
|
||||
frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user})
|
||||
|
||||
def cancel_merge_logs(merge_logs, closing_entry=None):
|
||||
try:
|
||||
for log in merge_logs:
|
||||
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
|
||||
merge_log.flags.ignore_permissions = True
|
||||
merge_log.cancel()
|
||||
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status='Cancelled')
|
||||
closing_entry.db_set('error_message', '')
|
||||
closing_entry.update_opening_entry(for_cancel=True)
|
||||
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
message_log = frappe.message_log.pop()
|
||||
error_message = safe_load_json(message_log)
|
||||
|
||||
if closing_entry:
|
||||
closing_entry.set_status(update=True, status='Submitted')
|
||||
closing_entry.db_set('error_message', error_message)
|
||||
raise
|
||||
|
||||
finally:
|
||||
frappe.db.commit()
|
||||
frappe.publish_realtime('closing_process_complete', {'user': frappe.session.user})
|
||||
|
||||
def enqueue_job(job, **kwargs):
|
||||
check_scheduler_status()
|
||||
|
||||
closing_entry = kwargs.get('closing_entry') or {}
|
||||
|
||||
job_name = closing_entry.get("name")
|
||||
if not job_already_enqueued(job_name):
|
||||
enqueue(
|
||||
job,
|
||||
**kwargs,
|
||||
queue="long",
|
||||
timeout=10000,
|
||||
event="processing_merge_logs",
|
||||
job_name=job_name,
|
||||
closing_entry=closing_entry,
|
||||
invoice_by_customer=invoice_by_customer,
|
||||
now=frappe.conf.developer_mode or frappe.flags.in_test
|
||||
)
|
||||
|
||||
@@ -286,4 +345,14 @@ def check_scheduler_status():
|
||||
def job_already_enqueued(job_name):
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
if job_name in enqueued_jobs:
|
||||
return True
|
||||
return True
|
||||
|
||||
def safe_load_json(message):
|
||||
JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
|
||||
|
||||
try:
|
||||
json_message = json.loads(message).get('message')
|
||||
except JSONDecodeError:
|
||||
json_message = message
|
||||
|
||||
return json_message
|
||||
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
import json
|
||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
|
||||
@@ -14,85 +15,136 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
def test_consolidated_invoice_creation(self):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
try:
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
|
||||
})
|
||||
pos_inv.submit()
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
|
||||
})
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
|
||||
})
|
||||
pos_inv2.submit()
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
|
||||
})
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
|
||||
})
|
||||
pos_inv3.submit()
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
|
||||
})
|
||||
pos_inv3.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
|
||||
|
||||
pos_inv3.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
pos_inv3.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
|
||||
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
def test_consolidated_credit_note_creation(self):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
try:
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
|
||||
})
|
||||
pos_inv.submit()
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
pos_inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
|
||||
})
|
||||
pos_inv.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
|
||||
})
|
||||
pos_inv2.submit()
|
||||
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
|
||||
pos_inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
|
||||
})
|
||||
pos_inv2.submit()
|
||||
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
|
||||
})
|
||||
pos_inv3.submit()
|
||||
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
|
||||
pos_inv3.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
|
||||
})
|
||||
pos_inv3.submit()
|
||||
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
pos_inv_cn.set("payments", [])
|
||||
pos_inv_cn.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
|
||||
})
|
||||
pos_inv_cn.paid_amount = -300
|
||||
pos_inv_cn.submit()
|
||||
pos_inv_cn = make_sales_return(pos_inv.name)
|
||||
pos_inv_cn.set("payments", [])
|
||||
pos_inv_cn.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
|
||||
})
|
||||
pos_inv_cn.paid_amount = -300
|
||||
pos_inv_cn.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
consolidate_pos_invoices()
|
||||
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
|
||||
pos_inv.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
|
||||
|
||||
pos_inv3.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
pos_inv3.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
|
||||
|
||||
pos_inv_cn.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
|
||||
self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
|
||||
pos_inv_cn.load_from_db()
|
||||
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
|
||||
self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
def test_consolidated_invoice_item_taxes(self):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
inv = create_pos_invoice(qty=1, rate=100, do_not_save=True)
|
||||
|
||||
inv.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 9
|
||||
})
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
|
||||
inv2.get('items')[0].item_code = '_Test Item 2'
|
||||
inv2.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 5
|
||||
})
|
||||
inv2.insert()
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
inv.load_from_db()
|
||||
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail)
|
||||
|
||||
tax_rate, amount = item_wise_tax_detail.get('_Test Item')
|
||||
self.assertEqual(tax_rate, 9)
|
||||
self.assertEqual(amount, 9)
|
||||
|
||||
tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2')
|
||||
self.assertEqual(tax_rate2, 5)
|
||||
self.assertEqual(amount2, 5)
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-04-19 14:56:06.652327",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"field",
|
||||
"fieldname"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Fieldname"
|
||||
},
|
||||
{
|
||||
"fieldname": "field",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Field"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-21 11:12:54.632093",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Search Fields",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class POSSearchFields(Document):
|
||||
pass
|
||||
@@ -1,9 +1,17 @@
|
||||
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
let search_fields_datatypes = ['Data', 'Link', 'Dynamic Link', 'Long Text', 'Select', 'Small Text', 'Text', 'Text Editor'];
|
||||
let do_not_include_fields = ["naming_series", "item_code", "item_name", "stock_uom", "hub_sync_id", "asset_naming_series",
|
||||
"default_material_request_type", "valuation_method", "warranty_period", "weight_uom", "batch_number_series",
|
||||
"serial_no_series", "purchase_uom", "customs_tariff_number", "sales_uom", "deferred_revenue_account",
|
||||
"deferred_expense_account", "quality_inspection_template", "route", "slideshow", "website_image_alt", "thumbnail",
|
||||
"web_long_description", "hub_sync_id"]
|
||||
|
||||
frappe.ui.form.on('POS Settings', {
|
||||
onload: function(frm) {
|
||||
frm.trigger("get_invoice_fields");
|
||||
frm.trigger("add_search_options");
|
||||
},
|
||||
|
||||
get_invoice_fields: function(frm) {
|
||||
@@ -16,8 +24,43 @@ frappe.ui.form.on('POS Settings', {
|
||||
}
|
||||
});
|
||||
|
||||
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields);
|
||||
frm.fields_dict.invoice_fields.grid.update_docfield_property(
|
||||
'fieldname', 'options', [""].concat(fields)
|
||||
);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
add_search_options: function(frm) {
|
||||
frappe.model.with_doctype("Item", () => {
|
||||
var fields = $.map(frappe.get_doc("DocType", "Item").fields, function(d) {
|
||||
if (search_fields_datatypes.includes(d.fieldtype) && !(do_not_include_fields.includes(d.fieldname))) {
|
||||
return [d.label];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
fields.unshift('');
|
||||
frm.fields_dict.pos_search_fields.grid.update_docfield_property('field', 'options', fields);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Search Fields", {
|
||||
field: function(frm, doctype, name) {
|
||||
var doc = frappe.get_doc(doctype, name);
|
||||
var df = $.map(frappe.get_doc("DocType", "Item").fields, function(d) {
|
||||
if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) {
|
||||
return d;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})[0];
|
||||
|
||||
doc.fieldname = df.fieldname;
|
||||
frm.refresh_field("fields");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"invoice_fields"
|
||||
"invoice_fields",
|
||||
"pos_search_fields"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -13,11 +14,17 @@
|
||||
"fieldtype": "Table",
|
||||
"label": "POS Field",
|
||||
"options": "POS Field"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_search_fields",
|
||||
"fieldtype": "Table",
|
||||
"label": "POS Search Fields",
|
||||
"options": "POS Search Fields"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-01 15:46:41.478928",
|
||||
"modified": "2021-04-19 14:56:24.465218",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Settings",
|
||||
|
||||
@@ -99,7 +99,7 @@ class TestPricingRule(unittest.TestCase):
|
||||
|
||||
args.item_code = "_Test Item 2"
|
||||
details = get_item_details(args)
|
||||
self.assertEquals(details.get("discount_percentage"), 15)
|
||||
self.assertEqual(details.get("discount_percentage"), 15)
|
||||
|
||||
def test_pricing_rule_for_margin(self):
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
@@ -145,8 +145,8 @@ class TestPricingRule(unittest.TestCase):
|
||||
"name": None
|
||||
})
|
||||
details = get_item_details(args)
|
||||
self.assertEquals(details.get("margin_type"), "Percentage")
|
||||
self.assertEquals(details.get("margin_rate_or_amount"), 10)
|
||||
self.assertEqual(details.get("margin_type"), "Percentage")
|
||||
self.assertEqual(details.get("margin_rate_or_amount"), 10)
|
||||
|
||||
def test_mixed_conditions_for_item_group(self):
|
||||
for item in ["Mixed Cond Item 1", "Mixed Cond Item 2"]:
|
||||
@@ -192,7 +192,7 @@ class TestPricingRule(unittest.TestCase):
|
||||
"name": None
|
||||
})
|
||||
details = get_item_details(args)
|
||||
self.assertEquals(details.get("discount_percentage"), 10)
|
||||
self.assertEqual(details.get("discount_percentage"), 10)
|
||||
|
||||
def test_pricing_rule_for_variants(self):
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
@@ -322,11 +322,11 @@ class TestPricingRule(unittest.TestCase):
|
||||
si.insert(ignore_permissions=True)
|
||||
|
||||
item = si.items[0]
|
||||
self.assertEquals(item.margin_rate_or_amount, 10)
|
||||
self.assertEquals(item.rate_with_margin, 1100)
|
||||
self.assertEqual(item.margin_rate_or_amount, 10)
|
||||
self.assertEqual(item.rate_with_margin, 1100)
|
||||
self.assertEqual(item.discount_percentage, 10)
|
||||
self.assertEquals(item.discount_amount, 110)
|
||||
self.assertEquals(item.rate, 990)
|
||||
self.assertEqual(item.discount_amount, 110)
|
||||
self.assertEqual(item.rate, 990)
|
||||
|
||||
def test_pricing_rule_with_margin_and_discount_amount(self):
|
||||
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
|
||||
@@ -338,10 +338,10 @@ class TestPricingRule(unittest.TestCase):
|
||||
si.insert(ignore_permissions=True)
|
||||
|
||||
item = si.items[0]
|
||||
self.assertEquals(item.margin_rate_or_amount, 10)
|
||||
self.assertEquals(item.rate_with_margin, 1100)
|
||||
self.assertEquals(item.discount_amount, 110)
|
||||
self.assertEquals(item.rate, 990)
|
||||
self.assertEqual(item.margin_rate_or_amount, 10)
|
||||
self.assertEqual(item.rate_with_margin, 1100)
|
||||
self.assertEqual(item.discount_amount, 110)
|
||||
self.assertEqual(item.rate, 990)
|
||||
|
||||
def test_pricing_rule_for_product_discount_on_same_item(self):
|
||||
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
|
||||
@@ -458,21 +458,21 @@ class TestPricingRule(unittest.TestCase):
|
||||
si.items[0].price_list_rate = 1000
|
||||
si.submit()
|
||||
item = si.items[0]
|
||||
self.assertEquals(item.rate, 100)
|
||||
self.assertEqual(item.rate, 100)
|
||||
|
||||
# Correct Customer and Incorrect is_return value
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=1, qty=-1)
|
||||
si.items[0].price_list_rate = 1000
|
||||
si.submit()
|
||||
item = si.items[0]
|
||||
self.assertEquals(item.rate, 100)
|
||||
self.assertEqual(item.rate, 100)
|
||||
|
||||
# Correct Customer and correct is_return value
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=0)
|
||||
si.items[0].price_list_rate = 1000
|
||||
si.submit()
|
||||
item = si.items[0]
|
||||
self.assertEquals(item.rate, 900)
|
||||
self.assertEqual(item.rate, 900)
|
||||
|
||||
def test_multiple_pricing_rules(self):
|
||||
make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
|
||||
@@ -545,11 +545,11 @@ class TestPricingRule(unittest.TestCase):
|
||||
apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10)
|
||||
|
||||
si = create_sales_invoice(qty=5, do_not_submit=True)
|
||||
self.assertEquals(len(si.items), 2)
|
||||
self.assertEquals(si.items[1].rate, 10)
|
||||
self.assertEqual(len(si.items), 2)
|
||||
self.assertEqual(si.items[1].rate, 10)
|
||||
|
||||
si1 = create_sales_invoice(qty=2, do_not_submit=True)
|
||||
self.assertEquals(len(si1.items), 1)
|
||||
self.assertEqual(len(si1.items), 1)
|
||||
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
@@ -173,7 +173,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
if parenttype in ["Customer Group", "Item Group", "Territory"]:
|
||||
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
|
||||
root_name = frappe.db.get_list(parenttype,
|
||||
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1)
|
||||
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1, ignore_permissions=True)
|
||||
|
||||
if root_name and root_name[0][0]:
|
||||
parent_groups.append(root_name[0][0])
|
||||
@@ -471,7 +471,7 @@ def apply_pricing_rule_on_transaction(doc):
|
||||
|
||||
if not d.get(pr_field): continue
|
||||
|
||||
if d.validate_applied_rule and doc.get(field) < d.get(pr_field):
|
||||
if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field):
|
||||
frappe.msgprint(_("User has not applied rule on the invoice {0}")
|
||||
.format(doc.name))
|
||||
else:
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in data %}
|
||||
{% for row in data %}
|
||||
<tr>
|
||||
{% if(row.posting_date) %}
|
||||
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
|
||||
@@ -38,30 +38,30 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}</td>
|
||||
{{ frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}</td>
|
||||
{{ frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b></td>
|
||||
<td style="text-align: right">
|
||||
{{ row.account and frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}
|
||||
{{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ row.account and frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}
|
||||
{{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.balance, filters.presentation_currency) }}
|
||||
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br><br>
|
||||
{% if aging %}
|
||||
<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ aging.ageing_based_on }}</h3>
|
||||
{% if ageing %}
|
||||
<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ ageing.ageing_based_on }}</h3>
|
||||
<h5 class="text-center">
|
||||
{{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
|
||||
</h5>
|
||||
@@ -78,10 +78,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ aging.range1 }}</td>
|
||||
<td>{{ aging.range2 }}</td>
|
||||
<td>{{ aging.range3 }}</td>
|
||||
<td>{{ aging.range4 }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa
|
||||
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
|
||||
from frappe.utils.print_format import report_to_pdf
|
||||
from frappe.utils.pdf import get_pdf
|
||||
@@ -29,7 +31,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
validate_template(self.body)
|
||||
|
||||
if not self.customers:
|
||||
frappe.throw(frappe._('Customers not selected.'))
|
||||
frappe.throw(_('Customers not selected.'))
|
||||
|
||||
if self.enable_auto_email:
|
||||
self.to_date = self.start_date
|
||||
@@ -38,7 +40,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = {}
|
||||
aging = ''
|
||||
ageing = ''
|
||||
base_template_path = "frappe/www/printview.html"
|
||||
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
|
||||
|
||||
@@ -54,26 +56,30 @@ def get_report_pdf(doc, consolidated=True):
|
||||
'range4': 120,
|
||||
'customer': entry.customer
|
||||
})
|
||||
col1, aging = get_ageing(ageing_filters)
|
||||
aging[0]['ageing_based_on'] = doc.ageing_based_on
|
||||
col1, ageing = get_ageing(ageing_filters)
|
||||
|
||||
if ageing:
|
||||
ageing[0]['ageing_based_on'] = doc.ageing_based_on
|
||||
|
||||
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
|
||||
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
|
||||
or doc.currency or get_company_currency(doc.company)
|
||||
|
||||
filters= frappe._dict({
|
||||
'from_date': doc.from_date,
|
||||
'to_date': doc.to_date,
|
||||
'company': doc.company,
|
||||
'finance_book': doc.finance_book if doc.finance_book else None,
|
||||
"account": doc.account if doc.account else None,
|
||||
'account': doc.account if doc.account else None,
|
||||
'party_type': 'Customer',
|
||||
'party': [entry.customer],
|
||||
'presentation_currency': presentation_currency,
|
||||
'group_by': doc.group_by,
|
||||
'currency': doc.currency,
|
||||
'cost_center': [cc.cost_center_name for cc in doc.cost_center],
|
||||
'project': [p.project_name for p in doc.project],
|
||||
'show_opening_entries': 0,
|
||||
'include_default_book_entries': 0,
|
||||
'show_cancelled_entries': 1,
|
||||
'tax_id': tax_id if tax_id else None
|
||||
})
|
||||
col, res = get_soa(filters)
|
||||
@@ -83,11 +89,14 @@ def get_report_pdf(doc, consolidated=True):
|
||||
|
||||
if len(res) == 3:
|
||||
continue
|
||||
|
||||
html = frappe.render_template(template_path, \
|
||||
{"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing else None})
|
||||
{"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None})
|
||||
|
||||
html = frappe.render_template(base_template_path, {"body": html, \
|
||||
"css": get_print_style(), "title": "Statement For " + entry.customer})
|
||||
statement_dict[entry.customer] = html
|
||||
|
||||
if not bool(statement_dict):
|
||||
return False
|
||||
elif consolidated:
|
||||
@@ -167,7 +176,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
if customer_collection == 'Sales Person':
|
||||
customers = get_customers_based_on_sales_person(collection_name)
|
||||
if not bool(customers):
|
||||
frappe.throw('No Customers found with selected options.')
|
||||
frappe.throw(_('No Customers found with selected options.'))
|
||||
else:
|
||||
if customer_collection == 'Sales Partner':
|
||||
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
|
||||
@@ -199,14 +208,14 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
|
||||
|
||||
if len(billing_email) == 0 or (billing_email[0][0] is None):
|
||||
if billing_and_primary:
|
||||
frappe.throw('No billing email found for customer: '+ customer_name)
|
||||
frappe.throw(_("No billing email found for customer: {0}").format(customer_name))
|
||||
else:
|
||||
return ''
|
||||
|
||||
if billing_and_primary:
|
||||
primary_email = frappe.get_value('Customer', customer_name, 'email_id')
|
||||
if primary_email is None and int(primary_mandatory):
|
||||
frappe.throw('No primary email found for customer: '+ customer_name)
|
||||
frappe.throw(_("No primary email found for customer: {0}").format(customer_name))
|
||||
return [primary_email or '', billing_email[0][0]]
|
||||
else:
|
||||
return billing_email[0][0] or ''
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe.utils import cstr
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.model.document import Document
|
||||
|
||||
pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group'
|
||||
pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group',
|
||||
'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from',
|
||||
'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier',
|
||||
'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules']
|
||||
@@ -111,4 +111,4 @@ def get_args_for_pricing_rule(doc):
|
||||
for d in pricing_rule_fields:
|
||||
args[d] = doc.get(d)
|
||||
|
||||
return args
|
||||
return args
|
||||
|
||||
@@ -496,15 +496,6 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc,
|
||||
}
|
||||
}
|
||||
|
||||
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
|
||||
if(doc.select_print_heading){
|
||||
// print heading
|
||||
cur_frm.pformat.print_heading = doc.select_print_heading;
|
||||
}
|
||||
else
|
||||
cur_frm.pformat.print_heading = __("Purchase Invoice");
|
||||
}
|
||||
|
||||
frappe.ui.form.on("Purchase Invoice", {
|
||||
setup: function(frm) {
|
||||
frm.custom_make_buttons = {
|
||||
@@ -523,6 +514,28 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.events.add_custom_buttons(frm);
|
||||
},
|
||||
|
||||
add_custom_buttons: function(frm) {
|
||||
if (frm.doc.per_received < 100) {
|
||||
frm.add_custom_button(__('Purchase Receipt'), () => {
|
||||
frm.events.make_purchase_receipt(frm);
|
||||
}, __('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received > 0) {
|
||||
frm.add_custom_button(__('Purchase Receipt'), () => {
|
||||
frappe.route_options = {
|
||||
'purchase_invoice': frm.doc.name
|
||||
}
|
||||
|
||||
frappe.set_route("List", "Purchase Receipt", "List")
|
||||
}, __('View'));
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
if(frm.doc.__onload && frm.is_new()) {
|
||||
if(frm.doc.supplier) {
|
||||
@@ -548,5 +561,13 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
update_stock: function(frm) {
|
||||
hide_fields(frm.doc);
|
||||
frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false);
|
||||
},
|
||||
|
||||
make_purchase_receipt: function(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
|
||||
frm: frm,
|
||||
freeze_message: __("Creating Purchase Receipt ...")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -127,7 +127,6 @@
|
||||
"write_off_cost_center",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"adjust_advance_taxes",
|
||||
"get_advances",
|
||||
"advances",
|
||||
"payment_schedule_section",
|
||||
@@ -164,7 +163,8 @@
|
||||
"to_date",
|
||||
"column_break_114",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference"
|
||||
"update_auto_repeat_reference",
|
||||
"per_received"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -1326,13 +1326,6 @@
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Taxes paid while advance payment will be adjusted against this invoice",
|
||||
"fieldname": "adjust_advance_taxes",
|
||||
"fieldtype": "Check",
|
||||
"label": "Adjust Advance Taxes"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_internal_supplier",
|
||||
"description": "Unrealized Profit / Loss account for intra-company transfers",
|
||||
@@ -1372,13 +1365,22 @@
|
||||
"print_hide": 1,
|
||||
"print_width": "50px",
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"fieldname": "per_received",
|
||||
"fieldtype": "Percent",
|
||||
"hidden": 1,
|
||||
"label": "Per Received",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-09 21:15:30.422084",
|
||||
"modified": "2021-04-30 22:45:58.334107",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -1207,3 +1207,41 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
|
||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||
target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \
|
||||
flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
|
||||
doc = get_mapped_doc("Purchase Invoice", source_name, {
|
||||
"Purchase Invoice": {
|
||||
"doctype": "Purchase Receipt",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
}
|
||||
},
|
||||
"Purchase Invoice Item": {
|
||||
"doctype": "Purchase Receipt Item",
|
||||
"field_map": {
|
||||
"name": "purchase_invoice_item",
|
||||
"parent": "purchase_invoice",
|
||||
"bom": "bom",
|
||||
"purchase_order": "purchase_order",
|
||||
"po_detail": "purchase_order_item",
|
||||
"material_request": "material_request",
|
||||
"material_request_item": "material_request_item"
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges"
|
||||
}
|
||||
}, target_doc)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -397,7 +397,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
pi.update({
|
||||
"payment_schedule": get_payment_terms("_Test Payment Term Template",
|
||||
pi.posting_date, pi.grand_total)
|
||||
pi.posting_date, pi.grand_total, pi.base_grand_total)
|
||||
})
|
||||
|
||||
pi.save()
|
||||
@@ -898,7 +898,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
acc_settings.submit_journal_entries = 1
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_expense_account = deferred_account
|
||||
item.save()
|
||||
|
||||
@@ -607,6 +607,7 @@
|
||||
"oldfieldname": "purchase_order",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -853,7 +854,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-23 00:59:52.614805",
|
||||
"modified": "2021-03-30 09:02:39.256602",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// print heading
|
||||
cur_frm.pformat.print_heading = 'Invoice';
|
||||
|
||||
{% include 'erpnext/selling/sales_common.js' %};
|
||||
frappe.provide("erpnext.accounts");
|
||||
|
||||
@@ -359,11 +356,11 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
},
|
||||
|
||||
items_on_form_rendered: function() {
|
||||
erpnext.setup_serial_no();
|
||||
erpnext.setup_serial_or_batch_no();
|
||||
},
|
||||
|
||||
packed_items_on_form_rendered: function(doc, grid_row) {
|
||||
erpnext.setup_serial_no();
|
||||
erpnext.setup_serial_or_batch_no();
|
||||
},
|
||||
|
||||
make_sales_return: function() {
|
||||
@@ -916,7 +913,7 @@ frappe.ui.form.on('Sales Invoice Timesheet', {
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message){
|
||||
data = r.message;
|
||||
let data = r.message;
|
||||
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
|
||||
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
|
||||
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"in_words",
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
@@ -1109,6 +1110,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@@ -1120,6 +1122,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@@ -1168,6 +1171,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@@ -1180,6 +1184,7 @@
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@@ -1945,6 +1950,13 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Set Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "grand_total",
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -1957,7 +1969,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-03-31 15:42:26.261540",
|
||||
"modified": "2021-04-15 23:57:58.766651",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -46,7 +46,6 @@ class SalesInvoice(SellingController):
|
||||
'target_parent_dt': 'Sales Order',
|
||||
'target_parent_field': 'per_billed',
|
||||
'source_field': 'amount',
|
||||
'join_field': 'so_detail',
|
||||
'percent_join_field': 'sales_order',
|
||||
'status_field': 'billing_status',
|
||||
'keyword': 'Billed',
|
||||
@@ -77,7 +76,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if not self.is_pos:
|
||||
self.so_dn_required()
|
||||
|
||||
|
||||
self.set_tax_withholding()
|
||||
|
||||
self.validate_proj_cust()
|
||||
@@ -276,7 +275,7 @@ class SalesInvoice(SellingController):
|
||||
pluck="pos_closing_entry"
|
||||
)
|
||||
if pos_closing_entry:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format(
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
|
||||
)
|
||||
@@ -394,6 +393,7 @@ class SalesInvoice(SellingController):
|
||||
if validate_against_credit_limit:
|
||||
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_missing_values(self, for_validate=False):
|
||||
pos = self.set_pos_fields(for_validate)
|
||||
|
||||
@@ -548,12 +548,12 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
|
||||
msg = _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " "
|
||||
msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
|
||||
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
|
||||
msg += _("Change the account type to Receivable or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
@@ -733,6 +733,7 @@ class SalesInvoice(SellingController):
|
||||
else:
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_timesheet_data(self):
|
||||
self.set('timesheets', [])
|
||||
if self.project:
|
||||
@@ -1110,7 +1111,7 @@ class SalesInvoice(SellingController):
|
||||
if not item.serial_no:
|
||||
continue
|
||||
|
||||
for serial_no in item.serial_no.split("\n"):
|
||||
for serial_no in get_serial_nos(item.serial_no):
|
||||
if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code:
|
||||
frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice)
|
||||
|
||||
@@ -1290,6 +1291,7 @@ class SalesInvoice(SellingController):
|
||||
break
|
||||
|
||||
# Healthcare
|
||||
@frappe.whitelist()
|
||||
def set_healthcare_services(self, checked_values):
|
||||
self.set("items", [])
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
@@ -1753,15 +1755,10 @@ def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, wa
|
||||
item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
|
||||
|
||||
def get_delivery_note_details(internal_reference):
|
||||
so_item_map = {}
|
||||
|
||||
si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
|
||||
filters={'parent': internal_reference})
|
||||
|
||||
for d in si_item_details:
|
||||
so_item_map.setdefault(d.name, d.so_detail)
|
||||
|
||||
return so_item_map
|
||||
return {d.name: d.so_detail for d in si_item_details if d.so_detail}
|
||||
|
||||
def get_sales_invoice_details(internal_reference):
|
||||
dn_item_map = {}
|
||||
|
||||
@@ -1802,6 +1802,15 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.selling_price_list = "_Test Price List Rest of the World"
|
||||
si.update_stock = 1
|
||||
si.items[0].target_warehouse = 'Work In Progress - TCP1'
|
||||
|
||||
# Add stock to stores for succesful stock transfer
|
||||
make_stock_entry(
|
||||
target="Stores - TCP1",
|
||||
company = "_Test Company with perpetual inventory",
|
||||
qty=1,
|
||||
basic_rate=100
|
||||
)
|
||||
|
||||
add_taxes(si)
|
||||
si.save()
|
||||
|
||||
@@ -1870,7 +1879,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
def test_einvoice_submission_without_irn(self):
|
||||
# init
|
||||
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
|
||||
einvoice_settings = frappe.get_doc('E Invoice Settings')
|
||||
einvoice_settings.enable = 1
|
||||
einvoice_settings.applicable_from = nowdate()
|
||||
einvoice_settings.append('credentials', {
|
||||
'company': '_Test Company',
|
||||
'gstin': '27AAECE4835E1ZR',
|
||||
'username': 'test',
|
||||
'password': 'test'
|
||||
})
|
||||
einvoice_settings.save()
|
||||
|
||||
country = frappe.flags.country
|
||||
frappe.flags.country = 'India'
|
||||
|
||||
@@ -1881,7 +1900,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.submit()
|
||||
|
||||
# reset
|
||||
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
|
||||
einvoice_settings = frappe.get_doc('E Invoice Settings')
|
||||
einvoice_settings.enable = 0
|
||||
frappe.flags.country = country
|
||||
|
||||
def test_einvoice_json(self):
|
||||
@@ -2272,4 +2292,4 @@ def add_taxes(doc):
|
||||
"cost_center": "Main - TCP1",
|
||||
"description": "Excise Duty",
|
||||
"rate": 12
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,5 +46,5 @@ def validate_disabled(doc):
|
||||
frappe.throw(_("Disabled template must not be default template"))
|
||||
|
||||
def validate_for_tax_category(doc):
|
||||
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}):
|
||||
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
|
||||
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"additional_discount_percentage",
|
||||
"additional_discount_amount",
|
||||
"sb_3",
|
||||
"submit_invoice",
|
||||
"invoices",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
@@ -45,9 +46,7 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cb_1",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
@@ -55,97 +54,73 @@
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subscription_period",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subscription Period",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Subscription Period"
|
||||
},
|
||||
{
|
||||
"fieldname": "cancelation_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Cancelation Date",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "trial_period_start",
|
||||
"fieldtype": "Date",
|
||||
"label": "Trial Period Start Date",
|
||||
"set_only_once": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.trial_period_start",
|
||||
"fieldname": "trial_period_end",
|
||||
"fieldtype": "Date",
|
||||
"label": "Trial Period End Date",
|
||||
"set_only_once": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "current_invoice_start",
|
||||
"fieldtype": "Date",
|
||||
"label": "Current Invoice Start Date",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "current_invoice_end",
|
||||
"fieldtype": "Date",
|
||||
"label": "Current Invoice End Date",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Number of days that the subscriber has to pay invoices generated by this subscription",
|
||||
"fieldname": "days_until_due",
|
||||
"fieldtype": "Int",
|
||||
"label": "Days Until Due",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Days Until Due"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "cancel_at_period_end",
|
||||
"fieldtype": "Check",
|
||||
"label": "Cancel At End Of Period",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Cancel At End Of Period"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "generate_invoice_at_period_start",
|
||||
"fieldtype": "Check",
|
||||
"label": "Generate Invoice At Beginning Of Period",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Generate Invoice At Beginning Of Period"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "sb_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Plans",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Plans"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -153,84 +128,62 @@
|
||||
"fieldtype": "Table",
|
||||
"label": "Plans",
|
||||
"options": "Subscription Plan Detail",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
|
||||
"fieldname": "sb_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Taxes",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Taxes"
|
||||
},
|
||||
{
|
||||
"fieldname": "sb_2",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discounts",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Discounts"
|
||||
},
|
||||
{
|
||||
"fieldname": "apply_additional_discount",
|
||||
"fieldtype": "Select",
|
||||
"label": "Apply Additional Discount On",
|
||||
"options": "\nGrand Total\nNet Total",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "\nGrand Total\nNet Total"
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_2",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "additional_discount_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Additional DIscount Percentage",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Additional DIscount Percentage"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "additional_discount_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional DIscount Amount",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Additional DIscount Amount"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.invoices",
|
||||
"fieldname": "sb_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Invoices",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Invoices"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "invoices",
|
||||
"fieldtype": "Table",
|
||||
"label": "Invoices",
|
||||
"options": "Subscription Invoice",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Subscription Invoice"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
@@ -238,9 +191,7 @@
|
||||
"label": "Party Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
@@ -249,27 +200,21 @@
|
||||
"label": "Party",
|
||||
"options": "party_type",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_type === 'Customer'",
|
||||
"fieldname": "sales_tax_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sales Taxes and Charges Template",
|
||||
"options": "Sales Taxes and Charges Template",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Sales Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_type === 'Supplier'",
|
||||
"fieldname": "purchase_tax_template",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Taxes and Charges Template",
|
||||
"options": "Purchase Taxes and Charges Template",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Purchase Taxes and Charges Template"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -277,55 +222,49 @@
|
||||
"fieldname": "follow_calendar_months",
|
||||
"fieldtype": "Check",
|
||||
"label": "Follow Calendar Months",
|
||||
"set_only_once": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date",
|
||||
"fieldname": "generate_new_invoices_past_due_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Generate New Invoices Past Due Date",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Generate New Invoices Past Due Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Subscription End Date",
|
||||
"set_only_once": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Subscription Start Date",
|
||||
"set_only_once": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "submit_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Submit Invoice Automatically"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-09 15:44:20.024789",
|
||||
"modified": "2021-04-19 15:24:27.550797",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription",
|
||||
|
||||
@@ -276,7 +276,7 @@ class Subscription(Document):
|
||||
frappe.throw(_('Subscription End Date is mandatory to follow calendar months'))
|
||||
|
||||
if billing_info[0]['billing_interval'] != 'Month':
|
||||
frappe.throw('Billing Interval in Subscription Plan must be Month to follow calendar months')
|
||||
frappe.throw(_('Billing Interval in Subscription Plan must be Month to follow calendar months'))
|
||||
|
||||
def after_insert(self):
|
||||
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
|
||||
@@ -383,7 +383,9 @@ class Subscription(Document):
|
||||
|
||||
invoice.flags.ignore_mandatory = True
|
||||
invoice.save()
|
||||
invoice.submit()
|
||||
|
||||
if self.submit_invoice:
|
||||
invoice.submit()
|
||||
|
||||
return invoice
|
||||
|
||||
|
||||
@@ -14,10 +14,15 @@ test_records = frappe.get_test_records('Tax Rule')
|
||||
from six import iteritems
|
||||
|
||||
class TestTaxRule(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
frappe.db.set_value("Shopping Cart Settings", None, "enabled", 0)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def tearDown(self):
|
||||
def setUp(self):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_conflict(self):
|
||||
|
||||
@@ -21,7 +21,10 @@ def get_party_details(inv):
|
||||
else:
|
||||
party_type = 'Supplier'
|
||||
party = inv.supplier
|
||||
|
||||
|
||||
if not party:
|
||||
frappe.throw(_("Please select {0} first").format(party_type))
|
||||
|
||||
return party_type, party
|
||||
|
||||
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
@@ -251,7 +254,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
||||
threshold = tax_details.get('threshold', 0)
|
||||
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
|
||||
|
||||
if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
|
||||
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
|
||||
if ldc and is_valid_certificate(
|
||||
ldc.valid_from, ldc.valid_upto,
|
||||
inv.posting_date, tax_deducted,
|
||||
@@ -324,7 +327,7 @@ def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, post
|
||||
net_total, ldc.certificate_limit
|
||||
):
|
||||
tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
|
||||
|
||||
|
||||
return tds_amount
|
||||
|
||||
def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
|
||||
|
||||
@@ -87,50 +87,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def test_single_threshold_tds_with_previous_vouchers(self):
|
||||
invoices = []
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
|
||||
self.assertEqual(pi.grand_total, 8000)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
|
||||
invoices = []
|
||||
doc = create_supplier(supplier_name = "Test TDS Supplier ABC",
|
||||
tax_withholding_category="Single Threshold TDS")
|
||||
supplier = doc.name
|
||||
|
||||
pi = create_purchase_invoice(supplier=supplier)
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# TDS not applied
|
||||
pi = create_purchase_invoice(supplier=supplier, do_not_apply_tds=True)
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
pi = create_purchase_invoice(supplier=supplier)
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
|
||||
self.assertEqual(pi.grand_total, 8000)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
|
||||
invoices = []
|
||||
@@ -177,7 +133,7 @@ def cancel_invoices():
|
||||
|
||||
for d in purchase_invoices:
|
||||
frappe.get_doc('Purchase Invoice', d).cancel()
|
||||
|
||||
|
||||
for d in sales_invoices:
|
||||
frappe.get_doc('Sales Invoice', d).cancel()
|
||||
|
||||
@@ -229,7 +185,8 @@ def create_sales_invoice(**args):
|
||||
'qty': args.qty or 1,
|
||||
'rate': args.rate or 10000,
|
||||
'cost_center': 'Main - _TC',
|
||||
'expense_account': 'Cost of Goods Sold - _TC'
|
||||
'expense_account': 'Cost of Goods Sold - _TC',
|
||||
'warehouse': args.warehouse or '_Test Warehouse - _TC'
|
||||
}]
|
||||
})
|
||||
|
||||
@@ -353,4 +310,4 @@ def create_tax_with_holding_category():
|
||||
'company': '_Test Company',
|
||||
'account': 'TDS - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
}).insert()
|
||||
|
||||
@@ -18,7 +18,8 @@ def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, upd
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||
else:
|
||||
# Post GL Map proccess there may no be any GL Entries
|
||||
elif gl_map:
|
||||
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
|
||||
else:
|
||||
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||
@@ -170,7 +171,7 @@ def round_off_debit_credit(gl_map):
|
||||
else:
|
||||
allowance = .5
|
||||
|
||||
if abs(debit_credit_diff) >= allowance:
|
||||
if abs(debit_credit_diff) > allowance:
|
||||
frappe.throw(_("Debit and Credit not equal for {0} #{1}. Difference is {2}.")
|
||||
.format(gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff))
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"attach_print": 0,
|
||||
"channel": "Email",
|
||||
"condition": "doc.auto_created",
|
||||
"creation": "2018-04-25 14:19:05.440361",
|
||||
"days_in_advance": 0,
|
||||
|
||||
@@ -364,7 +364,7 @@ class ReceivablePayableReport(object):
|
||||
payment_terms_details = frappe.db.sql("""
|
||||
select
|
||||
si.name, si.party_account_currency, si.currency, si.conversion_rate,
|
||||
ps.due_date, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
|
||||
ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
|
||||
from `tab{0}` si, `tabPayment Schedule` ps
|
||||
where
|
||||
si.name = ps.parent and
|
||||
@@ -394,7 +394,7 @@ class ReceivablePayableReport(object):
|
||||
"due_date": d.due_date,
|
||||
"invoiced": invoiced,
|
||||
"invoice_grand_total": row.invoiced,
|
||||
"payment_term": d.description,
|
||||
"payment_term": d.description or d.payment_term,
|
||||
"paid": d.paid_amount + d.discounted_amount,
|
||||
"credit_note": 0.0,
|
||||
"outstanding": invoiced - d.paid_amount - d.discounted_amount
|
||||
|
||||
@@ -5,7 +5,8 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, cint
|
||||
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data)
|
||||
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data,
|
||||
get_filtered_list_for_consolidated_report)
|
||||
|
||||
def execute(filters=None):
|
||||
period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
|
||||
@@ -132,6 +133,10 @@ def get_report_summary(period_list, asset, liability, equity, provisional_profit
|
||||
if filters.get('accumulated_values'):
|
||||
period_list = [period_list[-1]]
|
||||
|
||||
# from consolidated financial statement
|
||||
if filters.get('accumulated_in_group_company'):
|
||||
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
|
||||
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
if asset:
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports['Billed Items To Be Received'] = {
|
||||
'filters': [
|
||||
{
|
||||
'label': __('Company'),
|
||||
'fieldname': 'company',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Company',
|
||||
'reqd': 1,
|
||||
'default': frappe.defaults.get_default('Company')
|
||||
},
|
||||
{
|
||||
'label': __('As on Date'),
|
||||
'fieldname': 'posting_date',
|
||||
'fieldtype': 'Date',
|
||||
'reqd': 1,
|
||||
'default': get_today()
|
||||
},
|
||||
{
|
||||
'label': __('Purchase Invoice'),
|
||||
'fieldname': 'purchase_invoice',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Purchase Invoice'
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-03-30 09:35:38.683028",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-03-31 08:48:30.944429",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Billed Items To Be Received",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "",
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Billed Items To Be Received",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
def execute(filters=None):
|
||||
data = get_data(filters) or []
|
||||
columns = get_columns()
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(report_filters):
|
||||
filters = get_report_filters(report_filters)
|
||||
fields = get_report_fields()
|
||||
|
||||
return frappe.get_all('Purchase Invoice',
|
||||
fields= fields, filters=filters)
|
||||
|
||||
def get_report_filters(report_filters):
|
||||
filters = [['Purchase Invoice','company','=',report_filters.get('company')],
|
||||
['Purchase Invoice','posting_date','<=',report_filters.get('posting_date')], ['Purchase Invoice','docstatus','=',1],
|
||||
['Purchase Invoice','per_received','<',100], ['Purchase Invoice','update_stock','=',0]]
|
||||
|
||||
if report_filters.get('purchase_invoice'):
|
||||
filters.append(['Purchase Invoice','per_received','in',[report_filters.get('purchase_invoice')]])
|
||||
|
||||
return filters
|
||||
|
||||
def get_report_fields():
|
||||
fields = []
|
||||
for p_field in ['name', 'supplier', 'company', 'posting_date', 'currency']:
|
||||
fields.append('`tabPurchase Invoice`.`{}`'.format(p_field))
|
||||
|
||||
for c_field in ['item_code', 'item_name', 'uom', 'qty', 'received_qty', 'rate', 'amount']:
|
||||
fields.append('`tabPurchase Invoice Item`.`{}`'.format(c_field))
|
||||
|
||||
return fields
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
'label': _('Purchase Invoice'),
|
||||
'fieldname': 'name',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Purchase Invoice',
|
||||
'width': 170
|
||||
},
|
||||
{
|
||||
'label': _('Supplier'),
|
||||
'fieldname': 'supplier',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Supplier',
|
||||
'width': 120
|
||||
},
|
||||
{
|
||||
'label': _('Posting Date'),
|
||||
'fieldname': 'posting_date',
|
||||
'fieldtype': 'Date',
|
||||
'width': 100
|
||||
},
|
||||
{
|
||||
'label': _('Item Code'),
|
||||
'fieldname': 'item_code',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'Item',
|
||||
'width': 100
|
||||
},
|
||||
{
|
||||
'label': _('Item Name'),
|
||||
'fieldname': 'item_name',
|
||||
'fieldtype': 'Data',
|
||||
'width': 100
|
||||
},
|
||||
{
|
||||
'label': _('UOM'),
|
||||
'fieldname': 'uom',
|
||||
'fieldtype': 'Link',
|
||||
'options': 'UOM',
|
||||
'width': 100
|
||||
},
|
||||
{
|
||||
'label': _('Invoiced Qty'),
|
||||
'fieldname': 'qty',
|
||||
'fieldtype': 'Float',
|
||||
'width': 100
|
||||
},
|
||||
{
|
||||
'label': _('Received Qty'),
|
||||
'fieldname': 'received_qty',
|
||||
'fieldtype': 'Float',
|
||||
'width': 100
|
||||
},
|
||||
{
|
||||
'label': _('Rate'),
|
||||
'fieldname': 'rate',
|
||||
'fieldtype': 'Currency',
|
||||
'width': 100
|
||||
},
|
||||
{
|
||||
'label': _('Amount'),
|
||||
'fieldname': 'amount',
|
||||
'fieldtype': 'Currency',
|
||||
'width': 100
|
||||
}
|
||||
]
|
||||
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, cstr
|
||||
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data)
|
||||
from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data, get_filtered_list_for_consolidated_report)
|
||||
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import get_net_profit_loss
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from six import iteritems
|
||||
@@ -67,9 +67,9 @@ def execute(filters=None):
|
||||
section_data.append(account_data)
|
||||
|
||||
add_total_row_account(data, section_data, cash_flow_account['section_footer'],
|
||||
period_list, company_currency, summary_data)
|
||||
period_list, company_currency, summary_data, filters)
|
||||
|
||||
add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency, summary_data)
|
||||
add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
|
||||
chart = get_chart_data(columns, data)
|
||||
@@ -162,18 +162,26 @@ def get_start_date(period, accumulated_values, company):
|
||||
|
||||
return start_date
|
||||
|
||||
def add_total_row_account(out, data, label, period_list, currency, summary_data, consolidated = False):
|
||||
def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False):
|
||||
total_row = {
|
||||
"account_name": "'" + _("{0}").format(label) + "'",
|
||||
"account": "'" + _("{0}").format(label) + "'",
|
||||
"currency": currency
|
||||
}
|
||||
|
||||
summary_data[label] = 0
|
||||
|
||||
# from consolidated financial statement
|
||||
if filters.get('accumulated_in_group_company'):
|
||||
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
|
||||
|
||||
for row in data:
|
||||
if row.get("parent_account"):
|
||||
for period in period_list:
|
||||
key = period if consolidated else period['key']
|
||||
total_row.setdefault(key, 0.0)
|
||||
total_row[key] += row.get(key, 0.0)
|
||||
summary_data[label] += row.get(key)
|
||||
|
||||
total_row.setdefault("total", 0.0)
|
||||
total_row["total"] += row["total"]
|
||||
@@ -181,7 +189,6 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
|
||||
out.append(total_row)
|
||||
out.append({})
|
||||
|
||||
summary_data[label] = total_row["total"]
|
||||
|
||||
def get_report_summary(summary_data, currency):
|
||||
report_summary = []
|
||||
|
||||
@@ -2,118 +2,128 @@
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Consolidated Financial Statement"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
"label": __("Filter Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": ["Fiscal Year"],
|
||||
"reqd": 1,
|
||||
on_change: function() {
|
||||
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
|
||||
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
|
||||
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
|
||||
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
|
||||
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
|
||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
frappe.query_reports["Consolidated Financial Statement"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
"label": __("Filter Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": ["Fiscal Year"],
|
||||
"reqd": 1,
|
||||
on_change: function() {
|
||||
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
|
||||
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
|
||||
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
|
||||
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
|
||||
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
|
||||
|
||||
frappe.query_report.refresh();
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_fiscal_year",
|
||||
"label": __("Start Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
"label": __("End Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"finance_book",
|
||||
"label": __("Finance Book"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"fieldname":"report",
|
||||
"label": __("Report"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Profit and Loss Statement", "Balance Sheet", "Cash Flow"],
|
||||
"default": "Balance Sheet",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "presentation_currency",
|
||||
"label": __("Currency"),
|
||||
"fieldtype": "Select",
|
||||
"options": erpnext.get_presentation_currency_list(),
|
||||
"default": frappe.defaults.get_user_default("Currency")
|
||||
},
|
||||
{
|
||||
"fieldname":"accumulated_in_group_company",
|
||||
"label": __("Accumulated Values in Group Company"),
|
||||
"fieldtype": "Check",
|
||||
"default": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "include_default_book_entries",
|
||||
"label": __("Include Default Book Entries"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
}
|
||||
],
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
if (data && column.fieldname=="account") {
|
||||
value = data.account_name || value;
|
||||
|
||||
column.link_onclick =
|
||||
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
||||
column.is_tree = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_fiscal_year",
|
||||
"label": __("Start Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
"label": __("End Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"finance_book",
|
||||
"label": __("Finance Book"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"fieldname":"report",
|
||||
"label": __("Report"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Profit and Loss Statement", "Balance Sheet", "Cash Flow"],
|
||||
"default": "Balance Sheet",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "presentation_currency",
|
||||
"label": __("Currency"),
|
||||
"fieldtype": "Select",
|
||||
"options": erpnext.get_presentation_currency_list(),
|
||||
"default": frappe.defaults.get_user_default("Currency")
|
||||
},
|
||||
{
|
||||
"fieldname":"accumulated_in_group_company",
|
||||
"label": __("Accumulated Values in Group Company"),
|
||||
"fieldtype": "Check",
|
||||
"default": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "include_default_book_entries",
|
||||
"label": __("Include Default Book Entries"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
}
|
||||
],
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (!data.parent_account) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
if (!data.parent_account) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onload: function() {
|
||||
let fiscal_year = frappe.defaults.get_user_default("fiscal_year")
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
onload: function() {
|
||||
let fiscal_year = frappe.defaults.get_user_default("fiscal_year")
|
||||
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -94,7 +94,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||
|
||||
report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, company_currency, True)
|
||||
report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, company_currency, filters, True)
|
||||
|
||||
return data, None, chart, report_summary
|
||||
|
||||
@@ -149,9 +149,9 @@ def get_cash_flow_data(fiscal_year, companies, filters):
|
||||
section_data.append(account_data)
|
||||
|
||||
add_total_row_account(data, section_data, cash_flow_account['section_footer'],
|
||||
companies, company_currency, summary_data, True)
|
||||
companies, company_currency, summary_data, filters, True)
|
||||
|
||||
add_total_row_account(data, data, _("Net Change in Cash"), companies, company_currency, summary_data, True)
|
||||
add_total_row_account(data, data, _("Net Change in Cash"), companies, company_currency, summary_data, filters, True)
|
||||
|
||||
report_summary = get_cash_flow_summary(summary_data, company_currency)
|
||||
|
||||
@@ -329,8 +329,9 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
|
||||
has_value = False
|
||||
total = 0
|
||||
row = frappe._dict({
|
||||
"account_name": _(d.account_name),
|
||||
"account": _(d.account_name),
|
||||
"account_name": ('%s - %s' %(_(d.account_number), _(d.account_name))
|
||||
if d.account_number else _(d.account_name)),
|
||||
"account": _(d.name),
|
||||
"parent_account": _(d.parent_account),
|
||||
"indent": flt(d.indent),
|
||||
"year_start_date": start_date,
|
||||
|
||||
@@ -119,10 +119,10 @@ def validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year):
|
||||
|
||||
def validate_dates(from_date, to_date):
|
||||
if not from_date or not to_date:
|
||||
frappe.throw("From Date and To Date are mandatory")
|
||||
frappe.throw(_("From Date and To Date are mandatory"))
|
||||
|
||||
if to_date < from_date:
|
||||
frappe.throw("To Date cannot be less than From Date")
|
||||
frappe.throw(_("To Date cannot be less than From Date"))
|
||||
|
||||
def get_months(start_date, end_date):
|
||||
diff = (12 * end_date.year + end_date.month) - (12 * start_date.year + start_date.month)
|
||||
@@ -522,4 +522,12 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None):
|
||||
"width": 150
|
||||
})
|
||||
|
||||
return columns
|
||||
return columns
|
||||
|
||||
def get_filtered_list_for_consolidated_report(filters, period_list):
|
||||
filtered_summary_list = []
|
||||
for period in period_list:
|
||||
if period == filters.get('company'):
|
||||
filtered_summary_list.append(period)
|
||||
|
||||
return filtered_summary_list
|
||||
|
||||
@@ -116,22 +116,19 @@ def validate_filters(filters):
|
||||
frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method"))
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s".format(
|
||||
company=filters.get("company"),
|
||||
from_date=filters.get("from_date"),
|
||||
to_date=filters.get("to_date"))
|
||||
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s"
|
||||
|
||||
if filters.get("pos_profile"):
|
||||
conditions += " AND pos_profile = %(pos_profile)s".format(pos_profile=filters.get("pos_profile"))
|
||||
conditions += " AND pos_profile = %(pos_profile)s"
|
||||
|
||||
if filters.get("owner"):
|
||||
conditions += " AND owner = %(owner)s".format(owner=filters.get("owner"))
|
||||
conditions += " AND owner = %(owner)s"
|
||||
|
||||
if filters.get("customer"):
|
||||
conditions += " AND customer = %(customer)s".format(customer=filters.get("customer"))
|
||||
conditions += " AND customer = %(customer)s"
|
||||
|
||||
if filters.get("is_return"):
|
||||
conditions += " AND is_return = %(is_return)s".format(is_return=filters.get("is_return"))
|
||||
conditions += " AND is_return = %(is_return)s"
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user