Merge branch 'version-13-hotfix' into asset-repair-refactor
This commit is contained in:
commit
6eeaf9b933
1
.flake8
1
.flake8
@ -30,3 +30,4 @@ ignore =
|
|||||||
W191,
|
W191,
|
||||||
|
|
||||||
max-line-length = 200
|
max-line-length = 200
|
||||||
|
exclude=.github/helper/semgrep_rules
|
||||||
|
12
.git-blame-ignore-revs
Normal file
12
.git-blame-ignore-revs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Since version 2.23 (released in August 2019), git-blame has a feature
|
||||||
|
# to ignore or bypass certain commits.
|
||||||
|
#
|
||||||
|
# This file contains a list of commits that are not likely what you
|
||||||
|
# are looking for in a blame, such as mass reformatting or renaming.
|
||||||
|
# You can set this file as a default ignore file for blame by running
|
||||||
|
# the following command.
|
||||||
|
#
|
||||||
|
# $ git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
|
|
||||||
|
# This commit just changes spaces to tabs for indentation in some files
|
||||||
|
5f473611bd6ed57703716244a054d3fb5ba9cd23
|
@ -4,25 +4,61 @@ from frappe import _, flt
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
# ruleid: frappe-modifying-but-not-comitting
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.value_of_goods == 0:
|
if self.value_of_goods == 0:
|
||||||
frappe.throw(_('Value of goods cannot be 0'))
|
frappe.throw(_('Value of goods cannot be 0'))
|
||||||
# ruleid: frappe-modifying-after-submit
|
|
||||||
self.status = 'Submitted'
|
self.status = 'Submitted'
|
||||||
|
|
||||||
|
|
||||||
|
# ok: frappe-modifying-but-not-comitting
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if flt(self.per_billed) < 100:
|
if self.value_of_goods == 0:
|
||||||
self.update_billing_status()
|
frappe.throw(_('Value of goods cannot be 0'))
|
||||||
else:
|
self.status = 'Submitted'
|
||||||
# todook: frappe-modifying-after-submit
|
self.db_set('status', 'Submitted')
|
||||||
self.status = "Completed"
|
|
||||||
self.db_set("status", "Completed")
|
|
||||||
|
|
||||||
class TestDoc(Document):
|
# ok: frappe-modifying-but-not-comitting
|
||||||
pass
|
def on_submit(self):
|
||||||
|
if self.value_of_goods == 0:
|
||||||
|
frappe.throw(_('Value of goods cannot be 0'))
|
||||||
|
x = "y"
|
||||||
|
self.status = x
|
||||||
|
self.db_set('status', x)
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
#ruleid: frappe-modifying-child-tables-while-iterating
|
# ok: frappe-modifying-but-not-comitting
|
||||||
for item in self.child_table:
|
def on_submit(self):
|
||||||
if item.value < 0:
|
x = "y"
|
||||||
self.remove(item)
|
self.status = x
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
# ruleid: frappe-modifying-but-not-comitting-other-method
|
||||||
|
class DoctypeClass(Document):
|
||||||
|
def on_submit(self):
|
||||||
|
self.good_method()
|
||||||
|
self.tainted_method()
|
||||||
|
|
||||||
|
def tainted_method(self):
|
||||||
|
self.status = "uptate"
|
||||||
|
|
||||||
|
|
||||||
|
# ok: frappe-modifying-but-not-comitting-other-method
|
||||||
|
class DoctypeClass(Document):
|
||||||
|
def on_submit(self):
|
||||||
|
self.good_method()
|
||||||
|
self.tainted_method()
|
||||||
|
|
||||||
|
def tainted_method(self):
|
||||||
|
self.status = "update"
|
||||||
|
self.db_set("status", "update")
|
||||||
|
|
||||||
|
# ok: frappe-modifying-but-not-comitting-other-method
|
||||||
|
class DoctypeClass(Document):
|
||||||
|
def on_submit(self):
|
||||||
|
self.good_method()
|
||||||
|
self.tainted_method()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def tainted_method(self):
|
||||||
|
self.status = "uptate"
|
||||||
|
@ -1,32 +1,93 @@
|
|||||||
# This file specifies rules for correctness according to how frappe doctype data model works.
|
# This file specifies rules for correctness according to how frappe doctype data model works.
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
- id: frappe-modifying-after-submit
|
- id: frappe-modifying-but-not-comitting
|
||||||
patterns:
|
patterns:
|
||||||
- pattern: self.$ATTR = ...
|
- pattern: |
|
||||||
- pattern-inside: |
|
def $METHOD(self, ...):
|
||||||
def on_submit(self, ...):
|
|
||||||
...
|
...
|
||||||
|
self.$ATTR = ...
|
||||||
|
- pattern-not: |
|
||||||
|
def $METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ATTR = ...
|
||||||
|
...
|
||||||
|
self.db_set(..., self.$ATTR, ...)
|
||||||
|
- pattern-not: |
|
||||||
|
def $METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ATTR = $SOME_VAR
|
||||||
|
...
|
||||||
|
self.db_set(..., $SOME_VAR, ...)
|
||||||
|
- pattern-not: |
|
||||||
|
def $METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ATTR = $SOME_VAR
|
||||||
|
...
|
||||||
|
self.save()
|
||||||
- metavariable-regex:
|
- metavariable-regex:
|
||||||
metavariable: '$ATTR'
|
metavariable: '$ATTR'
|
||||||
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
|
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
|
||||||
regex: '^(?!status_updater)(.*)$'
|
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
|
||||||
|
- metavariable-regex:
|
||||||
|
metavariable: "$METHOD"
|
||||||
|
regex: "(on_submit|on_cancel)"
|
||||||
message: |
|
message: |
|
||||||
Doctype modified after submission. Please check if modification of self.$ATTR is commited to database.
|
DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
|
||||||
languages: [python]
|
languages: [python]
|
||||||
severity: ERROR
|
severity: ERROR
|
||||||
|
|
||||||
- id: frappe-modifying-after-cancel
|
- id: frappe-modifying-but-not-comitting-other-method
|
||||||
patterns:
|
patterns:
|
||||||
- pattern: self.$ATTR = ...
|
- pattern: |
|
||||||
- pattern-inside: |
|
class $DOCTYPE(...):
|
||||||
def on_cancel(self, ...):
|
def $METHOD(self, ...):
|
||||||
...
|
...
|
||||||
|
self.$ANOTHER_METHOD()
|
||||||
|
...
|
||||||
|
|
||||||
|
def $ANOTHER_METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ATTR = ...
|
||||||
|
- pattern-not: |
|
||||||
|
class $DOCTYPE(...):
|
||||||
|
def $METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ANOTHER_METHOD()
|
||||||
|
...
|
||||||
|
|
||||||
|
def $ANOTHER_METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ATTR = ...
|
||||||
|
...
|
||||||
|
self.db_set(..., self.$ATTR, ...)
|
||||||
|
- pattern-not: |
|
||||||
|
class $DOCTYPE(...):
|
||||||
|
def $METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ANOTHER_METHOD()
|
||||||
|
...
|
||||||
|
|
||||||
|
def $ANOTHER_METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ATTR = $SOME_VAR
|
||||||
|
...
|
||||||
|
self.db_set(..., $SOME_VAR, ...)
|
||||||
|
- pattern-not: |
|
||||||
|
class $DOCTYPE(...):
|
||||||
|
def $METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ANOTHER_METHOD()
|
||||||
|
...
|
||||||
|
self.save()
|
||||||
|
def $ANOTHER_METHOD(self, ...):
|
||||||
|
...
|
||||||
|
self.$ATTR = ...
|
||||||
- metavariable-regex:
|
- metavariable-regex:
|
||||||
metavariable: '$ATTR'
|
metavariable: "$METHOD"
|
||||||
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
|
regex: "(on_submit|on_cancel)"
|
||||||
message: |
|
message: |
|
||||||
Doctype modified after cancellation. Please check if modification of self.$ATTR is commited to database.
|
self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
|
||||||
languages: [python]
|
languages: [python]
|
||||||
severity: ERROR
|
severity: ERROR
|
||||||
|
|
||||||
|
7
.github/helper/semgrep_rules/translate.js
vendored
7
.github/helper/semgrep_rules/translate.js
vendored
@ -35,3 +35,10 @@ __('You have' + 'subscribers in your mailing list.')
|
|||||||
// ruleid: frappe-translation-js-splitting
|
// ruleid: frappe-translation-js-splitting
|
||||||
__('You have {0} subscribers' +
|
__('You have {0} subscribers' +
|
||||||
'in your mailing list', [subscribers.length])
|
'in your mailing list', [subscribers.length])
|
||||||
|
|
||||||
|
// ok: frappe-translation-js-splitting
|
||||||
|
__("Ctrl+Enter to add comment")
|
||||||
|
|
||||||
|
// ruleid: frappe-translation-js-splitting
|
||||||
|
__('You have {0} subscribers \
|
||||||
|
in your mailing list', [subscribers.length])
|
||||||
|
8
.github/helper/semgrep_rules/translate.py
vendored
8
.github/helper/semgrep_rules/translate.py
vendored
@ -51,3 +51,11 @@ _(f"what" + f"this is also not cool")
|
|||||||
_("")
|
_("")
|
||||||
# ruleid: frappe-translation-empty-string
|
# ruleid: frappe-translation-empty-string
|
||||||
_('')
|
_('')
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
# ok: frappe-translation-python-splitting
|
||||||
|
def __init__(
|
||||||
|
args
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
9
.github/helper/semgrep_rules/translate.yml
vendored
9
.github/helper/semgrep_rules/translate.yml
vendored
@ -42,9 +42,10 @@ rules:
|
|||||||
|
|
||||||
- id: frappe-translation-python-splitting
|
- id: frappe-translation-python-splitting
|
||||||
pattern-either:
|
pattern-either:
|
||||||
- pattern: _(...) + ... + _(...)
|
- pattern: _(...) + _(...)
|
||||||
- pattern: _("..." + "...")
|
- pattern: _("..." + "...")
|
||||||
- pattern-regex: '_\([^\)]*\\\s*'
|
- pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
|
||||||
|
- pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
|
||||||
message: |
|
message: |
|
||||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||||
@ -53,8 +54,8 @@ rules:
|
|||||||
|
|
||||||
- id: frappe-translation-js-splitting
|
- id: frappe-translation-js-splitting
|
||||||
pattern-either:
|
pattern-either:
|
||||||
- pattern-regex: '__\([^\)]*[\+\\]\s*'
|
- pattern-regex: '__\([^\)]*[\\]\s+'
|
||||||
- pattern: __('...' + '...')
|
- pattern: __('...' + '...', ...)
|
||||||
- pattern: __('...') + __('...')
|
- pattern: __('...') + __('...')
|
||||||
message: |
|
message: |
|
||||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||||
|
9
.github/helper/semgrep_rules/ux.js
vendored
Normal file
9
.github/helper/semgrep_rules/ux.js
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
// ok: frappe-missing-translate-function-js
|
||||||
|
frappe.msgprint('{{ _("Both login and password required") }}');
|
||||||
|
|
||||||
|
// ruleid: frappe-missing-translate-function-js
|
||||||
|
frappe.msgprint('What');
|
||||||
|
|
||||||
|
// ok: frappe-missing-translate-function-js
|
||||||
|
frappe.throw(' {{ _("Both login and password required") }}. ');
|
18
.github/helper/semgrep_rules/ux.py
vendored
18
.github/helper/semgrep_rules/ux.py
vendored
@ -2,30 +2,30 @@ import frappe
|
|||||||
from frappe import msgprint, throw, _
|
from frappe import msgprint, throw, _
|
||||||
|
|
||||||
|
|
||||||
# ruleid: frappe-missing-translate-function
|
# ruleid: frappe-missing-translate-function-python
|
||||||
throw("Error Occured")
|
throw("Error Occured")
|
||||||
|
|
||||||
# ruleid: frappe-missing-translate-function
|
# ruleid: frappe-missing-translate-function-python
|
||||||
frappe.throw("Error Occured")
|
frappe.throw("Error Occured")
|
||||||
|
|
||||||
# ruleid: frappe-missing-translate-function
|
# ruleid: frappe-missing-translate-function-python
|
||||||
frappe.msgprint("Useful message")
|
frappe.msgprint("Useful message")
|
||||||
|
|
||||||
# ruleid: frappe-missing-translate-function
|
# ruleid: frappe-missing-translate-function-python
|
||||||
msgprint("Useful message")
|
msgprint("Useful message")
|
||||||
|
|
||||||
|
|
||||||
# ok: frappe-missing-translate-function
|
# ok: frappe-missing-translate-function-python
|
||||||
translatedmessage = _("Hello")
|
translatedmessage = _("Hello")
|
||||||
|
|
||||||
# ok: frappe-missing-translate-function
|
# ok: frappe-missing-translate-function-python
|
||||||
throw(translatedmessage)
|
throw(translatedmessage)
|
||||||
|
|
||||||
# ok: frappe-missing-translate-function
|
# ok: frappe-missing-translate-function-python
|
||||||
msgprint(translatedmessage)
|
msgprint(translatedmessage)
|
||||||
|
|
||||||
# ok: frappe-missing-translate-function
|
# ok: frappe-missing-translate-function-python
|
||||||
msgprint(_("Helpful message"))
|
msgprint(_("Helpful message"))
|
||||||
|
|
||||||
# ok: frappe-missing-translate-function
|
# ok: frappe-missing-translate-function-python
|
||||||
frappe.throw(_("Error occured"))
|
frappe.throw(_("Error occured"))
|
||||||
|
23
.github/helper/semgrep_rules/ux.yml
vendored
23
.github/helper/semgrep_rules/ux.yml
vendored
@ -1,15 +1,30 @@
|
|||||||
rules:
|
rules:
|
||||||
- id: frappe-missing-translate-function
|
- id: frappe-missing-translate-function-python
|
||||||
pattern-either:
|
pattern-either:
|
||||||
- patterns:
|
- patterns:
|
||||||
- pattern: frappe.msgprint("...", ...)
|
- pattern: frappe.msgprint("...", ...)
|
||||||
- pattern-not: frappe.msgprint(_("..."), ...)
|
- pattern-not: frappe.msgprint(_("..."), ...)
|
||||||
- pattern-not: frappe.msgprint(__("..."), ...)
|
|
||||||
- patterns:
|
- patterns:
|
||||||
- pattern: frappe.throw("...", ...)
|
- pattern: frappe.throw("...", ...)
|
||||||
- pattern-not: frappe.throw(_("..."), ...)
|
- pattern-not: frappe.throw(_("..."), ...)
|
||||||
- pattern-not: frappe.throw(__("..."), ...)
|
|
||||||
message: |
|
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
|
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]
|
languages: [python]
|
||||||
|
severity: ERROR
|
||||||
|
|
||||||
|
- id: frappe-missing-translate-function-js
|
||||||
|
pattern-either:
|
||||||
|
- patterns:
|
||||||
|
- pattern: frappe.msgprint("...", ...)
|
||||||
|
- pattern-not: frappe.msgprint(__("..."), ...)
|
||||||
|
# ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
|
||||||
|
- pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
|
||||||
|
- patterns:
|
||||||
|
- pattern: frappe.throw("...", ...)
|
||||||
|
- pattern-not: frappe.throw(__("..."), ...)
|
||||||
|
# ignore microtemplating
|
||||||
|
- pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
|
||||||
|
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: [javascript]
|
||||||
severity: ERROR
|
severity: ERROR
|
||||||
|
69
.github/workflows/patch.yml
vendored
Normal file
69
.github/workflows/patch.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: Patch
|
||||||
|
|
||||||
|
on: [pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
|
||||||
|
name: Patch Test
|
||||||
|
|
||||||
|
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 Patch Tests
|
||||||
|
run: 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
|
12
.github/workflows/semgrep.yml
vendored
12
.github/workflows/semgrep.yml
vendored
@ -4,6 +4,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
|
- version-13-hotfix
|
||||||
|
- version-13-pre-release
|
||||||
jobs:
|
jobs:
|
||||||
semgrep:
|
semgrep:
|
||||||
name: Frappe Linter
|
name: Frappe Linter
|
||||||
@ -14,11 +16,19 @@ jobs:
|
|||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
- name: Run semgrep
|
|
||||||
|
- name: Setup semgrep
|
||||||
run: |
|
run: |
|
||||||
python -m pip install -q semgrep
|
python -m pip install -q semgrep
|
||||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||||
|
|
||||||
|
- name: Semgrep errors
|
||||||
|
run: |
|
||||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
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
|
[[ -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
|
semgrep --config="r/python.lang.correctness" --quiet --error $files
|
||||||
|
|
||||||
|
- name: Semgrep warnings
|
||||||
|
run: |
|
||||||
|
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
|
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
name: CI
|
name: Server
|
||||||
|
|
||||||
on: [pull_request, workflow_dispatch, push]
|
on: [pull_request, workflow_dispatch]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@ -10,15 +10,9 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
container: [1, 2, 3]
|
||||||
- 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 }}
|
name: Python Unit Tests
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
@ -36,7 +30,7 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.6
|
python-version: 3.7
|
||||||
|
|
||||||
- name: Add to Hosts
|
- name: Add to Hosts
|
||||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||||
@ -49,6 +43,7 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-
|
${{ runner.os }}-pip-
|
||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
env:
|
env:
|
||||||
@ -60,6 +55,7 @@ jobs:
|
|||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
${{ runner.os }}-build-
|
${{ runner.os }}-build-
|
||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
- name: Get yarn cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
@ -76,33 +72,39 @@ jobs:
|
|||||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: ${{ matrix.RUN_COMMAND }}
|
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
|
||||||
env:
|
env:
|
||||||
TYPE: ${{ matrix.TYPE }}
|
TYPE: server
|
||||||
|
CI_BUILD_ID: ${{ github.run_id }}
|
||||||
|
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||||
|
|
||||||
- name: Coverage - Pull Request
|
- name: Upload Coverage Data
|
||||||
if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
|
|
||||||
run: |
|
run: |
|
||||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||||
cd ${GITHUB_WORKSPACE}
|
cd ${GITHUB_WORKSPACE}
|
||||||
pip install coveralls==2.2.0
|
pip3 install coverage==5.5
|
||||||
pip install coverage==4.5.4
|
pip3 install coveralls==3.0.1
|
||||||
coveralls --service=github
|
coveralls
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
|
||||||
COVERALLS_SERVICE_NAME: github
|
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
|
||||||
|
COVERALLS_PARALLEL: true
|
||||||
|
|
||||||
- name: Coverage - Push
|
coveralls:
|
||||||
if: matrix.TYPE == 'server' && github.event_name == 'push'
|
name: Coverage Wrap Up
|
||||||
|
needs: test
|
||||||
|
container: python:3-slim
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
steps:
|
||||||
|
- name: Clone
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Coveralls Finished
|
||||||
run: |
|
run: |
|
||||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
|
||||||
cd ${GITHUB_WORKSPACE}
|
cd ${GITHUB_WORKSPACE}
|
||||||
pip install coveralls==2.2.0
|
pip3 install coverage==5.5
|
||||||
pip install coverage==4.5.4
|
pip3 install coveralls==3.0.1
|
||||||
coveralls --service=github-actions
|
coveralls --finish
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
|
||||||
COVERALLS_SERVICE_NAME: github-actions
|
|
||||||
|
|
@ -41,7 +41,7 @@ def build_conditions(process_type, account, company):
|
|||||||
if account:
|
if account:
|
||||||
conditions += "AND %s='%s'"%(deferred_account, account)
|
conditions += "AND %s='%s'"%(deferred_account, account)
|
||||||
elif company:
|
elif company:
|
||||||
conditions += "AND p.company='%s'"%(company)
|
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
@ -360,12 +360,10 @@ def make_gl_entries(doc, credit_account, debit_account, against,
|
|||||||
frappe.flags.deferred_accounting_error = True
|
frappe.flags.deferred_accounting_error = True
|
||||||
|
|
||||||
def send_mail(deferred_process):
|
def send_mail(deferred_process):
|
||||||
title = _("Error while processing deferred accounting for {0}".format(deferred_process))
|
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
|
||||||
content = _("""
|
link = get_link_to_form('Process Deferred Accounting', deferred_process)
|
||||||
Deferred accounting failed for some invoices:
|
content = _("Deferred accounting failed for some invoices:") + "\n"
|
||||||
Please check Process Deferred Accounting {0}
|
content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
|
||||||
and submit manually after resolving errors
|
|
||||||
""").format(get_link_to_form('Process Deferred Accounting', deferred_process))
|
|
||||||
sendmail_to_system_managers(title, content)
|
sendmail_to_system_managers(title, content)
|
||||||
|
|
||||||
def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||||
|
@ -13,7 +13,7 @@ class BalanceMismatchError(frappe.ValidationError): pass
|
|||||||
class Account(NestedSet):
|
class Account(NestedSet):
|
||||||
nsm_parent_field = 'parent_account'
|
nsm_parent_field = 'parent_account'
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if frappe.local.flags.ignore_on_update:
|
if frappe.local.flags.ignore_update_nsm:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
super(Account, self).on_update()
|
super(Account, self).on_update()
|
||||||
|
@ -57,10 +57,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
|
|||||||
|
|
||||||
# Rebuild NestedSet HSM tree for Account Doctype
|
# Rebuild NestedSet HSM tree for Account Doctype
|
||||||
# after all accounts are already inserted.
|
# 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)
|
_import_accounts(chart, None, None, root_account=True)
|
||||||
rebuild_tree("Account", "parent_account")
|
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):
|
def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||||
if account_number:
|
if account_number:
|
||||||
|
@ -27,7 +27,7 @@ class AccountingDimension(Document):
|
|||||||
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
|
exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
|
||||||
|
|
||||||
if exists and self.is_new():
|
if exists and self.is_new():
|
||||||
frappe.throw("Document Type already used as a dimension")
|
frappe.throw(_("Document Type already used as a dimension"))
|
||||||
|
|
||||||
if not self.is_new():
|
if not self.is_new():
|
||||||
self.validate_document_type_change()
|
self.validate_document_type_change()
|
||||||
|
@ -7,7 +7,8 @@ import frappe
|
|||||||
import unittest
|
import unittest
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import delete_accounting_dimension
|
|
||||||
|
test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department']
|
||||||
|
|
||||||
class TestAccountingDimension(unittest.TestCase):
|
class TestAccountingDimension(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -9,6 +9,8 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
|
|||||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
|
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
|
||||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||||
|
|
||||||
|
test_dependencies = ['Location', 'Cost Center', 'Department']
|
||||||
|
|
||||||
class TestAccountingDimensionFilter(unittest.TestCase):
|
class TestAccountingDimensionFilter(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
create_dimension()
|
create_dimension()
|
||||||
|
@ -10,6 +10,8 @@ from erpnext.accounts.general_ledger import ClosedAccountingPeriod
|
|||||||
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
|
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
|
||||||
|
test_dependencies = ['Item']
|
||||||
|
|
||||||
class TestAccountingPeriod(unittest.TestCase):
|
class TestAccountingPeriod(unittest.TestCase):
|
||||||
def test_overlap(self):
|
def test_overlap(self):
|
||||||
ap1 = create_accounting_period(start_date = "2018-04-01",
|
ap1 = create_accounting_period(start_date = "2018-04-01",
|
||||||
@ -38,7 +40,7 @@ def create_accounting_period(**args):
|
|||||||
accounting_period.start_date = args.start_date or nowdate()
|
accounting_period.start_date = args.start_date or nowdate()
|
||||||
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
|
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
|
||||||
accounting_period.company = args.company or "_Test Company"
|
accounting_period.company = args.company or "_Test Company"
|
||||||
accounting_period.period_name =args.period_name or "_Test_Period_Name_1"
|
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
|
||||||
accounting_period.append("closed_documents", {
|
accounting_period.append("closed_documents", {
|
||||||
"document_type": 'Sales Invoice', "closed": 1
|
"document_type": 'Sales Invoice', "closed": 1
|
||||||
})
|
})
|
||||||
|
@ -7,26 +7,30 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"auto_accounting_for_stock",
|
"accounts_transactions_settings_section",
|
||||||
"acc_frozen_upto",
|
|
||||||
"frozen_accounts_modifier",
|
|
||||||
"determine_address_tax_category_from",
|
|
||||||
"over_billing_allowance",
|
"over_billing_allowance",
|
||||||
"role_allowed_to_over_bill",
|
"role_allowed_to_over_bill",
|
||||||
"column_break_4",
|
|
||||||
"credit_controller",
|
|
||||||
"check_supplier_invoice_uniqueness",
|
|
||||||
"make_payment_via_journal_entry",
|
"make_payment_via_journal_entry",
|
||||||
|
"column_break_11",
|
||||||
|
"check_supplier_invoice_uniqueness",
|
||||||
"unlink_payment_on_cancellation_of_invoice",
|
"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",
|
"automatically_fetch_payment_terms",
|
||||||
"delete_linked_ledger_entries",
|
"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",
|
"deferred_accounting_settings_section",
|
||||||
"automatically_process_deferred_accounting_entry",
|
|
||||||
"book_deferred_entries_based_on",
|
"book_deferred_entries_based_on",
|
||||||
"column_break_18",
|
"column_break_18",
|
||||||
|
"automatically_process_deferred_accounting_entry",
|
||||||
"book_deferred_entries_via_journal_entry",
|
"book_deferred_entries_via_journal_entry",
|
||||||
"submit_journal_entries",
|
"submit_journal_entries",
|
||||||
"print_settings",
|
"print_settings",
|
||||||
@ -40,15 +44,6 @@
|
|||||||
"use_custom_cash_flow"
|
"use_custom_cash_flow"
|
||||||
],
|
],
|
||||||
"fields": [
|
"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",
|
"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",
|
"fieldname": "acc_frozen_upto",
|
||||||
@ -94,6 +89,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "make_payment_via_journal_entry",
|
"fieldname": "make_payment_via_journal_entry",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Make Payment via Journal Entry"
|
"label": "Make Payment via Journal Entry"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -234,6 +230,29 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Role Allowed to Over Bill ",
|
"label": "Role Allowed to Over Bill ",
|
||||||
"options": "Role"
|
"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",
|
"icon": "icon-cog",
|
||||||
@ -241,7 +260,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-11 18:52:05.601996",
|
"modified": "2021-04-30 15:25:10.381008",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
@ -24,11 +25,11 @@ class AccountsSettings(Document):
|
|||||||
def validate_stale_days(self):
|
def validate_stale_days(self):
|
||||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
"Stale Days should start from 1.", title='Error', indicator='red',
|
_("Stale Days should start from 1."), title='Error', indicator='red',
|
||||||
raise_exception=1)
|
raise_exception=1)
|
||||||
|
|
||||||
def enable_payment_schedule_in_print(self):
|
def enable_payment_schedule_in_print(self):
|
||||||
show_in_print = cint(self.show_payment_schedule_in_print)
|
show_in_print = cint(self.show_payment_schedule_in_print)
|
||||||
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
|
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, "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")
|
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False)
|
||||||
|
@ -239,6 +239,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
|||||||
"withdrawal",
|
"withdrawal",
|
||||||
"description",
|
"description",
|
||||||
"reference_number",
|
"reference_number",
|
||||||
|
"bank_account"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -146,7 +146,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.__islocal && !doc.import_file\n",
|
"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",
|
"fieldname": "google_sheets_url",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Import from Google Sheets"
|
"label": "Import from Google Sheets"
|
||||||
@ -202,7 +202,7 @@
|
|||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-02-10 19:29:59.027325",
|
"modified": "2021-05-12 14:17:37.777246",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Statement Import",
|
"name": "Bank Statement Import",
|
||||||
|
@ -47,6 +47,13 @@ class BankStatementImport(DataImport):
|
|||||||
|
|
||||||
def start_import(self):
|
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.core.page.background_jobs.background_jobs import get_info
|
||||||
from frappe.utils.scheduler import is_scheduler_inactive
|
from frappe.utils.scheduler import is_scheduler_inactive
|
||||||
|
|
||||||
@ -67,6 +74,7 @@ class BankStatementImport(DataImport):
|
|||||||
data_import=self.name,
|
data_import=self.name,
|
||||||
bank_account=self.bank_account,
|
bank_account=self.bank_account,
|
||||||
import_file_path=self.import_file,
|
import_file_path=self.import_file,
|
||||||
|
google_sheets_url=self.google_sheets_url,
|
||||||
bank=self.bank,
|
bank=self.bank,
|
||||||
template_options=self.template_options,
|
template_options=self.template_options,
|
||||||
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
now=frappe.conf.developer_mode or frappe.flags.in_test,
|
||||||
@ -90,16 +98,18 @@ def download_errored_template(data_import_name):
|
|||||||
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||||
data_import.export_errored_rows()
|
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"""
|
"""This method runs in background job"""
|
||||||
|
|
||||||
update_mapping_db(bank, template_options)
|
update_mapping_db(bank, template_options)
|
||||||
|
|
||||||
data_import = frappe.get_doc("Bank Statement Import", data_import)
|
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
|
data = import_file.raw_data
|
||||||
|
|
||||||
|
if import_file_path:
|
||||||
add_bank_account(data, bank_account)
|
add_bank_account(data, bank_account)
|
||||||
write_files(import_file, data)
|
write_files(import_file, data)
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_pur
|
|||||||
from erpnext.accounts.doctype.budget.budget import get_actual_expense, BudgetError
|
from erpnext.accounts.doctype.budget.budget import get_actual_expense, BudgetError
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
|
|
||||||
|
test_dependencies = ['Monthly Distribution']
|
||||||
|
|
||||||
class TestBudget(unittest.TestCase):
|
class TestBudget(unittest.TestCase):
|
||||||
def test_monthly_budget_crossed_ignore(self):
|
def test_monthly_budget_crossed_ignore(self):
|
||||||
set_total_expense_zero(nowdate(), "cost_center")
|
set_total_expense_zero(nowdate(), "cost_center")
|
||||||
|
@ -22,7 +22,7 @@ def validate_company(company):
|
|||||||
'allow_account_creation_against_child_company'])
|
'allow_account_creation_against_child_company'])
|
||||||
|
|
||||||
if parent_company and (not allow_account_creation_against_child_company):
|
if parent_company and (not allow_account_creation_against_child_company):
|
||||||
msg = _("{} is a child company. ").format(frappe.bold(company))
|
msg = _("{} is a child company.").format(frappe.bold(company)) + " "
|
||||||
msg += _("Please import accounts against parent company or enable {} in company master.").format(
|
msg += _("Please import accounts against parent company or enable {} in company master.").format(
|
||||||
frappe.bold('Allow Account Creation Against Child Company'))
|
frappe.bold('Allow Account Creation Against Child Company'))
|
||||||
frappe.throw(msg, title=_('Wrong Company'))
|
frappe.throw(msg, title=_('Wrong Company'))
|
||||||
@ -56,7 +56,7 @@ def get_file(file_name):
|
|||||||
extension = extension.lstrip(".")
|
extension = extension.lstrip(".")
|
||||||
|
|
||||||
if extension not in ('csv', 'xlsx', 'xls'):
|
if extension not in ('csv', 'xlsx', 'xls'):
|
||||||
frappe.throw("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload")
|
frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"))
|
||||||
|
|
||||||
return file_doc, extension
|
return file_doc, extension
|
||||||
|
|
||||||
@ -293,7 +293,7 @@ def validate_accounts(file_name):
|
|||||||
accounts_dict = {}
|
accounts_dict = {}
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
accounts_dict.setdefault(account["account_name"], account)
|
accounts_dict.setdefault(account["account_name"], account)
|
||||||
if not hasattr(account, "parent_account"):
|
if "parent_account" not in account:
|
||||||
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
|
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
|
||||||
msg += "<br><br>"
|
msg += "<br><br>"
|
||||||
msg += _("Alternatively, you can download the template and fill your data in.")
|
msg += _("Alternatively, you can download the template and fill your data in.")
|
||||||
|
@ -42,9 +42,9 @@ class TestDunning(unittest.TestCase):
|
|||||||
['Sales - _TC', 0.0, 20.44]
|
['Sales - _TC', 0.0, 20.44]
|
||||||
])
|
])
|
||||||
for gle in gl_entries:
|
for gle in gl_entries:
|
||||||
self.assertEquals(expected_values[gle.account][0], gle.account)
|
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||||
self.assertEquals(expected_values[gle.account][1], gle.debit)
|
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||||
self.assertEquals(expected_values[gle.account][2], gle.credit)
|
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||||
|
|
||||||
def test_payment_entry(self):
|
def test_payment_entry(self):
|
||||||
dunning = create_dunning()
|
dunning = create_dunning()
|
||||||
|
@ -75,8 +75,13 @@ class GLEntry(Document):
|
|||||||
def pl_must_have_cost_center(self):
|
def pl_must_have_cost_center(self):
|
||||||
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
|
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
|
||||||
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
|
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
|
||||||
frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.")
|
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
|
||||||
.format(self.voucher_type, self.voucher_no, self.account))
|
self.voucher_type, self.voucher_no, self.account)
|
||||||
|
msg += " "
|
||||||
|
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
|
||||||
|
self.voucher_type)
|
||||||
|
|
||||||
|
frappe.throw(msg, title=_("Missing Cost Center"))
|
||||||
|
|
||||||
def validate_dimensions_for_pl_and_bs(self):
|
def validate_dimensions_for_pl_and_bs(self):
|
||||||
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
||||||
|
@ -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)))
|
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]
|
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)
|
||||||
|
@ -1,196 +1,82 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-01-02 15:48:58.768352",
|
"creation": "2018-01-02 15:48:58.768352",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company",
|
||||||
|
"cgst_account",
|
||||||
|
"sgst_account",
|
||||||
|
"igst_account",
|
||||||
|
"cess_account",
|
||||||
|
"is_reverse_charge_account"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 1,
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 2,
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "cgst_account",
|
"fieldname": "cgst_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "CGST Account",
|
"label": "CGST Account",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 2,
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "sgst_account",
|
"fieldname": "sgst_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "SGST Account",
|
"label": "SGST Account",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 2,
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "igst_account",
|
"fieldname": "igst_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "IGST Account",
|
"label": "IGST Account",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 2,
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "cess_account",
|
"fieldname": "cess_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "CESS Account",
|
"label": "CESS Account",
|
||||||
"length": 0,
|
"options": "Account"
|
||||||
"no_copy": 0,
|
},
|
||||||
"options": "Account",
|
{
|
||||||
"permlevel": 0,
|
"columns": 1,
|
||||||
"precision": "",
|
"default": "0",
|
||||||
"print_hide": 0,
|
"fieldname": "is_reverse_charge_account",
|
||||||
"print_hide_if_no_value": 0,
|
"fieldtype": "Check",
|
||||||
"read_only": 0,
|
"in_list_view": 1,
|
||||||
"remember_last_selected_value": 0,
|
"label": "Is Reverse Charge Account"
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-01-02 15:52:22.335988",
|
"modified": "2021-04-09 12:30:25.889993",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "GST Account",
|
"name": "GST Account",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
@ -39,7 +39,11 @@ class JournalEntry(AccountsController):
|
|||||||
self.validate_multi_currency()
|
self.validate_multi_currency()
|
||||||
self.set_amounts_in_company_currency()
|
self.set_amounts_in_company_currency()
|
||||||
self.validate_debit_credit_amount()
|
self.validate_debit_credit_amount()
|
||||||
|
|
||||||
|
# Do not validate while importing via data import
|
||||||
|
if not frappe.flags.in_import:
|
||||||
self.validate_total_debit_and_credit()
|
self.validate_total_debit_and_credit()
|
||||||
|
|
||||||
self.validate_against_jv()
|
self.validate_against_jv()
|
||||||
self.validate_reference_doc()
|
self.validate_reference_doc()
|
||||||
self.set_against_account()
|
self.set_against_account()
|
||||||
|
17
erpnext/accounts/doctype/journal_entry/regional/india.js
Normal file
17
erpnext/accounts/doctype/journal_entry/regional/india.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
frappe.ui.form.on("Journal Entry", {
|
||||||
|
refresh: function(frm) {
|
||||||
|
frm.set_query('company_address', function(doc) {
|
||||||
|
if(!doc.company) {
|
||||||
|
frappe.throw(__('Please set Company'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: 'frappe.contacts.doctype.address.address.address_query',
|
||||||
|
filters: {
|
||||||
|
link_doctype: 'Company',
|
||||||
|
link_name: doc.company
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -31,10 +31,10 @@ class TestPaymentOrder(unittest.TestCase):
|
|||||||
|
|
||||||
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
|
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
|
||||||
reference_doc = doc.get("references")[0]
|
reference_doc = doc.get("references")[0]
|
||||||
self.assertEquals(reference_doc.reference_name, payment_entry.name)
|
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
||||||
self.assertEquals(reference_doc.reference_doctype, "Payment Entry")
|
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
||||||
self.assertEquals(reference_doc.supplier, "_Test Supplier")
|
self.assertEqual(reference_doc.supplier, "_Test Supplier")
|
||||||
self.assertEquals(reference_doc.amount, 250)
|
self.assertEqual(reference_doc.amount, 250)
|
||||||
|
|
||||||
def create_payment_order_against_payment_entry(ref_doc, order_type):
|
def create_payment_order_against_payment_entry(ref_doc, order_type):
|
||||||
payment_order = frappe.get_doc(dict(
|
payment_order = frappe.get_doc(dict(
|
||||||
|
@ -114,7 +114,7 @@ class PaymentReconciliation(Document):
|
|||||||
'party_type': self.party_type,
|
'party_type': self.party_type,
|
||||||
'voucher_type': voucher_type,
|
'voucher_type': voucher_type,
|
||||||
'account': self.receivable_payable_account
|
'account': self.receivable_payable_account
|
||||||
}, as_dict=1, debug=1)
|
}, as_dict=1)
|
||||||
|
|
||||||
def add_payment_entries(self, entries):
|
def add_payment_entries(self, entries):
|
||||||
self.set('payments', [])
|
self.set('payments', [])
|
||||||
|
@ -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 === 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) {
|
pos_opening_entry(frm) {
|
||||||
@ -61,33 +97,21 @@ frappe.ui.form.on('POS Closing Entry', {
|
|||||||
refresh_fields(frm);
|
refresh_fields(frm);
|
||||||
set_html_data(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', {
|
before_save: function(frm) {
|
||||||
pos_invoice(frm, cdt, cdn) {
|
frm.set_value("grand_total", 0);
|
||||||
const added_row = locals[cdt][cdn];
|
frm.set_value("net_total", 0);
|
||||||
|
frm.set_value("total_quantity", 0);
|
||||||
|
frm.set_value("taxes", []);
|
||||||
|
|
||||||
if (!added_row.pos_invoice) return;
|
for (let row of frm.doc.payment_reconciliation) {
|
||||||
|
row.expected_amount = row.opening_amount;
|
||||||
|
}
|
||||||
|
|
||||||
frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => {
|
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.grand_total += flt(doc.grand_total);
|
||||||
frm.doc.net_total += flt(doc.net_total);
|
frm.doc.net_total += flt(doc.net_total);
|
||||||
frm.doc.total_quantity += flt(doc.total_qty);
|
frm.doc.total_quantity += flt(doc.total_qty);
|
||||||
@ -97,12 +121,13 @@ frappe.ui.form.on('POS Invoice Reference', {
|
|||||||
set_html_data(frm);
|
set_html_data(frm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
});
|
||||||
|
|
||||||
frappe.ui.form.on('POS Closing Entry Detail', {
|
frappe.ui.form.on('POS Closing Entry Detail', {
|
||||||
closing_amount: (frm, cdt, cdn) => {
|
closing_amount: (frm, cdt, cdn) => {
|
||||||
const row = locals[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,31 @@ function add_to_pos_transaction(d, frm) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh_payments(d, frm, remove) {
|
function refresh_payments(d, frm) {
|
||||||
d.payments.forEach(p => {
|
d.payments.forEach(p => {
|
||||||
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
|
const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment);
|
||||||
|
if (p.account == d.account_for_change_amount) {
|
||||||
|
p.amount -= flt(d.change_amount);
|
||||||
|
}
|
||||||
if (payment) {
|
if (payment) {
|
||||||
if (!remove) payment.expected_amount += flt(p.amount);
|
payment.expected_amount += flt(p.amount);
|
||||||
else payment.expected_amount -= flt(p.amount);
|
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||||
} else {
|
} else {
|
||||||
frm.add_child("payment_reconciliation", {
|
frm.add_child("payment_reconciliation", {
|
||||||
mode_of_payment: p.mode_of_payment,
|
mode_of_payment: p.mode_of_payment,
|
||||||
opening_amount: 0,
|
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 => {
|
d.taxes.forEach(t => {
|
||||||
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
|
const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate);
|
||||||
if (tax) {
|
if (tax) {
|
||||||
if (!remove) tax.amount += flt(t.tax_amount);
|
tax.amount += flt(t.tax_amount);
|
||||||
else tax.amount -= flt(t.tax_amount);
|
|
||||||
} else {
|
} else {
|
||||||
frm.add_child("taxes", {
|
frm.add_child("taxes", {
|
||||||
account_head: t.account_head,
|
account_head: t.account_head,
|
||||||
@ -177,11 +205,13 @@ function refresh_fields(frm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function set_html_data(frm) {
|
function set_html_data(frm) {
|
||||||
|
if (frm.doc.docstatus === 1 && frm.doc.status == 'Submitted') {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_payment_reconciliation_details",
|
method: "get_payment_reconciliation_details",
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
|
frm.get_field("payment_reconciliation_details").$wrapper.html(r.message);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,8 @@
|
|||||||
"total_quantity",
|
"total_quantity",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"taxes",
|
"taxes",
|
||||||
|
"failure_description_section",
|
||||||
|
"error_message",
|
||||||
"section_break_14",
|
"section_break_14",
|
||||||
"amended_from"
|
"amended_from"
|
||||||
],
|
],
|
||||||
@ -195,7 +197,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"options": "Draft\nSubmitted\nQueued\nCancelled",
|
"options": "Draft\nSubmitted\nQueued\nFailed\nCancelled",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@ -203,6 +205,21 @@
|
|||||||
"fieldname": "period_details_section",
|
"fieldname": "period_details_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Period Details"
|
"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,
|
"is_submittable": 1,
|
||||||
@ -212,7 +229,7 @@
|
|||||||
"link_fieldname": "pos_closing_entry"
|
"link_fieldname": "pos_closing_entry"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-02-01 13:47:20.722104",
|
"modified": "2021-05-05 16:59:49.723261",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Closing Entry",
|
"name": "POS Closing Entry",
|
||||||
|
@ -60,6 +60,10 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
unconsolidate_pos_invoices(closing_entry=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):
|
def update_opening_entry(self, for_cancel=False):
|
||||||
opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
|
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
|
opening_entry.pos_closing_entry = self.name if not for_cancel else None
|
||||||
|
@ -8,6 +8,7 @@ frappe.listview_settings['POS Closing Entry'] = {
|
|||||||
"Draft": "red",
|
"Draft": "red",
|
||||||
"Submitted": "blue",
|
"Submitted": "blue",
|
||||||
"Queued": "orange",
|
"Queued": "orange",
|
||||||
|
"Failed": "red",
|
||||||
"Cancelled": "red"
|
"Cancelled": "red"
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "0",
|
||||||
"fieldname": "closing_amount",
|
"fieldname": "closing_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -57,7 +58,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-23 16:45:43.662034",
|
"modified": "2021-05-19 20:08:44.523861",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Closing Entry Detail",
|
"name": "POS Closing Entry Detail",
|
||||||
|
@ -140,6 +140,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
return
|
return
|
||||||
|
|
||||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
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)
|
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||||
if flt(available_stock) <= 0:
|
if flt(available_stock) <= 0:
|
||||||
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
|
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
|
||||||
@ -213,7 +214,8 @@ class POSInvoice(SalesInvoice):
|
|||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
|
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
|
||||||
if not 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. ")
|
if not frappe.db.exists('Product Bundle', d.item_code):
|
||||||
|
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
|
||||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||||
|
|
||||||
def validate_mode_of_payment(self):
|
def validate_mode_of_payment(self):
|
||||||
@ -455,29 +457,48 @@ class POSInvoice(SalesInvoice):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_availability(item_code, warehouse):
|
def get_stock_availability(item_code, warehouse):
|
||||||
latest_sle = frappe.db.sql("""select qty_after_transaction
|
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
||||||
from `tabStock Ledger Entry`
|
bin_qty = get_bin_qty(item_code, warehouse)
|
||||||
|
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||||
|
return bin_qty - pos_sales_qty
|
||||||
|
else:
|
||||||
|
if frappe.db.exists('Product Bundle', item_code):
|
||||||
|
return get_bundle_availability(item_code, warehouse)
|
||||||
|
|
||||||
|
def get_bundle_availability(bundle_item_code, warehouse):
|
||||||
|
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
|
||||||
|
|
||||||
|
bundle_bin_qty = 1000000
|
||||||
|
for item in product_bundle.items:
|
||||||
|
item_bin_qty = get_bin_qty(item.item_code, warehouse)
|
||||||
|
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
|
||||||
|
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||||
|
|
||||||
|
max_available_bundles = available_qty / item.qty
|
||||||
|
if bundle_bin_qty > max_available_bundles:
|
||||||
|
bundle_bin_qty = max_available_bundles
|
||||||
|
|
||||||
|
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
|
||||||
|
return bundle_bin_qty - pos_sales_qty
|
||||||
|
|
||||||
|
def get_bin_qty(item_code, warehouse):
|
||||||
|
bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
|
||||||
where item_code = %s and warehouse = %s
|
where item_code = %s and warehouse = %s
|
||||||
order by posting_date desc, posting_time desc
|
|
||||||
limit 1""", (item_code, warehouse), as_dict=1)
|
limit 1""", (item_code, warehouse), as_dict=1)
|
||||||
|
|
||||||
pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty
|
return bin_qty[0].actual_qty or 0 if bin_qty else 0
|
||||||
|
|
||||||
|
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
|
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
||||||
where p.name = p_item.parent
|
where p.name = p_item.parent
|
||||||
and p.consolidated_invoice is NULL
|
and ifnull(p.consolidated_invoice, '') = ''
|
||||||
and p.docstatus = 1
|
|
||||||
and p_item.docstatus = 1
|
and p_item.docstatus = 1
|
||||||
and p_item.item_code = %s
|
and p_item.item_code = %s
|
||||||
and p_item.warehouse = %s
|
and p_item.warehouse = %s
|
||||||
""", (item_code, warehouse), as_dict=1)
|
""", (item_code, warehouse), as_dict=1)
|
||||||
|
|
||||||
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
|
return reserved_qty[0].qty or 0 if reserved_qty 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
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_sales_return(source_name, target_doc=None):
|
def make_sales_return(source_name, target_doc=None):
|
||||||
|
@ -13,8 +13,7 @@ from frappe.model.mapper import map_doc, map_child_doc
|
|||||||
from frappe.utils.scheduler import is_scheduler_inactive
|
from frappe.utils.scheduler import is_scheduler_inactive
|
||||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||||
import json
|
import json
|
||||||
|
import six
|
||||||
from six import iteritems
|
|
||||||
|
|
||||||
class POSInvoiceMergeLog(Document):
|
class POSInvoiceMergeLog(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -43,8 +42,9 @@ class POSInvoiceMergeLog(Document):
|
|||||||
if return_against_status != "Consolidated":
|
if return_against_status != "Consolidated":
|
||||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||||
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
|
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}.")
|
||||||
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
|
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
|
||||||
|
msg += " "
|
||||||
msg += _("Original invoice should be consolidated before or along with the return invoice.")
|
msg += _("Original invoice should be consolidated before or along with the return invoice.")
|
||||||
msg += "<br><br>"
|
msg += "<br><br>"
|
||||||
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
|
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
|
||||||
@ -57,12 +57,12 @@ class POSInvoiceMergeLog(Document):
|
|||||||
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
|
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
|
||||||
|
|
||||||
sales_invoice, credit_note = "", ""
|
sales_invoice, credit_note = "", ""
|
||||||
if sales:
|
|
||||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
|
||||||
|
|
||||||
if returns:
|
if returns:
|
||||||
credit_note = self.process_merging_into_credit_note(returns)
|
credit_note = self.process_merging_into_credit_note(returns)
|
||||||
|
|
||||||
|
if sales:
|
||||||
|
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||||
|
|
||||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||||
|
|
||||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||||
@ -239,7 +239,7 @@ 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()
|
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)
|
invoice_by_customer = get_invoice_customer_map(invoices)
|
||||||
|
|
||||||
if len(invoices) >= 1 and closing_entry:
|
if len(invoices) >= 10 and closing_entry:
|
||||||
closing_entry.set_status(update=True, status='Queued')
|
closing_entry.set_status(update=True, status='Queued')
|
||||||
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
|
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
|
||||||
else:
|
else:
|
||||||
@ -252,14 +252,15 @@ def unconsolidate_pos_invoices(closing_entry):
|
|||||||
pluck='name'
|
pluck='name'
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(merge_logs) >= 1:
|
if len(merge_logs) >= 10:
|
||||||
closing_entry.set_status(update=True, status='Queued')
|
closing_entry.set_status(update=True, status='Queued')
|
||||||
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
|
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
|
||||||
else:
|
else:
|
||||||
cancel_merge_logs(merge_logs, closing_entry)
|
cancel_merge_logs(merge_logs, closing_entry)
|
||||||
|
|
||||||
def create_merge_logs(invoice_by_customer, closing_entry=None):
|
def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||||
for customer, invoices in iteritems(invoice_by_customer):
|
try:
|
||||||
|
for customer, invoices in six.iteritems(invoice_by_customer):
|
||||||
merge_log = frappe.new_doc('POS Invoice Merge Log')
|
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.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
|
||||||
merge_log.customer = customer
|
merge_log.customer = customer
|
||||||
@ -271,9 +272,25 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
|||||||
|
|
||||||
if closing_entry:
|
if closing_entry:
|
||||||
closing_entry.set_status(update=True, status='Submitted')
|
closing_entry.set_status(update=True, status='Submitted')
|
||||||
|
closing_entry.db_set('error_message', '')
|
||||||
closing_entry.update_opening_entry()
|
closing_entry.update_opening_entry()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
frappe.db.rollback()
|
||||||
|
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
|
||||||
|
error_message = safe_load_json(message_log)
|
||||||
|
|
||||||
|
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):
|
def cancel_merge_logs(merge_logs, closing_entry=None):
|
||||||
|
try:
|
||||||
for log in merge_logs:
|
for log in merge_logs:
|
||||||
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
|
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
|
||||||
merge_log.flags.ignore_permissions = True
|
merge_log.flags.ignore_permissions = True
|
||||||
@ -281,8 +298,23 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
|
|||||||
|
|
||||||
if closing_entry:
|
if closing_entry:
|
||||||
closing_entry.set_status(update=True, status='Cancelled')
|
closing_entry.set_status(update=True, status='Cancelled')
|
||||||
|
closing_entry.db_set('error_message', '')
|
||||||
closing_entry.update_opening_entry(for_cancel=True)
|
closing_entry.update_opening_entry(for_cancel=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
frappe.db.rollback()
|
||||||
|
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
|
||||||
|
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):
|
def enqueue_job(job, **kwargs):
|
||||||
check_scheduler_status()
|
check_scheduler_status()
|
||||||
|
|
||||||
@ -315,3 +347,11 @@ def job_already_enqueued(job_name):
|
|||||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||||
if job_name in enqueued_jobs:
|
if job_name in enqueued_jobs:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def safe_load_json(message):
|
||||||
|
try:
|
||||||
|
json_message = json.loads(message).get('message')
|
||||||
|
except Exception:
|
||||||
|
json_message = message
|
||||||
|
|
||||||
|
return json_message
|
@ -152,7 +152,7 @@ class PricingRule(Document):
|
|||||||
frappe.throw(_("Valid from date must be less than valid upto date"))
|
frappe.throw(_("Valid from date must be less than valid upto date"))
|
||||||
|
|
||||||
def validate_condition(self):
|
def validate_condition(self):
|
||||||
if self.condition and ("=" in self.condition) and re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", self.condition):
|
if self.condition and ("=" in self.condition) and re.match(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+', self.condition):
|
||||||
frappe.throw(_("Invalid condition expression"))
|
frappe.throw(_("Invalid condition expression"))
|
||||||
|
|
||||||
#--------------------------------------------------------------------------------
|
#--------------------------------------------------------------------------------
|
||||||
|
@ -99,7 +99,7 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
|
|
||||||
args.item_code = "_Test Item 2"
|
args.item_code = "_Test Item 2"
|
||||||
details = get_item_details(args)
|
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):
|
def test_pricing_rule_for_margin(self):
|
||||||
from erpnext.stock.get_item_details import get_item_details
|
from erpnext.stock.get_item_details import get_item_details
|
||||||
@ -145,8 +145,8 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
"name": None
|
"name": None
|
||||||
})
|
})
|
||||||
details = get_item_details(args)
|
details = get_item_details(args)
|
||||||
self.assertEquals(details.get("margin_type"), "Percentage")
|
self.assertEqual(details.get("margin_type"), "Percentage")
|
||||||
self.assertEquals(details.get("margin_rate_or_amount"), 10)
|
self.assertEqual(details.get("margin_rate_or_amount"), 10)
|
||||||
|
|
||||||
def test_mixed_conditions_for_item_group(self):
|
def test_mixed_conditions_for_item_group(self):
|
||||||
for item in ["Mixed Cond Item 1", "Mixed Cond Item 2"]:
|
for item in ["Mixed Cond Item 1", "Mixed Cond Item 2"]:
|
||||||
@ -192,7 +192,7 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
"name": None
|
"name": None
|
||||||
})
|
})
|
||||||
details = get_item_details(args)
|
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):
|
def test_pricing_rule_for_variants(self):
|
||||||
from erpnext.stock.get_item_details import get_item_details
|
from erpnext.stock.get_item_details import get_item_details
|
||||||
@ -322,11 +322,11 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
si.insert(ignore_permissions=True)
|
si.insert(ignore_permissions=True)
|
||||||
|
|
||||||
item = si.items[0]
|
item = si.items[0]
|
||||||
self.assertEquals(item.margin_rate_or_amount, 10)
|
self.assertEqual(item.margin_rate_or_amount, 10)
|
||||||
self.assertEquals(item.rate_with_margin, 1100)
|
self.assertEqual(item.rate_with_margin, 1100)
|
||||||
self.assertEqual(item.discount_percentage, 10)
|
self.assertEqual(item.discount_percentage, 10)
|
||||||
self.assertEquals(item.discount_amount, 110)
|
self.assertEqual(item.discount_amount, 110)
|
||||||
self.assertEquals(item.rate, 990)
|
self.assertEqual(item.rate, 990)
|
||||||
|
|
||||||
def test_pricing_rule_with_margin_and_discount_amount(self):
|
def test_pricing_rule_with_margin_and_discount_amount(self):
|
||||||
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
|
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
|
||||||
@ -338,10 +338,10 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
si.insert(ignore_permissions=True)
|
si.insert(ignore_permissions=True)
|
||||||
|
|
||||||
item = si.items[0]
|
item = si.items[0]
|
||||||
self.assertEquals(item.margin_rate_or_amount, 10)
|
self.assertEqual(item.margin_rate_or_amount, 10)
|
||||||
self.assertEquals(item.rate_with_margin, 1100)
|
self.assertEqual(item.rate_with_margin, 1100)
|
||||||
self.assertEquals(item.discount_amount, 110)
|
self.assertEqual(item.discount_amount, 110)
|
||||||
self.assertEquals(item.rate, 990)
|
self.assertEqual(item.rate, 990)
|
||||||
|
|
||||||
def test_pricing_rule_for_product_discount_on_same_item(self):
|
def test_pricing_rule_for_product_discount_on_same_item(self):
|
||||||
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
|
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.items[0].price_list_rate = 1000
|
||||||
si.submit()
|
si.submit()
|
||||||
item = si.items[0]
|
item = si.items[0]
|
||||||
self.assertEquals(item.rate, 100)
|
self.assertEqual(item.rate, 100)
|
||||||
|
|
||||||
# Correct Customer and Incorrect is_return value
|
# 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 = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=1, qty=-1)
|
||||||
si.items[0].price_list_rate = 1000
|
si.items[0].price_list_rate = 1000
|
||||||
si.submit()
|
si.submit()
|
||||||
item = si.items[0]
|
item = si.items[0]
|
||||||
self.assertEquals(item.rate, 100)
|
self.assertEqual(item.rate, 100)
|
||||||
|
|
||||||
# Correct Customer and correct is_return value
|
# Correct Customer and correct is_return value
|
||||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=0)
|
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=0)
|
||||||
si.items[0].price_list_rate = 1000
|
si.items[0].price_list_rate = 1000
|
||||||
si.submit()
|
si.submit()
|
||||||
item = si.items[0]
|
item = si.items[0]
|
||||||
self.assertEquals(item.rate, 900)
|
self.assertEqual(item.rate, 900)
|
||||||
|
|
||||||
def test_multiple_pricing_rules(self):
|
def test_multiple_pricing_rules(self):
|
||||||
make_pricing_rule(discount_percentage=20, selling=1, priority=1, apply_multiple_pricing_rules=1,
|
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)
|
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)
|
si = create_sales_invoice(qty=5, do_not_submit=True)
|
||||||
self.assertEquals(len(si.items), 2)
|
self.assertEqual(len(si.items), 2)
|
||||||
self.assertEquals(si.items[1].rate, 10)
|
self.assertEqual(si.items[1].rate, 10)
|
||||||
|
|
||||||
si1 = create_sales_invoice(qty=2, do_not_submit=True)
|
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]:
|
for doc in [si, si1]:
|
||||||
doc.delete()
|
doc.delete()
|
||||||
|
@ -1,18 +1,36 @@
|
|||||||
<h1 class="text-center" style="page-break-before:always">{{ filters.party[0] }}</h1>
|
<div class="page-break">
|
||||||
<h3 class="text-center">{{ _("Statement of Accounts") }}</h3>
|
<div id="header-html" class="hidden-pdf">
|
||||||
|
{% if letter_head %}
|
||||||
<h5 class="text-center">
|
<div class="letter-head text-center">{{ letter_head.content }}</div>
|
||||||
{{ frappe.format(filters.from_date, 'Date')}}
|
<hr style="height:2px;border-width:0;color:black;background-color:black;">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div id="footer-html" class="visible-pdf">
|
||||||
|
{% if letter_head.footer %}
|
||||||
|
<div class="letter-head-footer">
|
||||||
|
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
|
||||||
|
{{ letter_head.footer }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||||
|
<div>
|
||||||
|
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
|
||||||
|
<h5 style="float: right;">
|
||||||
|
{{ _("Date: ") }}
|
||||||
|
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||||
{{ _("to") }}
|
{{ _("to") }}
|
||||||
{{ frappe.format(filters.to_date, 'Date')}}
|
{{ frappe.format(filters.to_date, 'Date')}}</b>
|
||||||
</h5>
|
</h5>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 12%">{{ _("Date") }}</th>
|
<th style="width: 12%">{{ _("Date") }}</th>
|
||||||
<th style="width: 15%">{{ _("Ref") }}</th>
|
<th style="width: 15%">{{ _("Reference") }}</th>
|
||||||
<th style="width: 25%">{{ _("Party") }}</th>
|
<th style="width: 25%">{{ _("Remarks") }}</th>
|
||||||
<th style="width: 15%">{{ _("Debit") }}</th>
|
<th style="width: 15%">{{ _("Debit") }}</th>
|
||||||
<th style="width: 15%">{{ _("Credit") }}</th>
|
<th style="width: 15%">{{ _("Credit") }}</th>
|
||||||
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
|
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
|
||||||
@ -58,22 +76,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<br><br>
|
<br>
|
||||||
{% if ageing %}
|
{% if ageing %}
|
||||||
<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ ageing.ageing_based_on }}</h3>
|
<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
|
||||||
<h5 class="text-center">
|
{{ _("up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
|
||||||
{{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
|
</h4>
|
||||||
</h5>
|
<table class="table table-bordered">
|
||||||
<br>
|
|
||||||
|
|
||||||
<table class="table table-bordered">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 12%">30 Days</th>
|
<th style="width: 25%">30 Days</th>
|
||||||
<th style="width: 15%">60 Days</th>
|
<th style="width: 25%">60 Days</th>
|
||||||
<th style="width: 25%">90 Days</th>
|
<th style="width: 25%">90 Days</th>
|
||||||
<th style="width: 15%">120 Days</th>
|
<th style="width: 25%">120 Days</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -84,6 +99,11 @@
|
|||||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
|
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="text-right text-muted">Printed On {{ frappe.format(frappe.utils.get_datetime(), 'Datetime') }}</p>
|
{% if terms_and_conditions %}
|
||||||
|
<div>
|
||||||
|
{{ terms_and_conditions }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
@ -19,7 +19,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
|||||||
frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'});
|
frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'});
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
frappe.msgprint('No Records for these settings.')
|
frappe.msgprint(__('No Records for these settings.'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -33,7 +33,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
|||||||
type: 'GET',
|
type: 'GET',
|
||||||
success: function(result) {
|
success: function(result) {
|
||||||
if(jQuery.isEmptyObject(result)){
|
if(jQuery.isEmptyObject(result)){
|
||||||
frappe.msgprint('No Records for these settings.');
|
frappe.msgprint(__('No Records for these settings.'));
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
window.location = url;
|
window.location = url;
|
||||||
@ -92,7 +92,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
|
|||||||
frm.refresh_field('customers');
|
frm.refresh_field('customers');
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
frappe.throw('No Customers found with selected options.');
|
frappe.throw(__('No Customers found with selected options.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_workflow": 1,
|
|
||||||
"autoname": "Prompt",
|
"autoname": "Prompt",
|
||||||
"creation": "2020-05-22 16:46:18.712954",
|
"creation": "2020-05-22 16:46:18.712954",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@ -28,9 +27,11 @@
|
|||||||
"customers",
|
"customers",
|
||||||
"preferences",
|
"preferences",
|
||||||
"orientation",
|
"orientation",
|
||||||
"section_break_14",
|
|
||||||
"include_ageing",
|
"include_ageing",
|
||||||
"ageing_based_on",
|
"ageing_based_on",
|
||||||
|
"section_break_14",
|
||||||
|
"letter_head",
|
||||||
|
"terms_and_conditions",
|
||||||
"section_break_1",
|
"section_break_1",
|
||||||
"enable_auto_email",
|
"enable_auto_email",
|
||||||
"section_break_18",
|
"section_break_18",
|
||||||
@ -270,10 +271,22 @@
|
|||||||
"fieldname": "body",
|
"fieldname": "body",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Text Editor",
|
||||||
"label": "Body"
|
"label": "Body"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "letter_head",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Letter Head",
|
||||||
|
"options": "Letter Head"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "terms_and_conditions",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Terms and Conditions",
|
||||||
|
"options": "Terms and Conditions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-08 08:47:09.185728",
|
"modified": "2021-05-21 10:14:22.426672",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts",
|
"name": "Process Statement Of Accounts",
|
||||||
|
@ -64,6 +64,9 @@ def get_report_pdf(doc, consolidated=True):
|
|||||||
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
|
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
|
||||||
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
|
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
|
||||||
or doc.currency or get_company_currency(doc.company)
|
or doc.currency or get_company_currency(doc.company)
|
||||||
|
if doc.letter_head:
|
||||||
|
from frappe.www.printview import get_letter_head
|
||||||
|
letter_head = get_letter_head(doc, 0)
|
||||||
|
|
||||||
filters= frappe._dict({
|
filters= frappe._dict({
|
||||||
'from_date': doc.from_date,
|
'from_date': doc.from_date,
|
||||||
@ -91,7 +94,10 @@ def get_report_pdf(doc, consolidated=True):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
html = frappe.render_template(template_path, \
|
html = frappe.render_template(template_path, \
|
||||||
{"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None})
|
{"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
|
||||||
|
"letter_head": letter_head if doc.letter_head else None,
|
||||||
|
"terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms')
|
||||||
|
if doc.terms_and_conditions else None})
|
||||||
|
|
||||||
html = frappe.render_template(base_template_path, {"body": html, \
|
html = frappe.render_template(base_template_path, {"body": html, \
|
||||||
"css": get_print_style(), "title": "Statement For " + entry.customer})
|
"css": get_print_style(), "title": "Statement For " + entry.customer})
|
||||||
|
@ -1380,7 +1380,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-30 22:45:58.334107",
|
"modified": "2021-04-30 22:45:58.334107",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
@ -636,8 +636,8 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_rejected_serial_no(self):
|
def test_rejected_serial_no(self):
|
||||||
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
|
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
|
||||||
rejected_qty=1, rate=500, update_stock=1,
|
rejected_qty=1, rate=500, update_stock=1, rejected_warehouse = "_Test Rejected Warehouse - _TC",
|
||||||
rejected_warehouse = "_Test Rejected Warehouse - _TC")
|
allow_zero_valuation_rate=1)
|
||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"),
|
self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"),
|
||||||
pi.get("items")[0].warehouse)
|
pi.get("items")[0].warehouse)
|
||||||
@ -994,7 +994,8 @@ def make_purchase_invoice(**args):
|
|||||||
"project": args.project,
|
"project": args.project,
|
||||||
"rejected_warehouse": args.rejected_warehouse or "",
|
"rejected_warehouse": args.rejected_warehouse or "",
|
||||||
"rejected_serial_no": args.rejected_serial_no or "",
|
"rejected_serial_no": args.rejected_serial_no or "",
|
||||||
"asset_location": args.location or ""
|
"asset_location": args.location or "",
|
||||||
|
"allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0
|
||||||
})
|
})
|
||||||
|
|
||||||
if args.get_taxes_and_charges:
|
if args.get_taxes_and_charges:
|
||||||
|
@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
|||||||
var me = this;
|
var me = this;
|
||||||
this._super();
|
this._super();
|
||||||
|
|
||||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
|
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
|
||||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||||
// show debit_to in print format
|
// show debit_to in print format
|
||||||
this.frm.set_df_property("debit_to", "print_hide", 0);
|
this.frm.set_df_property("debit_to", "print_hide", 0);
|
||||||
@ -356,11 +356,11 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
|||||||
},
|
},
|
||||||
|
|
||||||
items_on_form_rendered: function() {
|
items_on_form_rendered: function() {
|
||||||
erpnext.setup_serial_no();
|
erpnext.setup_serial_or_batch_no();
|
||||||
},
|
},
|
||||||
|
|
||||||
packed_items_on_form_rendered: function(doc, grid_row) {
|
packed_items_on_form_rendered: function(doc, grid_row) {
|
||||||
erpnext.setup_serial_no();
|
erpnext.setup_serial_or_batch_no();
|
||||||
},
|
},
|
||||||
|
|
||||||
make_sales_return: function() {
|
make_sales_return: function() {
|
||||||
@ -582,6 +582,16 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("adjustment_against", function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
customer: frm.doc.customer,
|
||||||
|
docstatus: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
frm.custom_make_buttons = {
|
frm.custom_make_buttons = {
|
||||||
'Delivery Note': 'Delivery',
|
'Delivery Note': 'Delivery',
|
||||||
'Sales Invoice': 'Return / Credit Note',
|
'Sales Invoice': 'Return / Credit Note',
|
||||||
@ -685,6 +695,7 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
project: function(frm){
|
project: function(frm){
|
||||||
|
if (!frm.doc.is_return) {
|
||||||
frm.call({
|
frm.call({
|
||||||
method: "add_timesheet_data",
|
method: "add_timesheet_data",
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
@ -693,6 +704,7 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
@ -807,14 +819,27 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
add_timesheet_row: function(frm, row, exchange_rate) {
|
||||||
|
frm.add_child('timesheets', {
|
||||||
|
'activity_type': row.activity_type,
|
||||||
|
'description': row.description,
|
||||||
|
'time_sheet': row.parent,
|
||||||
|
'billing_hours': row.billing_hours,
|
||||||
|
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
|
||||||
|
'timesheet_detail': row.name
|
||||||
|
});
|
||||||
|
frm.refresh_field('timesheets');
|
||||||
|
calculate_total_billing_amount(frm);
|
||||||
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if (frm.doc.project) {
|
if (frm.doc.docstatus===0 && !frm.doc.is_return) {
|
||||||
frm.add_custom_button(__('Fetch Timesheet'), function() {
|
frm.add_custom_button(__('Fetch Timesheet'), function() {
|
||||||
let d = new frappe.ui.Dialog({
|
let d = new frappe.ui.Dialog({
|
||||||
title: __('Fetch Timesheet'),
|
title: __('Fetch Timesheet'),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
"label" : "From",
|
"label" : __("From"),
|
||||||
"fieldname": "from_time",
|
"fieldname": "from_time",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
@ -824,11 +849,18 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
fieldname: 'col_break_1',
|
fieldname: 'col_break_1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label" : "To",
|
"label" : __("To"),
|
||||||
"fieldname": "to_time",
|
"fieldname": "to_time",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"label" : __("Project"),
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Project",
|
||||||
|
"default": frm.doc.project
|
||||||
|
},
|
||||||
],
|
],
|
||||||
primary_action: function() {
|
primary_action: function() {
|
||||||
let data = d.get_values();
|
let data = d.get_values();
|
||||||
@ -837,28 +869,36 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
args: {
|
args: {
|
||||||
from_time: data.from_time,
|
from_time: data.from_time,
|
||||||
to_time: data.to_time,
|
to_time: data.to_time,
|
||||||
project: frm.doc.project
|
project: data.project
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(!r.exc) {
|
if (!r.exc && r.message.length > 0) {
|
||||||
if(r.message.length > 0) {
|
|
||||||
frm.clear_table('timesheets')
|
frm.clear_table('timesheets')
|
||||||
r.message.forEach((d) => {
|
r.message.forEach((d) => {
|
||||||
frm.add_child('timesheets',{
|
let exchange_rate = 1.0;
|
||||||
'time_sheet': d.parent,
|
if (frm.doc.currency != d.currency) {
|
||||||
'billing_hours': d.billing_hours,
|
frappe.call({
|
||||||
'billing_amount': d.billing_amt,
|
method: 'erpnext.setup.utils.get_exchange_rate',
|
||||||
'timesheet_detail': d.name
|
args: {
|
||||||
});
|
from_currency: d.currency,
|
||||||
});
|
to_currency: frm.doc.currency
|
||||||
frm.refresh_field('timesheets')
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
exchange_rate = r.message;
|
||||||
|
frm.events.add_timesheet_row(frm, d, exchange_rate);
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
frappe.msgprint(__('No Timesheet Found.'))
|
});
|
||||||
|
} else {
|
||||||
|
frm.events.add_timesheet_row(frm, d, exchange_rate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__('No Timesheets found with the selected filters.'))
|
||||||
}
|
}
|
||||||
d.hide();
|
d.hide();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
primary_action_label: __('Get Timesheets')
|
primary_action_label: __('Get Timesheets')
|
||||||
@ -867,6 +907,10 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.is_debit_note) {
|
||||||
|
frm.set_df_property('return_against', 'label', 'Adjustment Against');
|
||||||
|
}
|
||||||
|
|
||||||
if (frappe.boot.active_domains.includes("Healthcare")) {
|
if (frappe.boot.active_domains.includes("Healthcare")) {
|
||||||
frm.set_df_property("patient", "hidden", 0);
|
frm.set_df_property("patient", "hidden", 0);
|
||||||
frm.set_df_property("patient_name", "hidden", 0);
|
frm.set_df_property("patient_name", "hidden", 0);
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"is_pos",
|
"is_pos",
|
||||||
"is_consolidated",
|
"is_consolidated",
|
||||||
"is_return",
|
"is_return",
|
||||||
|
"is_debit_note",
|
||||||
"update_billed_amount_in_sales_order",
|
"update_billed_amount_in_sales_order",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"company",
|
"company",
|
||||||
@ -392,7 +393,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "return_against",
|
"depends_on": "eval:doc.return_against || doc.is_debit_note",
|
||||||
"fieldname": "return_against",
|
"fieldname": "return_against",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@ -401,7 +402,7 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Sales Invoice",
|
"options": "Sales Invoice",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only_depends_on": "eval:doc.is_return",
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -748,6 +749,7 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
|
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
|
||||||
|
"depends_on": "eval: !doc.is_return",
|
||||||
"fieldname": "time_sheet_list",
|
"fieldname": "time_sheet_list",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@ -770,6 +772,7 @@
|
|||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Total Billing Amount",
|
"label": "Total Billing Amount",
|
||||||
|
"options": "currency",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@ -1951,6 +1954,12 @@
|
|||||||
"label": "Set Target Warehouse",
|
"label": "Set Target Warehouse",
|
||||||
"options": "Warehouse"
|
"options": "Warehouse"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "is_debit_note",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Debit Note"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "grand_total",
|
"depends_on": "grand_total",
|
||||||
@ -1969,7 +1978,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-04-15 23:57:58.766651",
|
"modified": "2021-05-20 22:48:33.988881",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
@ -125,6 +125,8 @@ class SalesInvoice(SellingController):
|
|||||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
|
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
|
||||||
if not self.is_return:
|
if not self.is_return:
|
||||||
self.validate_serial_numbers()
|
self.validate_serial_numbers()
|
||||||
|
else:
|
||||||
|
self.timesheets = []
|
||||||
self.update_packing_list()
|
self.update_packing_list()
|
||||||
self.set_billing_hours_and_amount()
|
self.set_billing_hours_and_amount()
|
||||||
self.update_timesheet_billing_for_project()
|
self.update_timesheet_billing_for_project()
|
||||||
@ -337,7 +339,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
if "Healthcare" in active_domains:
|
if "Healthcare" in active_domains:
|
||||||
manage_invoice_submit_cancel(self, "on_cancel")
|
manage_invoice_submit_cancel(self, "on_cancel")
|
||||||
|
self.unlink_sales_invoice_from_timesheets()
|
||||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
|
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
|
||||||
|
|
||||||
def update_status_updater_args(self):
|
def update_status_updater_args(self):
|
||||||
@ -393,6 +395,18 @@ class SalesInvoice(SellingController):
|
|||||||
if validate_against_credit_limit:
|
if validate_against_credit_limit:
|
||||||
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
|
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
|
||||||
|
|
||||||
|
def unlink_sales_invoice_from_timesheets(self):
|
||||||
|
for row in self.timesheets:
|
||||||
|
timesheet = frappe.get_doc('Timesheet', row.time_sheet)
|
||||||
|
for time_log in timesheet.time_logs:
|
||||||
|
if time_log.sales_invoice == self.name:
|
||||||
|
time_log.sales_invoice = None
|
||||||
|
timesheet.calculate_total_amounts()
|
||||||
|
timesheet.calculate_percentage_billed()
|
||||||
|
timesheet.flags.ignore_validate_update_after_submit = True
|
||||||
|
timesheet.set_status()
|
||||||
|
timesheet.db_update_all()
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def set_missing_values(self, for_validate=False):
|
def set_missing_values(self, for_validate=False):
|
||||||
pos = self.set_pos_fields(for_validate)
|
pos = self.set_pos_fields(for_validate)
|
||||||
@ -427,7 +441,7 @@ class SalesInvoice(SellingController):
|
|||||||
timesheet.calculate_percentage_billed()
|
timesheet.calculate_percentage_billed()
|
||||||
timesheet.flags.ignore_validate_update_after_submit = True
|
timesheet.flags.ignore_validate_update_after_submit = True
|
||||||
timesheet.set_status()
|
timesheet.set_status()
|
||||||
timesheet.save()
|
timesheet.db_update_all()
|
||||||
|
|
||||||
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
|
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
|
||||||
for data in timesheet.time_logs:
|
for data in timesheet.time_logs:
|
||||||
@ -517,7 +531,7 @@ class SalesInvoice(SellingController):
|
|||||||
# set pos values in items
|
# set pos values in items
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if item.get('item_code'):
|
if item.get('item_code'):
|
||||||
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
|
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True)
|
||||||
for fname, val in iteritems(profile_details):
|
for fname, val in iteritems(profile_details):
|
||||||
if (not for_validate) or (for_validate and not item.get(fname)):
|
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||||
item.set(fname, val)
|
item.set(fname, val)
|
||||||
@ -741,8 +755,10 @@ class SalesInvoice(SellingController):
|
|||||||
self.append('timesheets', {
|
self.append('timesheets', {
|
||||||
'time_sheet': data.parent,
|
'time_sheet': data.parent,
|
||||||
'billing_hours': data.billing_hours,
|
'billing_hours': data.billing_hours,
|
||||||
'billing_amount': data.billing_amt,
|
'billing_amount': data.billing_amount,
|
||||||
'timesheet_detail': data.name
|
'timesheet_detail': data.name,
|
||||||
|
'activity_type': data.activity_type,
|
||||||
|
'description': data.description
|
||||||
})
|
})
|
||||||
|
|
||||||
self.calculate_billing_amount_for_timesheet()
|
self.calculate_billing_amount_for_timesheet()
|
||||||
@ -1111,7 +1127,7 @@ class SalesInvoice(SellingController):
|
|||||||
if not item.serial_no:
|
if not item.serial_no:
|
||||||
continue
|
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:
|
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)
|
frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice)
|
||||||
|
|
||||||
@ -1121,7 +1137,6 @@ class SalesInvoice(SellingController):
|
|||||||
"""
|
"""
|
||||||
self.set_serial_no_against_delivery_note()
|
self.set_serial_no_against_delivery_note()
|
||||||
self.validate_serial_against_delivery_note()
|
self.validate_serial_against_delivery_note()
|
||||||
self.validate_serial_against_sales_invoice()
|
|
||||||
|
|
||||||
def set_serial_no_against_delivery_note(self):
|
def set_serial_no_against_delivery_note(self):
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
@ -1152,26 +1167,6 @@ class SalesInvoice(SellingController):
|
|||||||
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
|
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
|
||||||
item.idx, item.qty, item.item_code, len(si_serial_nos)))
|
item.idx, item.qty, item.item_code, len(si_serial_nos)))
|
||||||
|
|
||||||
def validate_serial_against_sales_invoice(self):
|
|
||||||
""" check if serial number is already used in other sales invoice """
|
|
||||||
for item in self.items:
|
|
||||||
if not item.serial_no:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for serial_no in item.serial_no.split("\n"):
|
|
||||||
serial_no_details = frappe.db.get_value("Serial No", serial_no,
|
|
||||||
["sales_invoice", "item_code"], as_dict=1)
|
|
||||||
|
|
||||||
if not serial_no_details:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if serial_no_details.sales_invoice and serial_no_details.item_code == item.item_code \
|
|
||||||
and self.name != serial_no_details.sales_invoice:
|
|
||||||
sales_invoice_company = frappe.db.get_value("Sales Invoice", serial_no_details.sales_invoice, "company")
|
|
||||||
if sales_invoice_company == self.company:
|
|
||||||
frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}")
|
|
||||||
.format(serial_no, serial_no_details.sales_invoice))
|
|
||||||
|
|
||||||
def update_project(self):
|
def update_project(self):
|
||||||
if self.project:
|
if self.project:
|
||||||
project = frappe.get_doc("Project", self.project)
|
project = frappe.get_doc("Project", self.project)
|
||||||
@ -1755,15 +1750,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))
|
item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
|
||||||
|
|
||||||
def get_delivery_note_details(internal_reference):
|
def get_delivery_note_details(internal_reference):
|
||||||
so_item_map = {}
|
|
||||||
|
|
||||||
si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
|
si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
|
||||||
filters={'parent': internal_reference})
|
filters={'parent': internal_reference})
|
||||||
|
|
||||||
for d in si_item_details:
|
return {d.name: d.so_detail for d in si_item_details if d.so_detail}
|
||||||
so_item_map.setdefault(d.name, d.so_detail)
|
|
||||||
|
|
||||||
return so_item_map
|
|
||||||
|
|
||||||
def get_sales_invoice_details(internal_reference):
|
def get_sales_invoice_details(internal_reference):
|
||||||
dn_item_map = {}
|
dn_item_map = {}
|
||||||
|
@ -933,12 +933,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
|
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
|
||||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0],
|
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0],
|
||||||
"delivery_document_no"), si.name)
|
"delivery_document_no"), si.name)
|
||||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"),
|
|
||||||
si.name)
|
|
||||||
|
|
||||||
# check if the serial number is already linked with any other Sales Invoice
|
|
||||||
_si = frappe.copy_doc(si.as_dict())
|
|
||||||
self.assertRaises(frappe.ValidationError, _si.insert)
|
|
||||||
|
|
||||||
return si
|
return si
|
||||||
|
|
||||||
|
@ -1,172 +1,78 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2016-06-14 19:21:34.321662",
|
"creation": "2016-06-14 19:21:34.321662",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"activity_type",
|
||||||
|
"description",
|
||||||
|
"billing_hours",
|
||||||
|
"billing_amount",
|
||||||
|
"time_sheet",
|
||||||
|
"timesheet_detail"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "time_sheet",
|
"fieldname": "time_sheet",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Time Sheet",
|
"label": "Time Sheet",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Timesheet",
|
"options": "Timesheet",
|
||||||
"permlevel": 0,
|
"read_only": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "billing_hours",
|
"fieldname": "billing_hours",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Billing Hours",
|
"label": "Billing Hours",
|
||||||
"length": 0,
|
"read_only": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "billing_amount",
|
"fieldname": "billing_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Billing Amount",
|
"label": "Billing Amount",
|
||||||
"length": 0,
|
"options": "currency",
|
||||||
"no_copy": 0,
|
"read_only": 1
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "timesheet_detail",
|
"fieldname": "timesheet_detail",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Timesheet Detail",
|
"label": "Timesheet Detail",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
},
|
||||||
"remember_last_selected_value": 0,
|
{
|
||||||
"report_hide": 0,
|
"fieldname": "activity_type",
|
||||||
"reqd": 0,
|
"fieldtype": "Link",
|
||||||
"search_index": 0,
|
"in_list_view": 1,
|
||||||
"set_only_once": 0,
|
"label": "Activity Type",
|
||||||
"translatable": 0,
|
"options": "Activity Type",
|
||||||
"unique": 0
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Description",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2019-02-18 18:50:44.770361",
|
"modified": "2021-05-20 22:33:57.234846",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Timesheet",
|
"name": "Sales Invoice Timesheet",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
@ -22,6 +22,9 @@ def get_party_details(inv):
|
|||||||
party_type = 'Supplier'
|
party_type = 'Supplier'
|
||||||
party = inv.supplier
|
party = inv.supplier
|
||||||
|
|
||||||
|
if not party:
|
||||||
|
frappe.throw(_("Please select {0} first").format(party_type))
|
||||||
|
|
||||||
return party_type, party
|
return party_type, party
|
||||||
|
|
||||||
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||||
|
@ -185,10 +185,10 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
|||||||
for d in gl_map:
|
for d in gl_map:
|
||||||
if d.account == round_off_account:
|
if d.account == round_off_account:
|
||||||
round_off_gle = d
|
round_off_gle = d
|
||||||
if d.debit_in_account_currency:
|
if d.debit:
|
||||||
debit_credit_diff -= flt(d.debit_in_account_currency)
|
debit_credit_diff -= flt(d.debit)
|
||||||
else:
|
else:
|
||||||
debit_credit_diff += flt(d.credit_in_account_currency)
|
debit_credit_diff += flt(d.credit)
|
||||||
round_off_account_exists = True
|
round_off_account_exists = True
|
||||||
|
|
||||||
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
|
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"attach_print": 0,
|
"attach_print": 0,
|
||||||
|
"channel": "Email",
|
||||||
"condition": "doc.auto_created",
|
"condition": "doc.auto_created",
|
||||||
"creation": "2018-04-25 14:19:05.440361",
|
"creation": "2018-04-25 14:19:05.440361",
|
||||||
"days_in_advance": 0,
|
"days_in_advance": 0,
|
||||||
|
@ -5,7 +5,8 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt, cint
|
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):
|
def execute(filters=None):
|
||||||
period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
|
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'):
|
if filters.get('accumulated_values'):
|
||||||
period_list = [period_list[-1]]
|
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:
|
for period in period_list:
|
||||||
key = period if consolidated else period.key
|
key = period if consolidated else period.key
|
||||||
if asset:
|
if asset:
|
||||||
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, cstr
|
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.report.profit_and_loss_statement.profit_and_loss_statement import get_net_profit_loss
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
@ -67,9 +67,9 @@ def execute(filters=None):
|
|||||||
section_data.append(account_data)
|
section_data.append(account_data)
|
||||||
|
|
||||||
add_total_row_account(data, section_data, cash_flow_account['section_footer'],
|
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)
|
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||||
|
|
||||||
chart = get_chart_data(columns, data)
|
chart = get_chart_data(columns, data)
|
||||||
@ -162,18 +162,26 @@ def get_start_date(period, accumulated_values, company):
|
|||||||
|
|
||||||
return start_date
|
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 = {
|
total_row = {
|
||||||
"account_name": "'" + _("{0}").format(label) + "'",
|
"account_name": "'" + _("{0}").format(label) + "'",
|
||||||
"account": "'" + _("{0}").format(label) + "'",
|
"account": "'" + _("{0}").format(label) + "'",
|
||||||
"currency": currency
|
"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:
|
for row in data:
|
||||||
if row.get("parent_account"):
|
if row.get("parent_account"):
|
||||||
for period in period_list:
|
for period in period_list:
|
||||||
key = period if consolidated else period['key']
|
key = period if consolidated else period['key']
|
||||||
total_row.setdefault(key, 0.0)
|
total_row.setdefault(key, 0.0)
|
||||||
total_row[key] += row.get(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.setdefault("total", 0.0)
|
||||||
total_row["total"] += row["total"]
|
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(total_row)
|
||||||
out.append({})
|
out.append({})
|
||||||
|
|
||||||
summary_data[label] = total_row["total"]
|
|
||||||
|
|
||||||
def get_report_summary(summary_data, currency):
|
def get_report_summary(summary_data, currency):
|
||||||
report_summary = []
|
report_summary = []
|
||||||
|
@ -165,7 +165,7 @@ def add_data_for_operating_activities(
|
|||||||
if profit_data:
|
if profit_data:
|
||||||
profit_data.update({
|
profit_data.update({
|
||||||
"indent": 1,
|
"indent": 1,
|
||||||
"parent_account": get_mapper_for(light_mappers, position=0)['section_header']
|
"parent_account": get_mapper_for(light_mappers, position=1)['section_header']
|
||||||
})
|
})
|
||||||
data.append(profit_data)
|
data.append(profit_data)
|
||||||
section_data.append(profit_data)
|
section_data.append(profit_data)
|
||||||
@ -312,10 +312,10 @@ def add_data_for_other_activities(
|
|||||||
def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper):
|
def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper):
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
operating_activities_mapper = get_mapper_for(light_mappers, position=0)
|
operating_activities_mapper = get_mapper_for(light_mappers, position=1)
|
||||||
other_mappers = [
|
other_mappers = [
|
||||||
get_mapper_for(light_mappers, position=1),
|
get_mapper_for(light_mappers, position=2),
|
||||||
get_mapper_for(light_mappers, position=2)
|
get_mapper_for(light_mappers, position=3)
|
||||||
]
|
]
|
||||||
|
|
||||||
if operating_activities_mapper:
|
if operating_activities_mapper:
|
||||||
|
@ -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)
|
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
|
return data, None, chart, report_summary
|
||||||
|
|
||||||
@ -149,9 +149,9 @@ def get_cash_flow_data(fiscal_year, companies, filters):
|
|||||||
section_data.append(account_data)
|
section_data.append(account_data)
|
||||||
|
|
||||||
add_total_row_account(data, section_data, cash_flow_account['section_footer'],
|
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)
|
report_summary = get_cash_flow_summary(summary_data, company_currency)
|
||||||
|
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||||
|
frappe.query_reports["Dimension-wise Accounts Balance Report"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"label": __("Company"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"default": frappe.defaults.get_user_default("Company"),
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fiscal_year",
|
||||||
|
"label": __("Fiscal Year"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Fiscal Year",
|
||||||
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
|
"reqd": 1,
|
||||||
|
"on_change": function(query_report) {
|
||||||
|
var fiscal_year = query_report.get_values().fiscal_year;
|
||||||
|
if (!fiscal_year) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||||
|
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
from_date: fy.year_start_date,
|
||||||
|
to_date: fy.year_end_date
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_date",
|
||||||
|
"label": __("From Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"default": frappe.defaults.get_user_default("year_start_date"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "to_date",
|
||||||
|
"label": __("To Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"default": frappe.defaults.get_user_default("year_end_date"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "finance_book",
|
||||||
|
"label": __("Finance Book"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Finance Book",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dimension",
|
||||||
|
"label": __("Select Dimension"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": get_accounting_dimension_options(),
|
||||||
|
"reqd": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"formatter": erpnext.financial_statements.formatter,
|
||||||
|
"tree": true,
|
||||||
|
"name_field": "account",
|
||||||
|
"parent_field": "parent_account",
|
||||||
|
"initial_depth": 3
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function get_accounting_dimension_options() {
|
||||||
|
let options =["", "Cost Center", "Project"];
|
||||||
|
frappe.db.get_list('Accounting Dimension',
|
||||||
|
{fields:['document_type']}).then((res) => {
|
||||||
|
res.forEach((dimension) => {
|
||||||
|
options.push(dimension.document_type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return options
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2021-04-09 16:48:59.548018",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2021-04-09 16:48:59.548018",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Dimension-wise Accounts Balance Report",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "GL Entry",
|
||||||
|
"report_name": "Dimension-wise Accounts Balance Report",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": []
|
||||||
|
}
|
@ -0,0 +1,213 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe, erpnext
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import (flt, cstr)
|
||||||
|
|
||||||
|
from erpnext.accounts.report.financial_statements import filter_accounts, filter_out_zero_value_rows
|
||||||
|
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||||
|
|
||||||
|
from six import itervalues
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
validate_filters(filters)
|
||||||
|
dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
|
||||||
|
|
||||||
|
if not dimension_items_list:
|
||||||
|
return [], []
|
||||||
|
|
||||||
|
dimension_items_list = [''.join(d) for d in dimension_items_list]
|
||||||
|
columns = get_columns(dimension_items_list)
|
||||||
|
data = get_data(filters, dimension_items_list)
|
||||||
|
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
def get_data(filters, dimension_items_list):
|
||||||
|
company_currency = erpnext.get_company_currency(filters.company)
|
||||||
|
acc = frappe.db.sql("""
|
||||||
|
select
|
||||||
|
name, account_number, parent_account, lft, rgt, root_type,
|
||||||
|
report_type, account_name, include_in_gross, account_type, is_group
|
||||||
|
from
|
||||||
|
`tabAccount`
|
||||||
|
where
|
||||||
|
company=%s
|
||||||
|
order by lft""", (filters.company), as_dict=True)
|
||||||
|
|
||||||
|
if not acc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
|
||||||
|
|
||||||
|
min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount`
|
||||||
|
where company=%s""", (filters.company))[0]
|
||||||
|
|
||||||
|
account = frappe.db.sql_list("""select name from `tabAccount`
|
||||||
|
where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
|
||||||
|
|
||||||
|
gl_entries_by_account = {}
|
||||||
|
set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
|
||||||
|
format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
|
||||||
|
accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
|
||||||
|
out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
|
||||||
|
out = filter_out_zero_value_rows(out, parent_children_map)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
|
||||||
|
for item in dimension_items_list:
|
||||||
|
condition = get_condition(filters.from_date, item, filters.dimension)
|
||||||
|
if account:
|
||||||
|
condition += " and account in ({})"\
|
||||||
|
.format(", ".join([frappe.db.escape(d) for d in account]))
|
||||||
|
|
||||||
|
gl_filters = {
|
||||||
|
"company": filters.get("company"),
|
||||||
|
"from_date": filters.get("from_date"),
|
||||||
|
"to_date": filters.get("to_date"),
|
||||||
|
"finance_book": cstr(filters.get("finance_book"))
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_filters['item'] = ''.join(item)
|
||||||
|
|
||||||
|
if filters.get("include_default_book_entries"):
|
||||||
|
gl_filters["company_fb"] = frappe.db.get_value("Company",
|
||||||
|
filters.company, 'default_finance_book')
|
||||||
|
|
||||||
|
for key, value in filters.items():
|
||||||
|
if value:
|
||||||
|
gl_filters.update({
|
||||||
|
key: value
|
||||||
|
})
|
||||||
|
|
||||||
|
gl_entries = frappe.db.sql("""
|
||||||
|
select
|
||||||
|
posting_date, account, debit, credit, is_opening, fiscal_year,
|
||||||
|
debit_in_account_currency, credit_in_account_currency, account_currency
|
||||||
|
from
|
||||||
|
`tabGL Entry`
|
||||||
|
where
|
||||||
|
company=%(company)s
|
||||||
|
{condition}
|
||||||
|
and posting_date <= %(to_date)s
|
||||||
|
and is_cancelled = 0
|
||||||
|
order by account, posting_date""".format(
|
||||||
|
condition=condition),
|
||||||
|
gl_filters, as_dict=True) #nosec
|
||||||
|
|
||||||
|
for entry in gl_entries:
|
||||||
|
entry['dimension_item'] = ''.join(item)
|
||||||
|
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||||
|
|
||||||
|
def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
|
||||||
|
|
||||||
|
for entries in itervalues(gl_entries_by_account):
|
||||||
|
for entry in entries:
|
||||||
|
d = accounts_by_name.get(entry.account)
|
||||||
|
if not d:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Could not retrieve information for {0}.").format(entry.account), title="Error",
|
||||||
|
raise_exception=1
|
||||||
|
)
|
||||||
|
for item in dimension_items_list:
|
||||||
|
if item == entry.dimension_item:
|
||||||
|
d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
|
||||||
|
|
||||||
|
def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
|
||||||
|
data = []
|
||||||
|
|
||||||
|
for d in accounts:
|
||||||
|
has_value = False
|
||||||
|
total = 0
|
||||||
|
row = {
|
||||||
|
"account": d.name,
|
||||||
|
"parent_account": d.parent_account,
|
||||||
|
"indent": d.indent,
|
||||||
|
"from_date": filters.from_date,
|
||||||
|
"to_date": filters.to_date,
|
||||||
|
"currency": company_currency,
|
||||||
|
"account_name": ('{} - {}'.format(d.account_number, d.account_name)
|
||||||
|
if d.account_number else d.account_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in dimension_items_list:
|
||||||
|
row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
|
||||||
|
|
||||||
|
if abs(row[frappe.scrub(item)]) >= 0.005:
|
||||||
|
# ignore zero values
|
||||||
|
has_value = True
|
||||||
|
total += flt(d.get(frappe.scrub(item), 0.0), 3)
|
||||||
|
|
||||||
|
row["has_value"] = has_value
|
||||||
|
row["total"] = total
|
||||||
|
data.append(row)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
|
||||||
|
"""accumulate children's values in parent accounts"""
|
||||||
|
for d in reversed(accounts):
|
||||||
|
if d.parent_account:
|
||||||
|
for item in dimension_items_list:
|
||||||
|
accounts_by_name[d.parent_account][frappe.scrub(item)] = \
|
||||||
|
accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
|
||||||
|
|
||||||
|
def get_condition(from_date, item, dimension):
|
||||||
|
conditions = []
|
||||||
|
|
||||||
|
if from_date:
|
||||||
|
conditions.append("posting_date >= %(from_date)s")
|
||||||
|
if dimension:
|
||||||
|
if dimension not in ['Cost Center', 'Project']:
|
||||||
|
if dimension in ['Customer', 'Supplier']:
|
||||||
|
dimension = 'Party'
|
||||||
|
else:
|
||||||
|
dimension = 'Voucher No'
|
||||||
|
txt = "{0} = %(item)s".format(frappe.scrub(dimension))
|
||||||
|
conditions.append(txt)
|
||||||
|
|
||||||
|
return " and {}".format(" and ".join(conditions)) if conditions else ""
|
||||||
|
|
||||||
|
def get_dimension_items_list(dimension, company):
|
||||||
|
meta = frappe.get_meta(dimension, cached=False)
|
||||||
|
fieldnames = [d.fieldname for d in meta.get("fields")]
|
||||||
|
filters = {}
|
||||||
|
if 'company' in fieldnames:
|
||||||
|
filters['company'] = company
|
||||||
|
return frappe.get_all(dimension, filters, as_list=True)
|
||||||
|
|
||||||
|
def get_columns(dimension_items_list, accumulated_values=1, company=None):
|
||||||
|
columns = [{
|
||||||
|
"fieldname": "account",
|
||||||
|
"label": _("Account"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Account",
|
||||||
|
"width": 300
|
||||||
|
}]
|
||||||
|
if company:
|
||||||
|
columns.append({
|
||||||
|
"fieldname": "currency",
|
||||||
|
"label": _("Currency"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Currency",
|
||||||
|
"hidden": 1
|
||||||
|
})
|
||||||
|
for item in dimension_items_list:
|
||||||
|
columns.append({
|
||||||
|
"fieldname": frappe.scrub(item),
|
||||||
|
"label": item,
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"options": "currency",
|
||||||
|
"width": 150
|
||||||
|
})
|
||||||
|
columns.append({
|
||||||
|
"fieldname": "total",
|
||||||
|
"label": "Total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"options": "currency",
|
||||||
|
"width": 150
|
||||||
|
})
|
||||||
|
|
||||||
|
return columns
|
@ -119,10 +119,10 @@ def validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year):
|
|||||||
|
|
||||||
def validate_dates(from_date, to_date):
|
def validate_dates(from_date, to_date):
|
||||||
if not from_date or not 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:
|
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):
|
def get_months(start_date, end_date):
|
||||||
diff = (12 * end_date.year + end_date.month) - (12 * start_date.year + start_date.month)
|
diff = (12 * end_date.year + end_date.month) - (12 * start_date.year + start_date.month)
|
||||||
@ -523,3 +523,11 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None):
|
|||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
|
@ -166,6 +166,11 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
"fieldname": "show_cancelled_entries",
|
"fieldname": "show_cancelled_entries",
|
||||||
"label": __("Show Cancelled Entries"),
|
"label": __("Show Cancelled Entries"),
|
||||||
"fieldtype": "Check"
|
"fieldtype": "Check"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "show_net_values_in_party_account",
|
||||||
|
"label": __("Show Net Values in Party Account"),
|
||||||
|
"fieldtype": "Check"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -344,6 +344,9 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
consolidated_gle = OrderedDict()
|
consolidated_gle = OrderedDict()
|
||||||
group_by = group_by_field(filters.get('group_by'))
|
group_by = group_by_field(filters.get('group_by'))
|
||||||
|
|
||||||
|
if filters.get('show_net_values_in_party_account'):
|
||||||
|
account_type_map = get_account_type_map(filters.get('company'))
|
||||||
|
|
||||||
def update_value_in_dict(data, key, gle):
|
def update_value_in_dict(data, key, gle):
|
||||||
data[key].debit += flt(gle.debit)
|
data[key].debit += flt(gle.debit)
|
||||||
data[key].credit += flt(gle.credit)
|
data[key].credit += flt(gle.credit)
|
||||||
@ -351,6 +354,24 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
data[key].debit_in_account_currency += flt(gle.debit_in_account_currency)
|
data[key].debit_in_account_currency += flt(gle.debit_in_account_currency)
|
||||||
data[key].credit_in_account_currency += flt(gle.credit_in_account_currency)
|
data[key].credit_in_account_currency += flt(gle.credit_in_account_currency)
|
||||||
|
|
||||||
|
if filters.get('show_net_values_in_party_account') and \
|
||||||
|
account_type_map.get(data[key].account) in ('Receivable', 'Payable'):
|
||||||
|
net_value = flt(data[key].debit) - flt(data[key].credit)
|
||||||
|
net_value_in_account_currency = flt(data[key].debit_in_account_currency) \
|
||||||
|
- flt(data[key].credit_in_account_currency)
|
||||||
|
|
||||||
|
if net_value < 0:
|
||||||
|
dr_or_cr = 'credit'
|
||||||
|
rev_dr_or_cr = 'debit'
|
||||||
|
else:
|
||||||
|
dr_or_cr = 'debit'
|
||||||
|
rev_dr_or_cr = 'credit'
|
||||||
|
|
||||||
|
data[key][dr_or_cr] = abs(net_value)
|
||||||
|
data[key][dr_or_cr+'_in_account_currency'] = abs(net_value_in_account_currency)
|
||||||
|
data[key][rev_dr_or_cr] = 0
|
||||||
|
data[key][rev_dr_or_cr+'_in_account_currency'] = 0
|
||||||
|
|
||||||
if data[key].against_voucher and gle.against_voucher:
|
if data[key].against_voucher and gle.against_voucher:
|
||||||
data[key].against_voucher += ', ' + gle.against_voucher
|
data[key].against_voucher += ', ' + gle.against_voucher
|
||||||
|
|
||||||
@ -388,6 +409,12 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
|
|
||||||
return totals, entries
|
return totals, entries
|
||||||
|
|
||||||
|
def get_account_type_map(company):
|
||||||
|
account_type_map = frappe._dict(frappe.get_all('Account', fields=['name', 'account_type'],
|
||||||
|
filters={'company': company}, as_list=1))
|
||||||
|
|
||||||
|
return account_type_map
|
||||||
|
|
||||||
def get_result_as_list(data, filters):
|
def get_result_as_list(data, filters):
|
||||||
balance, balance_in_account_currency = 0, 0
|
balance, balance_in_account_currency = 0, 0
|
||||||
inv_details = get_supplier_invoice_details()
|
inv_details = get_supplier_invoice_details()
|
||||||
|
@ -116,22 +116,19 @@ def validate_filters(filters):
|
|||||||
frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method"))
|
frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method"))
|
||||||
|
|
||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s".format(
|
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s"
|
||||||
company=filters.get("company"),
|
|
||||||
from_date=filters.get("from_date"),
|
|
||||||
to_date=filters.get("to_date"))
|
|
||||||
|
|
||||||
if filters.get("pos_profile"):
|
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"):
|
if filters.get("owner"):
|
||||||
conditions += " AND owner = %(owner)s".format(owner=filters.get("owner"))
|
conditions += " AND owner = %(owner)s"
|
||||||
|
|
||||||
if filters.get("customer"):
|
if filters.get("customer"):
|
||||||
conditions += " AND customer = %(customer)s".format(customer=filters.get("customer"))
|
conditions += " AND customer = %(customer)s"
|
||||||
|
|
||||||
if filters.get("is_return"):
|
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"):
|
if filters.get("mode_of_payment"):
|
||||||
conditions += """
|
conditions += """
|
||||||
|
@ -5,7 +5,8 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
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):
|
def execute(filters=None):
|
||||||
period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
|
period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year,
|
||||||
@ -33,13 +34,17 @@ def execute(filters=None):
|
|||||||
chart = get_chart_data(filters, columns, income, expense, net_profit_loss)
|
chart = get_chart_data(filters, columns, income, expense, net_profit_loss)
|
||||||
|
|
||||||
currency = filters.presentation_currency or frappe.get_cached_value('Company', filters.company, "default_currency")
|
currency = filters.presentation_currency or frappe.get_cached_value('Company', filters.company, "default_currency")
|
||||||
report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, currency)
|
report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters)
|
||||||
|
|
||||||
return columns, data, None, chart, report_summary
|
return columns, data, None, chart, report_summary
|
||||||
|
|
||||||
def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, currency, consolidated=False):
|
def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, currency, filters, consolidated=False):
|
||||||
net_income, net_expense, net_profit = 0.0, 0.0, 0.0
|
net_income, net_expense, net_profit = 0.0, 0.0, 0.0
|
||||||
|
|
||||||
|
# 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:
|
for period in period_list:
|
||||||
key = period if consolidated else period.key
|
key = period if consolidated else period.key
|
||||||
if income:
|
if income:
|
||||||
|
@ -81,8 +81,7 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
|||||||
presentation_currency = currency_info['presentation_currency']
|
presentation_currency = currency_info['presentation_currency']
|
||||||
company_currency = currency_info['company_currency']
|
company_currency = currency_info['company_currency']
|
||||||
|
|
||||||
pl_accounts = [d.name for d in frappe.get_list('Account',
|
account_currencies = list(set(entry['account_currency'] for entry in gl_entries))
|
||||||
filters={'report_type': 'Profit and Loss', 'company': company})]
|
|
||||||
|
|
||||||
for entry in gl_entries:
|
for entry in gl_entries:
|
||||||
account = entry['account']
|
account = entry['account']
|
||||||
@ -92,10 +91,15 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
|||||||
credit_in_account_currency = flt(entry['credit_in_account_currency'])
|
credit_in_account_currency = flt(entry['credit_in_account_currency'])
|
||||||
account_currency = entry['account_currency']
|
account_currency = entry['account_currency']
|
||||||
|
|
||||||
if account_currency != presentation_currency:
|
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||||
value = debit or credit
|
if entry.get('debit'):
|
||||||
|
entry['debit'] = debit_in_account_currency
|
||||||
|
|
||||||
date = entry['posting_date'] if account in pl_accounts else currency_info['report_date']
|
if entry.get('credit'):
|
||||||
|
entry['credit'] = credit_in_account_currency
|
||||||
|
else:
|
||||||
|
value = debit or credit
|
||||||
|
date = currency_info['report_date']
|
||||||
converted_value = convert(value, presentation_currency, company_currency, date)
|
converted_value = convert(value, presentation_currency, company_currency, date)
|
||||||
|
|
||||||
if entry.get('debit'):
|
if entry.get('debit'):
|
||||||
@ -104,13 +108,6 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
|||||||
if entry.get('credit'):
|
if entry.get('credit'):
|
||||||
entry['credit'] = converted_value
|
entry['credit'] = converted_value
|
||||||
|
|
||||||
elif account_currency == presentation_currency:
|
|
||||||
if entry.get('debit'):
|
|
||||||
entry['debit'] = debit_in_account_currency
|
|
||||||
|
|
||||||
if entry.get('credit'):
|
|
||||||
entry['credit'] = credit_in_account_currency
|
|
||||||
|
|
||||||
converted_gl_list.append(entry)
|
converted_gl_list.append(entry)
|
||||||
|
|
||||||
return converted_gl_list
|
return converted_gl_list
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"hide_custom": 0,
|
"hide_custom": 0,
|
||||||
"icon": "accounting",
|
"icon": "accounting",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
|
"is_default": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Accounting",
|
"label": "Accounting",
|
||||||
"links": [
|
"links": [
|
||||||
@ -625,9 +626,9 @@
|
|||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Bank Reconciliation",
|
"label": "Bank Reconciliation Tool",
|
||||||
"link_to": "bank-reconciliation",
|
"link_to": "Bank Reconciliation Tool",
|
||||||
"link_type": "Page",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
@ -641,26 +642,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"dependencies": "",
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Bank Statement Transaction Entry",
|
|
||||||
"link_to": "Bank Statement Transaction Entry",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dependencies": "",
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Bank Statement Settings",
|
|
||||||
"link_to": "Bank Statement Settings",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
@ -1071,7 +1052,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-03-04 00:38:35.349024",
|
"modified": "2021-05-12 11:48:01.905144",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounting",
|
"name": "Accounting",
|
||||||
|
@ -217,7 +217,7 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
# For first row
|
# For first row
|
||||||
if has_pro_rata and n==0:
|
if has_pro_rata and n==0:
|
||||||
depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount,
|
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||||
self.available_for_use_date, d.depreciation_start_date)
|
self.available_for_use_date, d.depreciation_start_date)
|
||||||
|
|
||||||
# For first depr schedule date will be the start date
|
# For first depr schedule date will be the start date
|
||||||
@ -230,7 +230,7 @@ class Asset(AccountsController):
|
|||||||
self.to_date = add_months(self.available_for_use_date,
|
self.to_date = add_months(self.available_for_use_date,
|
||||||
n * cint(d.frequency_of_depreciation))
|
n * cint(d.frequency_of_depreciation))
|
||||||
|
|
||||||
depreciation_amount, days, months = get_pro_rata_amt(d,
|
depreciation_amount, days, months = self.get_pro_rata_amt(d,
|
||||||
depreciation_amount, schedule_date, self.to_date)
|
depreciation_amount, schedule_date, self.to_date)
|
||||||
|
|
||||||
monthly_schedule_date = add_months(schedule_date, 1)
|
monthly_schedule_date = add_months(schedule_date, 1)
|
||||||
@ -568,6 +568,13 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
return 100 * (1 - flt(depreciation_rate, float_precision))
|
return 100 * (1 - flt(depreciation_rate, float_precision))
|
||||||
|
|
||||||
|
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
|
||||||
|
days = date_diff(to_date, from_date)
|
||||||
|
months = month_diff(to_date, from_date)
|
||||||
|
total_days = get_total_days(to_date, row.frequency_of_depreciation)
|
||||||
|
|
||||||
|
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
||||||
|
|
||||||
def update_maintenance_status():
|
def update_maintenance_status():
|
||||||
assets = frappe.get_all(
|
assets = frappe.get_all(
|
||||||
"Asset", filters={"docstatus": 1, "maintenance_required": 1}
|
"Asset", filters={"docstatus": 1, "maintenance_required": 1}
|
||||||
@ -760,13 +767,6 @@ def make_asset_movement(assets, purpose=None):
|
|||||||
def is_cwip_accounting_enabled(asset_category):
|
def is_cwip_accounting_enabled(asset_category):
|
||||||
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting"))
|
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting"))
|
||||||
|
|
||||||
def get_pro_rata_amt(row, depreciation_amount, from_date, to_date):
|
|
||||||
days = date_diff(to_date, from_date)
|
|
||||||
months = month_diff(to_date, from_date)
|
|
||||||
total_days = get_total_days(to_date, row.frequency_of_depreciation)
|
|
||||||
|
|
||||||
return (depreciation_amount * flt(days)) / flt(total_days), days, months
|
|
||||||
|
|
||||||
def get_total_days(date, frequency):
|
def get_total_days(date, frequency):
|
||||||
period_start_date = add_months(date,
|
period_start_date = add_months(date,
|
||||||
cint(frequency) * -1)
|
cint(frequency) * -1)
|
||||||
|
@ -78,7 +78,7 @@ class TestAsset(unittest.TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
doc.set_missing_values()
|
doc.set_missing_values()
|
||||||
self.assertEquals(doc.items[0].is_fixed_asset, 1)
|
self.assertEqual(doc.items[0].is_fixed_asset, 1)
|
||||||
|
|
||||||
def test_schedule_for_straight_line_method(self):
|
def test_schedule_for_straight_line_method(self):
|
||||||
pr = make_purchase_receipt(item_code="Macbook Pro",
|
pr = make_purchase_receipt(item_code="Macbook Pro",
|
||||||
@ -565,7 +565,7 @@ class TestAsset(unittest.TestCase):
|
|||||||
|
|
||||||
doc = make_invoice(pr.name)
|
doc = make_invoice(pr.name)
|
||||||
|
|
||||||
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
|
self.assertEqual('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
|
||||||
|
|
||||||
def test_asset_cwip_toggling_cases(self):
|
def test_asset_cwip_toggling_cases(self):
|
||||||
cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting")
|
cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting")
|
||||||
@ -635,6 +635,45 @@ class TestAsset(unittest.TestCase):
|
|||||||
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
|
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
|
||||||
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
|
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
|
||||||
|
|
||||||
|
def test_discounted_wdv_depreciation_rate_for_indian_region(self):
|
||||||
|
# set indian company
|
||||||
|
company_flag = frappe.flags.company
|
||||||
|
frappe.flags.company = "_Test Company"
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(item_code="Macbook Pro",
|
||||||
|
qty=1, rate=8000.0, location="Test Location")
|
||||||
|
|
||||||
|
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
|
||||||
|
asset = frappe.get_doc('Asset', asset_name)
|
||||||
|
asset.calculate_depreciation = 1
|
||||||
|
asset.available_for_use_date = '2030-06-12'
|
||||||
|
asset.purchase_date = '2030-01-01'
|
||||||
|
asset.append("finance_books", {
|
||||||
|
"expected_value_after_useful_life": 1000,
|
||||||
|
"depreciation_method": "Written Down Value",
|
||||||
|
"total_number_of_depreciations": 3,
|
||||||
|
"frequency_of_depreciation": 12,
|
||||||
|
"depreciation_start_date": "2030-12-31"
|
||||||
|
})
|
||||||
|
asset.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
|
||||||
|
|
||||||
|
expected_schedules = [
|
||||||
|
["2030-12-31", 1106.85, 1106.85],
|
||||||
|
["2031-12-31", 3446.58, 4553.43],
|
||||||
|
["2032-12-31", 1723.29, 6276.72],
|
||||||
|
["2033-06-12", 723.28, 7000.00]
|
||||||
|
]
|
||||||
|
|
||||||
|
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
|
||||||
|
for d in asset.get("schedules")]
|
||||||
|
|
||||||
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
|
||||||
|
# reset indian company
|
||||||
|
frappe.flags.company = company_flag
|
||||||
|
|
||||||
def create_asset_data():
|
def create_asset_data():
|
||||||
if not frappe.db.exists("Asset Category", "Computers"):
|
if not frappe.db.exists("Asset Category", "Computers"):
|
||||||
create_asset_category()
|
create_asset_category()
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
frappe.ui.form.on('Asset Category', {
|
frappe.ui.form.on('Asset Category', {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
frm.add_fetch('company_name', 'accumulated_depreciation_account', 'accumulated_depreciation_account');
|
frm.add_fetch('company_name', 'accumulated_depreciation_account', 'accumulated_depreciation_account');
|
||||||
frm.add_fetch('company_name', 'depreciation_expense_account', 'accumulated_depreciation_account');
|
frm.add_fetch('company_name', 'depreciation_expense_account', 'depreciation_expense_account');
|
||||||
|
|
||||||
frm.set_query('fixed_asset_account', 'accounts', function(doc, cdt, cdn) {
|
frm.set_query('fixed_asset_account', 'accounts', function(doc, cdt, cdn) {
|
||||||
var d = locals[cdt][cdn];
|
var d = locals[cdt][cdn];
|
||||||
|
@ -187,7 +187,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
update_child_qty_rate('Purchase Order', trans_item, po.name)
|
update_child_qty_rate('Purchase Order', trans_item, po.name)
|
||||||
|
|
||||||
po.reload()
|
po.reload()
|
||||||
self.assertEquals(len(po.get('items')), 2)
|
self.assertEqual(len(po.get('items')), 2)
|
||||||
self.assertEqual(po.status, 'To Receive and Bill')
|
self.assertEqual(po.status, 'To Receive and Bill')
|
||||||
# ordered qty should increase on row addition
|
# ordered qty should increase on row addition
|
||||||
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
|
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
|
||||||
@ -234,7 +234,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
update_child_qty_rate('Purchase Order', trans_item, po.name)
|
update_child_qty_rate('Purchase Order', trans_item, po.name)
|
||||||
|
|
||||||
po.reload()
|
po.reload()
|
||||||
self.assertEquals(len(po.get('items')), 1)
|
self.assertEqual(len(po.get('items')), 1)
|
||||||
self.assertEqual(po.status, 'To Receive and Bill')
|
self.assertEqual(po.status, 'To Receive and Bill')
|
||||||
|
|
||||||
# ordered qty should decrease (back to initial) on row deletion
|
# ordered qty should decrease (back to initial) on row deletion
|
||||||
@ -448,13 +448,13 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
|
|
||||||
pi.load_from_db()
|
pi.load_from_db()
|
||||||
|
|
||||||
self.assertEquals(pi.per_received, 100.00)
|
self.assertEqual(pi.per_received, 100.00)
|
||||||
self.assertEquals(pi.items[0].qty, pi.items[0].received_qty)
|
self.assertEqual(pi.items[0].qty, pi.items[0].received_qty)
|
||||||
|
|
||||||
po.load_from_db()
|
po.load_from_db()
|
||||||
|
|
||||||
self.assertEquals(po.per_received, 100.00)
|
self.assertEqual(po.per_received, 100.00)
|
||||||
self.assertEquals(po.per_billed, 100.00)
|
self.assertEqual(po.per_billed, 100.00)
|
||||||
|
|
||||||
pr.cancel()
|
pr.cancel()
|
||||||
|
|
||||||
@ -674,8 +674,8 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1)
|
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
|
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
|
||||||
self.assertEquals(bin2.projected_qty, bin1.projected_qty - 10)
|
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
|
||||||
|
|
||||||
# Create stock transfer
|
# Create stock transfer
|
||||||
rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item",
|
rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item",
|
||||||
@ -690,7 +690,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
||||||
|
|
||||||
# close PO
|
# close PO
|
||||||
po.update_status("Closed")
|
po.update_status("Closed")
|
||||||
@ -698,7 +698,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||||
|
|
||||||
# Re-open PO
|
# Re-open PO
|
||||||
po.update_status("Submitted")
|
po.update_status("Submitted")
|
||||||
@ -706,7 +706,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
||||||
|
|
||||||
make_stock_entry(target="_Test Warehouse 1 - _TC", item_code="_Test Item",
|
make_stock_entry(target="_Test Warehouse 1 - _TC", item_code="_Test Item",
|
||||||
qty=40, basic_rate=100)
|
qty=40, basic_rate=100)
|
||||||
@ -723,7 +723,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||||
|
|
||||||
# Cancel PR
|
# Cancel PR
|
||||||
pr.cancel()
|
pr.cancel()
|
||||||
@ -731,7 +731,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
self.assertEqual(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
||||||
|
|
||||||
# Make Purchase Invoice
|
# Make Purchase Invoice
|
||||||
pi = make_pi_from_po(po.name)
|
pi = make_pi_from_po(po.name)
|
||||||
@ -743,7 +743,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
self.assertEqual(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||||
|
|
||||||
# Cancel PR
|
# Cancel PR
|
||||||
pi.cancel()
|
pi.cancel()
|
||||||
@ -751,7 +751,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
self.assertEqual(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
||||||
|
|
||||||
# Cancel Stock Entry
|
# Cancel Stock Entry
|
||||||
se.cancel()
|
se.cancel()
|
||||||
@ -759,7 +759,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
|
self.assertEqual(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
|
||||||
|
|
||||||
# Cancel PO
|
# Cancel PO
|
||||||
po.reload()
|
po.reload()
|
||||||
@ -768,7 +768,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||||
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
fieldname="reserved_qty_for_sub_contract", as_dict=1)
|
||||||
|
|
||||||
self.assertEquals(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||||
|
|
||||||
def test_exploded_items_in_subcontracted(self):
|
def test_exploded_items_in_subcontracted(self):
|
||||||
item_code = "_Test Subcontracted FG Item 1"
|
item_code = "_Test Subcontracted FG Item 1"
|
||||||
@ -782,7 +782,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
|
|
||||||
exploded_items = sorted([d.item_code for d in bom.exploded_items if not d.get('sourced_by_supplier')])
|
exploded_items = sorted([d.item_code for d in bom.exploded_items if not d.get('sourced_by_supplier')])
|
||||||
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
|
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
|
||||||
self.assertEquals(exploded_items, supplied_items)
|
self.assertEqual(exploded_items, supplied_items)
|
||||||
|
|
||||||
po1 = create_purchase_order(item_code=item_code, qty=1,
|
po1 = create_purchase_order(item_code=item_code, qty=1,
|
||||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=0)
|
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=0)
|
||||||
@ -790,7 +790,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items])
|
supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items])
|
||||||
bom_items = sorted([d.item_code for d in bom.items if not d.get('sourced_by_supplier')])
|
bom_items = sorted([d.item_code for d in bom.items if not d.get('sourced_by_supplier')])
|
||||||
|
|
||||||
self.assertEquals(supplied_items1, bom_items)
|
self.assertEqual(supplied_items1, bom_items)
|
||||||
|
|
||||||
def test_backflush_based_on_stock_entry(self):
|
def test_backflush_based_on_stock_entry(self):
|
||||||
item_code = "_Test Subcontracted FG Item 1"
|
item_code = "_Test Subcontracted FG Item 1"
|
||||||
@ -840,8 +840,8 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
transferred_items = sorted([d.item_code for d in se.get('items') if se.purchase_order == po.name])
|
transferred_items = sorted([d.item_code for d in se.get('items') if se.purchase_order == po.name])
|
||||||
issued_items = sorted([d.rm_item_code for d in pr.get('supplied_items')])
|
issued_items = sorted([d.rm_item_code for d in pr.get('supplied_items')])
|
||||||
|
|
||||||
self.assertEquals(transferred_items, issued_items)
|
self.assertEqual(transferred_items, issued_items)
|
||||||
self.assertEquals(pr.get('items')[0].rm_supp_cost, 2000)
|
self.assertEqual(pr.get('items')[0].rm_supp_cost, 2000)
|
||||||
|
|
||||||
|
|
||||||
transferred_rm_map = frappe._dict()
|
transferred_rm_map = frappe._dict()
|
||||||
|
@ -62,6 +62,7 @@ class RequestforQuotation(BuyingController):
|
|||||||
for supplier in self.suppliers:
|
for supplier in self.suppliers:
|
||||||
supplier.email_sent = 0
|
supplier.email_sent = 0
|
||||||
supplier.quote_status = 'Pending'
|
supplier.quote_status = 'Pending'
|
||||||
|
self.send_to_supplier()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
frappe.db.set(self, 'status', 'Cancelled')
|
frappe.db.set(self, 'status', 'Cancelled')
|
||||||
@ -81,7 +82,7 @@ class RequestforQuotation(BuyingController):
|
|||||||
def send_to_supplier(self):
|
def send_to_supplier(self):
|
||||||
"""Sends RFQ mail to involved suppliers."""
|
"""Sends RFQ mail to involved suppliers."""
|
||||||
for rfq_supplier in self.suppliers:
|
for rfq_supplier in self.suppliers:
|
||||||
if rfq_supplier.send_email:
|
if rfq_supplier.email_id is not None and rfq_supplier.send_email:
|
||||||
self.validate_email_id(rfq_supplier)
|
self.validate_email_id(rfq_supplier)
|
||||||
|
|
||||||
# make new user if required
|
# make new user if required
|
||||||
|
@ -383,8 +383,14 @@
|
|||||||
"icon": "fa fa-user",
|
"icon": "fa fa-user",
|
||||||
"idx": 370,
|
"idx": 370,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2021-01-06 19:51:40.939087",
|
{
|
||||||
|
"group": "Item Group",
|
||||||
|
"link_doctype": "Supplier Item Group",
|
||||||
|
"link_fieldname": "supplier"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-05-18 15:10:11.087191",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier",
|
"name": "Supplier",
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Supplier Item Group', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-07 18:16:40.621421",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"supplier",
|
||||||
|
"item_group"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "supplier",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Supplier",
|
||||||
|
"options": "Supplier",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item Group",
|
||||||
|
"options": "Item Group",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-05-19 13:48:16.742303",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Buying",
|
||||||
|
"name": "Supplier Item Group",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Purchase User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Purchase Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
# -*- 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 import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class SupplierItemGroup(Document):
|
||||||
|
def validate(self):
|
||||||
|
exists = frappe.db.exists({
|
||||||
|
'doctype': 'Supplier Item Group',
|
||||||
|
'supplier': self.supplier,
|
||||||
|
'item_group': self.item_group
|
||||||
|
})
|
||||||
|
if exists:
|
||||||
|
frappe.throw(_("Item Group has already been linked to this supplier."))
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestSupplierItemGroup(unittest.TestCase):
|
||||||
|
pass
|
@ -9,12 +9,12 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
|||||||
from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import execute
|
from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import execute
|
||||||
import json, frappe, unittest
|
import json, frappe, unittest
|
||||||
|
|
||||||
class TestSubcontractedItemToBeReceived(unittest.TestCase):
|
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
|
||||||
|
|
||||||
def test_pending_and_received_qty(self):
|
def test_pending_and_transferred_qty(self):
|
||||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
|
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
|
||||||
make_stock_entry(item_code='_Test Item', target='_Test Warehouse 1 - _TC', qty=100, basic_rate=100)
|
make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
|
||||||
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse 1 - _TC', qty=100, basic_rate=100)
|
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
|
||||||
transfer_subcontracted_raw_materials(po.name)
|
transfer_subcontracted_raw_materials(po.name)
|
||||||
col, data = execute(filters=frappe._dict({'supplier': po.supplier,
|
col, data = execute(filters=frappe._dict({'supplier': po.supplier,
|
||||||
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
|
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
|
||||||
@ -38,6 +38,7 @@ def transfer_subcontracted_raw_materials(po):
|
|||||||
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
|
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
|
||||||
rm_item_string = json.dumps(rm_item)
|
rm_item_string = json.dumps(rm_item)
|
||||||
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
|
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
|
||||||
|
se.from_warehouse = '_Test Warehouse 1 - _TC'
|
||||||
se.to_warehouse = '_Test Warehouse 1 - _TC'
|
se.to_warehouse = '_Test Warehouse 1 - _TC'
|
||||||
se.stock_entry_type = 'Send to Subcontractor'
|
se.stock_entry_type = 'Send to Subcontractor'
|
||||||
se.save()
|
se.save()
|
||||||
|
@ -225,7 +225,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
def validate_date_with_fiscal_year(self):
|
def validate_date_with_fiscal_year(self):
|
||||||
if self.meta.get_field("fiscal_year"):
|
if self.meta.get_field("fiscal_year"):
|
||||||
date_field = ""
|
date_field = None
|
||||||
if self.meta.get_field("posting_date"):
|
if self.meta.get_field("posting_date"):
|
||||||
date_field = "posting_date"
|
date_field = "posting_date"
|
||||||
elif self.meta.get_field("transaction_date"):
|
elif self.meta.get_field("transaction_date"):
|
||||||
@ -368,6 +368,11 @@ class AccountsController(TransactionBase):
|
|||||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
|
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
|
||||||
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
|
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
|
||||||
|
|
||||||
|
# Double check for cost center
|
||||||
|
# Items add via promotional scheme may not have cost center set
|
||||||
|
if hasattr(item, 'cost_center') and not item.get('cost_center'):
|
||||||
|
item.set('cost_center', self.get('cost_center') or erpnext.get_default_cost_center(self.company))
|
||||||
|
|
||||||
if ret.get("pricing_rules"):
|
if ret.get("pricing_rules"):
|
||||||
self.apply_pricing_rule_on_items(item, ret)
|
self.apply_pricing_rule_on_items(item, ret)
|
||||||
self.set_pricing_rule_details(item, ret)
|
self.set_pricing_rule_details(item, ret)
|
||||||
@ -1006,7 +1011,6 @@ class AccountsController(TransactionBase):
|
|||||||
else:
|
else:
|
||||||
grand_total -= self.get("total_advance")
|
grand_total -= self.get("total_advance")
|
||||||
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
|
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
|
||||||
print(grand_total, base_grand_total)
|
|
||||||
if total != flt(grand_total, self.precision("grand_total")) or \
|
if total != flt(grand_total, self.precision("grand_total")) or \
|
||||||
base_total != flt(base_grand_total, self.precision("base_grand_total")):
|
base_total != flt(base_grand_total, self.precision("base_grand_total")):
|
||||||
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
|
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
|
||||||
@ -1445,6 +1449,7 @@ def validate_and_delete_children(parent, data):
|
|||||||
for d in deleted_children:
|
for d in deleted_children:
|
||||||
update_bin_on_delete(d, parent.doctype)
|
update_bin_on_delete(d, parent.doctype)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||||
def check_doc_permissions(doc, perm_type='create'):
|
def check_doc_permissions(doc, perm_type='create'):
|
||||||
|
@ -838,9 +838,10 @@ class BuyingController(StockController):
|
|||||||
if not self.get("items"):
|
if not self.get("items"):
|
||||||
return
|
return
|
||||||
|
|
||||||
earliest_schedule_date = min([d.schedule_date for d in self.get("items")])
|
if any(d.schedule_date for d in self.get("items")):
|
||||||
if earliest_schedule_date:
|
# Select earliest schedule_date.
|
||||||
self.schedule_date = earliest_schedule_date
|
self.schedule_date = min(d.schedule_date for d in self.get("items")
|
||||||
|
if d.schedule_date is not None)
|
||||||
|
|
||||||
if self.schedule_date:
|
if self.schedule_date:
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
import erpnext
|
import erpnext
|
||||||
|
import json
|
||||||
from frappe.desk.reportview import get_match_cond, get_filters_cond
|
from frappe.desk.reportview import get_match_cond, get_filters_cond
|
||||||
from frappe.utils import nowdate, getdate
|
from frappe.utils import nowdate, getdate
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@ -198,6 +199,9 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||||
conditions = []
|
conditions = []
|
||||||
|
|
||||||
|
if isinstance(filters, str):
|
||||||
|
filters = json.loads(filters)
|
||||||
|
|
||||||
#Get searchfields from meta and use in Item Link field query
|
#Get searchfields from meta and use in Item Link field query
|
||||||
meta = frappe.get_meta("Item", cached=True)
|
meta = frappe.get_meta("Item", cached=True)
|
||||||
searchfields = meta.get_search_fields()
|
searchfields = meta.get_search_fields()
|
||||||
@ -216,11 +220,23 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
|||||||
if not field in searchfields]
|
if not field in searchfields]
|
||||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||||
|
|
||||||
|
if filters and isinstance(filters, dict) and filters.get('supplier'):
|
||||||
|
item_group_list = frappe.get_all('Supplier Item Group',
|
||||||
|
filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
|
||||||
|
|
||||||
|
item_groups = []
|
||||||
|
for i in item_group_list:
|
||||||
|
item_groups.append(i.item_group)
|
||||||
|
|
||||||
|
del filters['supplier']
|
||||||
|
|
||||||
|
if item_groups:
|
||||||
|
filters['item_group'] = ['in', item_groups]
|
||||||
|
|
||||||
description_cond = ''
|
description_cond = ''
|
||||||
if frappe.db.count('Item', cache=True) < 50000:
|
if frappe.db.count('Item', cache=True) < 50000:
|
||||||
# scan description only if items are less than 50000
|
# scan description only if items are less than 50000
|
||||||
description_cond = 'or tabItem.description LIKE %(txt)s'
|
description_cond = 'or tabItem.description LIKE %(txt)s'
|
||||||
|
|
||||||
return frappe.db.sql("""select tabItem.name,
|
return frappe.db.sql("""select tabItem.name,
|
||||||
if(length(tabItem.item_name) > 40,
|
if(length(tabItem.item_name) > 40,
|
||||||
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name,
|
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name,
|
||||||
@ -292,11 +308,14 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
cond = """(`tabProject`.customer = %s or
|
cond = """(`tabProject`.customer = %s or
|
||||||
ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer")))
|
ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer")))
|
||||||
|
|
||||||
fields = get_fields("Project", ["name"])
|
fields = get_fields("Project", ["name", "project_name"])
|
||||||
|
searchfields = frappe.get_meta("Project").get_search_fields()
|
||||||
|
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||||
|
|
||||||
return frappe.db.sql("""select {fields} from `tabProject`
|
return frappe.db.sql("""select {fields} from `tabProject`
|
||||||
where `tabProject`.status not in ("Completed", "Cancelled")
|
where
|
||||||
and {cond} `tabProject`.name like %(txt)s {match_cond}
|
`tabProject`.status not in ("Completed", "Cancelled")
|
||||||
|
and {cond} {match_cond} {scond}
|
||||||
order by
|
order by
|
||||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
@ -304,6 +323,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
limit {start}, {page_len}""".format(
|
limit {start}, {page_len}""".format(
|
||||||
fields=", ".join(['`tabProject`.{0}'.format(f) for f in fields]),
|
fields=", ".join(['`tabProject`.{0}'.format(f) for f in fields]),
|
||||||
cond=cond,
|
cond=cond,
|
||||||
|
scond=searchfields,
|
||||||
match_cond=get_match_cond(doctype),
|
match_cond=get_match_cond(doctype),
|
||||||
start=start,
|
start=start,
|
||||||
page_len=page_len), {
|
page_len=page_len), {
|
||||||
|
@ -76,12 +76,12 @@ status_map = {
|
|||||||
["Stopped", "eval:self.status == 'Stopped'"],
|
["Stopped", "eval:self.status == 'Stopped'"],
|
||||||
["Cancelled", "eval:self.docstatus == 2"],
|
["Cancelled", "eval:self.docstatus == 2"],
|
||||||
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
|
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
|
||||||
["Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1"],
|
|
||||||
["Ordered", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
["Ordered", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
||||||
["Transferred", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'"],
|
["Transferred", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'"],
|
||||||
["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"],
|
["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"],
|
||||||
["Received", "eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
["Received", "eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
||||||
["Partially Received", "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
["Partially Received", "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
||||||
|
["Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1"],
|
||||||
["Manufactured", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'"]
|
["Manufactured", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'"]
|
||||||
],
|
],
|
||||||
"Bank Transaction": [
|
"Bank Transaction": [
|
||||||
@ -98,7 +98,12 @@ status_map = {
|
|||||||
["Draft", None],
|
["Draft", None],
|
||||||
["Submitted", "eval:self.docstatus == 1"],
|
["Submitted", "eval:self.docstatus == 1"],
|
||||||
["Queued", "eval:self.status == 'Queued'"],
|
["Queued", "eval:self.status == 'Queued'"],
|
||||||
|
["Failed", "eval:self.status == 'Failed'"],
|
||||||
["Cancelled", "eval:self.docstatus == 2"],
|
["Cancelled", "eval:self.docstatus == 2"],
|
||||||
|
],
|
||||||
|
"Transaction Deletion Record": [
|
||||||
|
["Draft", None],
|
||||||
|
["Completed", "eval:self.docstatus == 1"],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
import json
|
||||||
import frappe, erpnext
|
|
||||||
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
|
|
||||||
from frappe import _
|
|
||||||
import frappe.defaults
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
|
|
||||||
|
import frappe
|
||||||
|
import frappe.defaults
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
|
||||||
|
|
||||||
|
import erpnext
|
||||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
||||||
|
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
|
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||||
|
|
||||||
|
|
||||||
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
||||||
class QualityInspectionRejectedError(frappe.ValidationError): pass
|
class QualityInspectionRejectedError(frappe.ValidationError): pass
|
||||||
@ -189,7 +193,6 @@ class StockController(AccountsController):
|
|||||||
if hasattr(self, "items"):
|
if hasattr(self, "items"):
|
||||||
item_doclist = self.get("items")
|
item_doclist = self.get("items")
|
||||||
elif self.doctype == "Stock Reconciliation":
|
elif self.doctype == "Stock Reconciliation":
|
||||||
import json
|
|
||||||
item_doclist = []
|
item_doclist = []
|
||||||
data = json.loads(self.reconciliation_json)
|
data = json.loads(self.reconciliation_json)
|
||||||
for row in data[data.index(self.head_row)+1:]:
|
for row in data[data.index(self.head_row)+1:]:
|
||||||
@ -319,7 +322,7 @@ class StockController(AccountsController):
|
|||||||
return serialized_items
|
return serialized_items
|
||||||
|
|
||||||
def validate_warehouse(self):
|
def validate_warehouse(self):
|
||||||
from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
|
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
|
||||||
|
|
||||||
warehouses = list(set([d.warehouse for d in
|
warehouses = list(set([d.warehouse for d in
|
||||||
self.get("items") if getattr(d, "warehouse", None)]))
|
self.get("items") if getattr(d, "warehouse", None)]))
|
||||||
@ -379,8 +382,7 @@ class StockController(AccountsController):
|
|||||||
link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
|
link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
|
||||||
frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
|
frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
|
||||||
|
|
||||||
qa_failed = any([r.status=="Rejected" for r in qa_doc.readings])
|
if qa_doc.status != 'Accepted':
|
||||||
if qa_failed:
|
|
||||||
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
|
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
|
||||||
.format(d.idx, d.item_code), QualityInspectionRejectedError)
|
.format(d.idx, d.item_code), QualityInspectionRejectedError)
|
||||||
elif qa_required :
|
elif qa_required :
|
||||||
@ -499,6 +501,39 @@ class StockController(AccountsController):
|
|||||||
check_if_stock_and_account_balance_synced(self.posting_date,
|
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||||
self.company, self.doctype, self.name)
|
self.company, self.doctype, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_quality_inspections(doctype, docname, items):
|
||||||
|
if isinstance(items, str):
|
||||||
|
items = json.loads(items)
|
||||||
|
|
||||||
|
inspections = []
|
||||||
|
for item in items:
|
||||||
|
if flt(item.get("sample_size")) > flt(item.get("qty")):
|
||||||
|
frappe.throw(_("{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})").format(
|
||||||
|
item_name=item.get("item_name"),
|
||||||
|
sample_size=item.get("sample_size"),
|
||||||
|
accepted_quantity=item.get("qty")
|
||||||
|
))
|
||||||
|
|
||||||
|
quality_inspection = frappe.get_doc({
|
||||||
|
"doctype": "Quality Inspection",
|
||||||
|
"inspection_type": "Incoming",
|
||||||
|
"inspected_by": frappe.session.user,
|
||||||
|
"reference_type": doctype,
|
||||||
|
"reference_name": docname,
|
||||||
|
"item_code": item.get("item_code"),
|
||||||
|
"description": item.get("description"),
|
||||||
|
"sample_size": flt(item.get("sample_size")),
|
||||||
|
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
|
||||||
|
"batch_no": item.get("batch_no")
|
||||||
|
}).insert()
|
||||||
|
quality_inspection.save()
|
||||||
|
inspections.append(quality_inspection.name)
|
||||||
|
|
||||||
|
return inspections
|
||||||
|
|
||||||
|
|
||||||
def is_reposting_pending():
|
def is_reposting_pending():
|
||||||
return frappe.db.exists("Repost Item Valuation",
|
return frappe.db.exists("Repost Item Valuation",
|
||||||
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
||||||
|
@ -38,7 +38,7 @@ class Appointment(Document):
|
|||||||
number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents')
|
number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents')
|
||||||
if not number_of_agents == 0:
|
if not number_of_agents == 0:
|
||||||
if (number_of_appointments_in_same_slot >= number_of_agents):
|
if (number_of_appointments_in_same_slot >= number_of_agents):
|
||||||
frappe.throw('Time slot is not available')
|
frappe.throw(_('Time slot is not available'))
|
||||||
# Link lead
|
# Link lead
|
||||||
if not self.party:
|
if not self.party:
|
||||||
lead = self.find_lead_by_email()
|
lead = self.find_lead_by_email()
|
||||||
@ -75,10 +75,10 @@ class Appointment(Document):
|
|||||||
subject=_('Appointment Confirmation'))
|
subject=_('Appointment Confirmation'))
|
||||||
if frappe.session.user == "Guest":
|
if frappe.session.user == "Guest":
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
'Please check your email to confirm the appointment')
|
_('Please check your email to confirm the appointment'))
|
||||||
else :
|
else :
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
'Appointment was created. But no lead was found. Please check the email to confirm')
|
_('Appointment was created. But no lead was found. Please check the email to confirm'))
|
||||||
|
|
||||||
def on_change(self):
|
def on_change(self):
|
||||||
# Sync Calendar
|
# Sync Calendar
|
||||||
@ -91,7 +91,7 @@ class Appointment(Document):
|
|||||||
|
|
||||||
def set_verified(self, email):
|
def set_verified(self, email):
|
||||||
if not email == self.customer_email:
|
if not email == self.customer_email:
|
||||||
frappe.throw('Email verification failed.')
|
frappe.throw(_('Email verification failed.'))
|
||||||
# Create new lead
|
# Create new lead
|
||||||
self.create_lead_and_link()
|
self.create_lead_and_link()
|
||||||
# Remove unverified status
|
# Remove unverified status
|
||||||
|
@ -280,7 +280,6 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:",
|
|
||||||
"fieldname": "territory",
|
"fieldname": "territory",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Territory",
|
"label": "Territory",
|
||||||
@ -431,7 +430,7 @@
|
|||||||
"icon": "fa fa-info-sign",
|
"icon": "fa fa-info-sign",
|
||||||
"idx": 195,
|
"idx": 195,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-01-06 19:42:46.190051",
|
"modified": "2021-06-04 10:11:22.831139",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity",
|
"name": "Opportunity",
|
||||||
|
@ -75,7 +75,7 @@ class CourseSchedulingTool(Document):
|
|||||||
"""Validates if Course Start Date is greater than Course End Date"""
|
"""Validates if Course Start Date is greater than Course End Date"""
|
||||||
if self.course_start_date > self.course_end_date:
|
if self.course_start_date > self.course_end_date:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
"Course Start Date cannot be greater than Course End Date.")
|
_("Course Start Date cannot be greater than Course End Date."))
|
||||||
|
|
||||||
def delete_course_schedule(self, rescheduled, reschedule_errors):
|
def delete_course_schedule(self, rescheduled, reschedule_errors):
|
||||||
"""Delete all course schedule within the Date range and specified filters"""
|
"""Delete all course schedule within the Date range and specified filters"""
|
||||||
|
@ -31,9 +31,9 @@ class EducationSettings(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
if self.get('instructor_created_by')=='Naming Series':
|
if self.get('instructor_created_by')=='Naming Series':
|
||||||
make_property_setter('Instructor', "naming_series", "hidden", 0, "Check")
|
make_property_setter('Instructor', "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False)
|
||||||
else:
|
else:
|
||||||
make_property_setter('Instructor', "naming_series", "hidden", 1, "Check")
|
make_property_setter('Instructor', "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False)
|
||||||
|
|
||||||
def update_website_context(context):
|
def update_website_context(context):
|
||||||
context["lms_enabled"] = frappe.get_doc("Education Settings").enable_lms
|
context["lms_enabled"] = frappe.get_doc("Education Settings").enable_lms
|
@ -9,8 +9,7 @@ from frappe.utils import nowdate
|
|||||||
from frappe.utils.make_random import get_random
|
from frappe.utils.make_random import get_random
|
||||||
from erpnext.education.doctype.program.test_program import make_program_and_linked_courses
|
from erpnext.education.doctype.program.test_program import make_program_and_linked_courses
|
||||||
|
|
||||||
# test_records = frappe.get_test_records('Fees')
|
test_dependencies = ['Company']
|
||||||
|
|
||||||
class TestFees(unittest.TestCase):
|
class TestFees(unittest.TestCase):
|
||||||
|
|
||||||
def test_fees(self):
|
def test_fees(self):
|
||||||
|
@ -74,7 +74,6 @@ class Student(Document):
|
|||||||
student_user.flags.ignore_permissions = True
|
student_user.flags.ignore_permissions = True
|
||||||
student_user.add_roles("Student")
|
student_user.add_roles("Student")
|
||||||
student_user.save()
|
student_user.save()
|
||||||
update_password_link = student_user.reset_password()
|
|
||||||
|
|
||||||
def update_applicant_status(self):
|
def update_applicant_status(self):
|
||||||
"""Updates Student Applicant status to Admitted"""
|
"""Updates Student Applicant status to Admitted"""
|
||||||
|
@ -335,13 +335,13 @@ def get_url(shopify_settings):
|
|||||||
|
|
||||||
if not last_order_id:
|
if not last_order_id:
|
||||||
if shopify_settings.sync_based_on == 'Date':
|
if shopify_settings.sync_based_on == 'Date':
|
||||||
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&created_at_min={0}&since_id=0".format(
|
url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&created_at_min={0}&since_id=0".format(
|
||||||
get_datetime(shopify_settings.from_date)), shopify_settings)
|
get_datetime(shopify_settings.from_date)), shopify_settings)
|
||||||
else:
|
else:
|
||||||
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&since_id={0}".format(
|
url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format(
|
||||||
shopify_settings.from_order_id), shopify_settings)
|
shopify_settings.from_order_id), shopify_settings)
|
||||||
else:
|
else:
|
||||||
url = get_shopify_url("admin/api/2020-10/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings)
|
url = get_shopify_url("admin/api/2021-04/orders.json?limit=250&since_id={0}".format(last_order_id), shopify_settings)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe, base64, hashlib, hmac, json
|
import frappe, base64, hashlib, hmac, json
|
||||||
|
from frappe.utils import cstr
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
def verify_request():
|
def verify_request():
|
||||||
@ -146,20 +147,17 @@ def rename_address(address, customer):
|
|||||||
|
|
||||||
def link_items(items_list, woocommerce_settings, sys_lang):
|
def link_items(items_list, woocommerce_settings, sys_lang):
|
||||||
for item_data in items_list:
|
for item_data in items_list:
|
||||||
item_woo_com_id = item_data.get("product_id")
|
item_woo_com_id = cstr(item_data.get("product_id"))
|
||||||
|
|
||||||
if frappe.get_value("Item", {"woocommerce_id": item_woo_com_id}):
|
if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, 'name'):
|
||||||
#Edit Item
|
|
||||||
item = frappe.get_doc("Item", {"woocommerce_id": item_woo_com_id})
|
|
||||||
else:
|
|
||||||
#Create Item
|
#Create Item
|
||||||
item = frappe.new_doc("Item")
|
item = frappe.new_doc("Item")
|
||||||
|
item.item_code = _("woocommerce - {0}", sys_lang).format(item_woo_com_id)
|
||||||
|
item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang)
|
||||||
|
item.item_group = _("WooCommerce Products", sys_lang)
|
||||||
|
|
||||||
item.item_name = item_data.get("name")
|
item.item_name = item_data.get("name")
|
||||||
item.item_code = _("woocommerce - {0}", sys_lang).format(item_data.get("product_id"))
|
item.woocommerce_id = item_woo_com_id
|
||||||
item.woocommerce_id = item_data.get("product_id")
|
|
||||||
item.item_group = _("WooCommerce Products", sys_lang)
|
|
||||||
item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang)
|
|
||||||
item.flags.ignore_mandatory = True
|
item.flags.ignore_mandatory = True
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
@ -194,12 +192,12 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l
|
|||||||
|
|
||||||
for item in order.get("line_items"):
|
for item in order.get("line_items"):
|
||||||
woocomm_item_id = item.get("product_id")
|
woocomm_item_id = item.get("product_id")
|
||||||
found_item = frappe.get_doc("Item", {"woocommerce_id": woocomm_item_id})
|
found_item = frappe.get_doc("Item", {"woocommerce_id": cstr(woocomm_item_id)})
|
||||||
|
|
||||||
ordered_items_tax = item.get("total_tax")
|
ordered_items_tax = item.get("total_tax")
|
||||||
|
|
||||||
new_sales_order.append("items",{
|
new_sales_order.append("items", {
|
||||||
"item_code": found_item.item_code,
|
"item_code": found_item.name,
|
||||||
"item_name": found_item.item_name,
|
"item_name": found_item.item_name,
|
||||||
"description": found_item.item_name,
|
"description": found_item.item_name,
|
||||||
"delivery_date": new_sales_order.delivery_date,
|
"delivery_date": new_sales_order.delivery_date,
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import urllib
|
import urllib
|
||||||
|
from urllib.parse import quote
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import base64
|
import base64
|
||||||
@ -68,8 +69,9 @@ def calc_md5(string):
|
|||||||
"""
|
"""
|
||||||
md = hashlib.md5()
|
md = hashlib.md5()
|
||||||
md.update(string)
|
md.update(string)
|
||||||
return base64.encodestring(md.digest()).strip('\n') if six.PY2 \
|
return base64.encodebytes(md.digest()).decode().strip()
|
||||||
else base64.encodebytes(md.digest()).decode().strip()
|
|
||||||
|
|
||||||
|
|
||||||
def remove_empty(d):
|
def remove_empty(d):
|
||||||
"""
|
"""
|
||||||
@ -177,7 +179,6 @@ class MWS(object):
|
|||||||
'SignatureMethod': 'HmacSHA256',
|
'SignatureMethod': 'HmacSHA256',
|
||||||
}
|
}
|
||||||
params.update(extra_data)
|
params.update(extra_data)
|
||||||
quote = urllib.quote if six.PY2 else urllib.parse.quote
|
|
||||||
request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)])
|
request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)])
|
||||||
signature = self.calc_signature(method, request_description)
|
signature = self.calc_signature(method, request_description)
|
||||||
url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature))
|
url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature))
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user