Merge branch 'develop' into feature/check-field-subscription-invoice

This commit is contained in:
Raffael Meyer 2021-04-19 16:40:35 +02:00 committed by GitHub
commit 4a1159408a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
319 changed files with 4789 additions and 2453 deletions

46
.github/helper/install.sh vendored Normal file
View File

@ -0,0 +1,46 @@
#!/bin/bash
set -e
cd ~ || exit
sudo apt-get install redis-server
sudo apt install nodejs
sudo apt install npm
pip install frappe-bench
git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
sudo chmod o+x /usr/local/bin/wkhtmltopdf
sudo apt-get install libcups2-dev
cd ~/frappe-bench || exit
sed -i 's/watch:/# watch:/g' Procfile
sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
bench start &
bench --site test_site reinstall --yes

38
.github/helper/semgrep_rules/README.md vendored Normal file
View File

@ -0,0 +1,38 @@
# Semgrep linting
## What is semgrep?
Semgrep or "semantic grep" is language agnostic static analysis tool. In simple terms semgrep is syntax-aware `grep`, so unlike regex it doesn't get confused by different ways of writing same thing or whitespaces or code split in multiple lines etc.
Example:
To check if a translate function is using f-string or not the regex would be `r"_\(\s*f[\"']"` while equivalent rule in semgrep would be `_(f"...")`. As semgrep knows grammer of language it takes care of unnecessary whitespace, type of quotation marks etc.
You can read more such examples in `.github/helper/semgrep_rules` directory.
# Why/when to use this?
We want to maintain quality of contributions, at the same time remembering all the good practices can be pain to deal with while evaluating contributions. Using semgrep if you can translate "best practice" into a rule then it can automate the task for us.
## Running locally
Install semgrep using homebrew `brew install semgrep` or pip `pip install semgrep`.
To run locally use following command:
`semgrep --config=.github/helper/semgrep_rules [file/folder names]`
## Testing
semgrep allows testing the tests. Refer to this page: https://semgrep.dev/docs/writing-rules/testing-rules/
When writing new rules you should write few positive and few negative cases as shown in the guide and current tests.
To run current tests: `semgrep --test --test-ignore-todo .github/helper/semgrep_rules`
## Reference
If you are new to Semgrep read following pages to get started on writing/modifying rules:
- https://semgrep.dev/docs/getting-started/
- https://semgrep.dev/docs/writing-rules/rule-syntax
- https://semgrep.dev/docs/writing-rules/pattern-examples/
- https://semgrep.dev/docs/writing-rules/rule-ideas/#common-use-cases

View File

@ -0,0 +1,28 @@
import frappe
from frappe import _, flt
from frappe.model.document import Document
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
# ruleid: frappe-modifying-after-submit
self.status = 'Submitted'
def on_submit(self):
if flt(self.per_billed) < 100:
self.update_billing_status()
else:
# todook: frappe-modifying-after-submit
self.status = "Completed"
self.db_set("status", "Completed")
class TestDoc(Document):
pass
def validate(self):
#ruleid: frappe-modifying-child-tables-while-iterating
for item in self.child_table:
if item.value < 0:
self.remove(item)

View File

@ -0,0 +1,74 @@
# This file specifies rules for correctness according to how frappe doctype data model works.
rules:
- id: frappe-modifying-after-submit
patterns:
- pattern: self.$ATTR = ...
- pattern-inside: |
def on_submit(self, ...):
...
- metavariable-regex:
metavariable: '$ATTR'
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
regex: '^(?!status_updater)(.*)$'
message: |
Doctype modified after submission. Please check if modification of self.$ATTR is commited to database.
languages: [python]
severity: ERROR
- id: frappe-modifying-after-cancel
patterns:
- pattern: self.$ATTR = ...
- pattern-inside: |
def on_cancel(self, ...):
...
- metavariable-regex:
metavariable: '$ATTR'
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
message: |
Doctype modified after cancellation. Please check if modification of self.$ATTR is commited to database.
languages: [python]
severity: ERROR
- id: frappe-print-function-in-doctypes
pattern: print(...)
message: |
Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
languages: [python]
severity: WARNING
paths:
exclude:
- test_*.py
include:
- "*/**/doctype/*"
- id: frappe-modifying-child-tables-while-iterating
pattern-either:
- pattern: |
for $ROW in self.$TABLE:
...
self.remove(...)
- pattern: |
for $ROW in self.$TABLE:
...
self.append(...)
message: |
Child table being modified while iterating on it.
languages: [python]
severity: ERROR
paths:
include:
- "*/**/doctype/*"
- id: frappe-same-key-assigned-twice
pattern-either:
- pattern: |
{..., $X: $A, ..., $X: $B, ...}
- pattern: |
dict(..., ($X, $A), ..., ($X, $B), ...)
- pattern: |
_dict(..., ($X, $A), ..., ($X, $B), ...)
message: |
key `$X` is uselessly assigned twice. This could be a potential bug.
languages: [python]
severity: ERROR

View File

@ -0,0 +1,6 @@
def function_name(input):
# ruleid: frappe-codeinjection-eval
eval(input)
# ok: frappe-codeinjection-eval
eval("1 + 1")

View File

@ -0,0 +1,25 @@
rules:
- id: frappe-codeinjection-eval
patterns:
- pattern-not: eval("...")
- pattern: eval(...)
message: |
Detected the use of eval(). eval() can be dangerous if used to evaluate
dynamic content. Avoid it or use safe_eval().
languages: [python]
severity: ERROR
- id: frappe-sqli-format-strings
patterns:
- pattern-inside: |
@frappe.whitelist()
def $FUNC(...):
...
- pattern-either:
- pattern: frappe.db.sql("..." % ...)
- pattern: frappe.db.sql(f"...", ...)
- pattern: frappe.db.sql("...".format(...), ...)
message: |
Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
languages: [python]
severity: WARNING

View File

@ -0,0 +1,37 @@
// ruleid: frappe-translation-empty-string
__("")
// ruleid: frappe-translation-empty-string
__('')
// ok: frappe-translation-js-formatting
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
// ruleid: frappe-translation-js-formatting
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
// ok: frappe-translation-js-formatting
__('This is fine');
// ok: frappe-translation-trailing-spaces
__('This is fine');
// ruleid: frappe-translation-trailing-spaces
__(' this is not ok ');
// ruleid: frappe-translation-trailing-spaces
__('this is not ok ');
// ruleid: frappe-translation-trailing-spaces
__(' this is not ok');
// ok: frappe-translation-js-splitting
__('You have {0} subscribers in your mailing list.', [subscribers.length])
// todoruleid: frappe-translation-js-splitting
__('You have') + subscribers.length + __('subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have' + 'subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers' +
'in your mailing list', [subscribers.length])

View File

@ -0,0 +1,53 @@
# Examples taken from https://frappeframework.com/docs/user/en/translations
# This file is used for testing the tests.
from frappe import _
full_name = "Jon Doe"
# ok: frappe-translation-python-formatting
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
# ruleid: frappe-translation-python-formatting
_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
# ruleid: frappe-translation-python-formatting
_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
# ruleid: frappe-translation-python-formatting
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
subscribers = ["Jon", "Doe"]
# ok: frappe-translation-python-formatting
_('You have {0} subscribers in your mailing list.').format(len(subscribers))
# ruleid: frappe-translation-python-splitting
_('You have') + len(subscribers) + _('subscribers in your mailing list.')
# ruleid: frappe-translation-python-splitting
_('You have {0} subscribers \
in your mailing list').format(len(subscribers))
# ok: frappe-translation-python-splitting
_('You have {0} subscribers') \
+ 'in your mailing list'
# ruleid: frappe-translation-trailing-spaces
msg = _(" You have {0} pending invoice ")
# ruleid: frappe-translation-trailing-spaces
msg = _("You have {0} pending invoice ")
# ruleid: frappe-translation-trailing-spaces
msg = _(" You have {0} pending invoice")
# ok: frappe-translation-trailing-spaces
msg = ' ' + _("You have {0} pending invoices") + ' '
# ruleid: frappe-translation-python-formatting
_(f"can not format like this - {subscribers}")
# ruleid: frappe-translation-python-splitting
_(f"what" + f"this is also not cool")
# ruleid: frappe-translation-empty-string
_("")
# ruleid: frappe-translation-empty-string
_('')

View File

@ -0,0 +1,63 @@
rules:
- id: frappe-translation-empty-string
pattern-either:
- pattern: _("")
- pattern: __("")
message: |
Empty string is useless for translation.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python, javascript, json]
severity: ERROR
- id: frappe-translation-trailing-spaces
pattern-either:
- pattern: _("=~/(^[ \t]+|[ \t]+$)/")
- pattern: __("=~/(^[ \t]+|[ \t]+$)/")
message: |
Trailing or leading whitespace not allowed in translate strings.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python, javascript, json]
severity: ERROR
- id: frappe-translation-python-formatting
pattern-either:
- pattern: _("..." % ...)
- pattern: _("...".format(...))
- pattern: _(f"...")
message: |
Only positional formatters are allowed and formatting should not be done before translating.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python]
severity: ERROR
- id: frappe-translation-js-formatting
patterns:
- pattern: __(`...`)
- pattern-not: __("...")
message: |
Template strings are not allowed for text formatting.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [javascript, json]
severity: ERROR
- id: frappe-translation-python-splitting
pattern-either:
- pattern: _(...) + ... + _(...)
- pattern: _("..." + "...")
- pattern-regex: '_\([^\)]*\\\s*'
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [python]
severity: ERROR
- id: frappe-translation-js-splitting
pattern-either:
- pattern-regex: '__\([^\)]*[\+\\]\s*'
- pattern: __('...' + '...')
- pattern: __('...') + __('...')
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
languages: [javascript, json]
severity: ERROR

31
.github/helper/semgrep_rules/ux.py vendored Normal file
View File

@ -0,0 +1,31 @@
import frappe
from frappe import msgprint, throw, _
# ruleid: frappe-missing-translate-function
throw("Error Occured")
# ruleid: frappe-missing-translate-function
frappe.throw("Error Occured")
# ruleid: frappe-missing-translate-function
frappe.msgprint("Useful message")
# ruleid: frappe-missing-translate-function
msgprint("Useful message")
# ok: frappe-missing-translate-function
translatedmessage = _("Hello")
# ok: frappe-missing-translate-function
throw(translatedmessage)
# ok: frappe-missing-translate-function
msgprint(translatedmessage)
# ok: frappe-missing-translate-function
msgprint(_("Helpful message"))
# ok: frappe-missing-translate-function
frappe.throw(_("Error occured"))

15
.github/helper/semgrep_rules/ux.yml vendored Normal file
View File

@ -0,0 +1,15 @@
rules:
- id: frappe-missing-translate-function
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(_("..."), ...)
- pattern-not: frappe.msgprint(__("..."), ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(_("..."), ...)
- pattern-not: frappe.throw(__("..."), ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [python, javascript, json]
severity: ERROR

View File

@ -1,4 +1,6 @@
{ {
"db_host": "127.0.0.1",
"db_port": 3306,
"db_name": "test_frappe", "db_name": "test_frappe",
"db_password": "test_frappe", "db_password": "test_frappe",
"auto_email_id": "test@example.com", "auto_email_id": "test@example.com",

94
.github/workflows/ci-tests.yml vendored Normal file
View File

@ -0,0 +1,94 @@
name: CI
on: [pull_request, workflow_dispatch, push]
jobs:
test:
runs-on: ubuntu-18.04
strategy:
fail-fast: false
matrix:
include:
- TYPE: "server"
JOB_NAME: "Server"
RUN_COMMAND: cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --coverage
- TYPE: "patch"
JOB_NAME: "Patch"
RUN_COMMAND: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate
name: ${{ matrix.JOB_NAME }}
services:
mysql:
image: mariadb:10.3
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Tests
run: ${{ matrix.RUN_COMMAND }}
env:
TYPE: ${{ matrix.TYPE }}
- name: Coverage
if: matrix.TYPE == 'server'
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0
pip install coverage==4.5.4
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}

24
.github/workflows/semgrep.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Semgrep
on:
pull_request:
branches:
- develop
jobs:
semgrep:
name: Frappe Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup python3
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Run semgrep
run: |
python -m pip install -q semgrep
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
semgrep --config="r/python.lang.correctness" --quiet --error $files
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files

View File

@ -1,69 +0,0 @@
language: python
dist: trusty
git:
depth: 1
cache:
- pip
addons:
hosts: test_site
mariadb: 10.3
jobs:
include:
- name: "Python 3.6 Server Side Test"
python: 3.6
script: bench --site test_site run-tests --app erpnext --coverage
- name: "Python 3.6 Patch Test"
python: 3.6
before_script:
- wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz
- bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz
script: bench --site test_site migrate
install:
- cd ~
- nvm install 10
- pip install frappe-bench
- git clone https://github.com/frappe/frappe --branch $TRAVIS_BRANCH --depth 1
- bench init --skip-assets --frappe-path ~/frappe --python $(which python) frappe-bench
- mkdir ~/frappe-bench/sites/test_site
- cp -r $TRAVIS_BUILD_DIR/.travis/site_config.json ~/frappe-bench/sites/test_site/
- mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
- mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
- mysql -u root -e "CREATE DATABASE test_frappe"
- mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
- mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
- mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
- mysql -u root -e "FLUSH PRIVILEGES"
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
- sudo apt-get install libcups2-dev
- cd ~/frappe-bench
- sed -i 's/watch:/# watch:/g' Procfile
- sed -i 's/schedule:/# schedule:/g' Procfile
- sed -i 's/socketio:/# socketio:/g' Procfile
- sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
- bench get-app erpnext $TRAVIS_BUILD_DIR
- bench start &
- bench --site test_site reinstall --yes
after_script:
- pip install coverage==4.5.4
- pip install python-coveralls
- coveralls -b apps/erpnext -d ../../sites/.coverage

View File

@ -5,7 +5,7 @@
<p>ERP made simple</p> <p>ERP made simple</p>
</p> </p>
[![Build Status](https://api.travis-ci.com/frappe/erpnext.svg?branch=develop)](https://travis-ci.com/frappe/erpnext) [![CI](https://github.com/frappe/erpnext/actions/workflows/ci-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/ci-tests.yml)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext) [![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop)

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.0.2' __version__ = '13.0.0-dev'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -214,6 +214,7 @@ class Account(NestedSet):
if parent_value_changed: if parent_value_changed:
doc.save() doc.save()
@frappe.whitelist()
def convert_group_to_ledger(self): def convert_group_to_ledger(self):
if self.check_if_child_exists(): if self.check_if_child_exists():
throw(_("Account with child nodes cannot be converted to ledger")) throw(_("Account with child nodes cannot be converted to ledger"))
@ -224,6 +225,7 @@ class Account(NestedSet):
self.save() self.save()
return 1 return 1
@frappe.whitelist()
def convert_ledger_to_group(self): def convert_ledger_to_group(self):
if self.check_gle_exists(): if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group.")) throw(_("Account with existing transaction can not be converted to group."))

View File

@ -39,6 +39,7 @@ class AccountingPeriod(Document):
frappe.throw(_("Accounting Period overlaps with {0}") frappe.throw(_("Accounting Period overlaps with {0}")
.format(existing_accounting_period[0].get("name")), OverlapError) .format(existing_accounting_period[0].get("name")), OverlapError)
@frappe.whitelist()
def get_doctypes_for_closing(self): def get_doctypes_for_closing(self):
docs_for_closing = [] docs_for_closing = []
doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \ doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \

View File

@ -11,36 +11,36 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
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",
end_date = "2018-06-30", company = "Wind Power LLC") end_date = "2018-06-30", company = "Wind Power LLC")
ap1.save() ap1.save()
ap2 = create_accounting_period(start_date = "2018-06-30", ap2 = create_accounting_period(start_date = "2018-06-30",
end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1") end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
self.assertRaises(OverlapError, ap2.save) self.assertRaises(OverlapError, ap2.save)
def test_accounting_period(self): def test_accounting_period(self):
ap1 = create_accounting_period(period_name = "Test Accounting Period 2") ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
ap1.save() ap1.save()
doc = create_sales_invoice(do_not_submit=1, cost_center = "_Test Company - _TC", warehouse = "Stores - _TC") doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
self.assertRaises(ClosedAccountingPeriod, doc.submit) self.assertRaises(ClosedAccountingPeriod, doc.submit)
def tearDown(self): def tearDown(self):
for d in frappe.get_all("Accounting Period"): for d in frappe.get_all("Accounting Period"):
frappe.delete_doc("Accounting Period", d.name) frappe.delete_doc("Accounting Period", d.name)
def create_accounting_period(**args): def create_accounting_period(**args):
args = frappe._dict(args) args = frappe._dict(args)
accounting_period = frappe.new_doc("Accounting Period") accounting_period = frappe.new_doc("Accounting Period")
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
}) })
return accounting_period return accounting_period

View File

@ -12,6 +12,7 @@
"frozen_accounts_modifier", "frozen_accounts_modifier",
"determine_address_tax_category_from", "determine_address_tax_category_from",
"over_billing_allowance", "over_billing_allowance",
"role_allowed_to_over_bill",
"column_break_4", "column_break_4",
"credit_controller", "credit_controller",
"check_supplier_invoice_uniqueness", "check_supplier_invoice_uniqueness",
@ -226,6 +227,13 @@
"fieldname": "delete_linked_ledger_entries", "fieldname": "delete_linked_ledger_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction" "label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
},
{
"description": "Users with this role are allowed to over bill above the allowance percentage",
"fieldname": "role_allowed_to_over_bill",
"fieldtype": "Link",
"label": "Role Allowed to Over Bill ",
"options": "Role"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -233,7 +241,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-01-05 13:04:00.118892", "modified": "2021-03-11 18:52:05.601996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -42,10 +42,9 @@ let add_fields_to_mapping_table = function (frm) {
}); });
}); });
frappe.meta.get_docfield("Bank Transaction Mapping", "bank_transaction_field", frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
frm.doc.name).options = options; 'bank_transaction_field', 'options', options
);
frm.fields_dict.bank_transaction_mapping.grid.refresh();
}; };
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {

View File

@ -12,6 +12,7 @@ form_grid_templates = {
} }
class BankClearance(Document): class BankClearance(Document):
@frappe.whitelist()
def get_payment_entries(self): def get_payment_entries(self):
if not (self.from_date and self.to_date): if not (self.from_date and self.to_date):
frappe.throw(_("From Date and To Date are Mandatory")) frappe.throw(_("From Date and To Date are Mandatory"))
@ -108,6 +109,7 @@ class BankClearance(Document):
row.update(d) row.update(d)
self.total_amount += flt(amount) self.total_amount += flt(amount)
@frappe.whitelist()
def update_clearance_date(self): def update_clearance_date(self):
clearance_date_updated = False clearance_date_updated = False
for d in self.get('payment_entries'): for d in self.get('payment_entries'):

View File

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

View File

@ -532,43 +532,4 @@ frappe.ui.form.on("Bank Statement Import", {
</table> </table>
`); `);
}, },
show_missing_link_values(frm, missing_link_values) {
let can_be_created_automatically = missing_link_values.every(
(d) => d.has_one_mandatory_field
);
let html = missing_link_values
.map((d) => {
let doctype = d.doctype;
let values = d.missing_values;
return `
<h5>${doctype}</h5>
<ul>${values.map((v) => `<li>${v}</li>`).join("")}</ul>
`;
})
.join("");
if (can_be_created_automatically) {
// prettier-ignore
let message = __('There are some linked records which needs to be created before we can import your file. Do you want to create the following missing records automatically?');
frappe.confirm(message + html, () => {
frm.call("create_missing_link_values", {
missing_link_values,
}).then((r) => {
let records = r.message;
frappe.msgprint(__(
"Created {0} records successfully.", [
records.length,
]
));
});
});
} else {
frappe.msgprint(
// prettier-ignore
__('The following records needs to be created before we can import your file.') + html
);
}
},
}); });

View File

@ -175,22 +175,24 @@
}, },
{ {
"fieldname": "deposit", "fieldname": "deposit",
"oldfieldname": "debit",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Deposit" "label": "Deposit",
"oldfieldname": "debit",
"options": "currency"
}, },
{ {
"fieldname": "withdrawal", "fieldname": "withdrawal",
"oldfieldname": "credit",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Withdrawal" "label": "Withdrawal",
"oldfieldname": "credit",
"options": "currency"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-30 19:40:54.221070", "modified": "2021-04-14 17:31:58.963529",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Transaction", "name": "Bank Transaction",

View File

@ -15,12 +15,14 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
test_dependencies = ["Item", "Cost Center"] test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase): class TestBankTransaction(unittest.TestCase):
def setUp(self): @classmethod
def setUpClass(cls):
make_pos_profile() make_pos_profile()
add_transactions() add_transactions()
add_vouchers() add_vouchers()
def tearDown(self): @classmethod
def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"): for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name) doc = frappe.get_doc("Bank Transaction", bt.name)
doc.cancel() doc.cancel()
@ -33,9 +35,6 @@ class TestBankTransaction(unittest.TestCase):
# Delete POS Profile # Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
frappe.flags.test_bank_transactions_created = False
frappe.flags.test_payments_created = False
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self): def test_linked_payments(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic")) bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
@ -44,8 +43,8 @@ class TestBankTransaction(unittest.TestCase):
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self): def test_reconcile(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps([{ vouchers = json.dumps([{
"payment_doctype":"Payment Entry", "payment_doctype":"Payment Entry",
"payment_name":payment.name, "payment_name":payment.name,
@ -62,7 +61,6 @@ class TestBankTransaction(unittest.TestCase):
def test_debit_credit_output(self): def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match']) linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
print(linked_payments)
self.assertTrue(linked_payments[0][3]) self.assertTrue(linked_payments[0][3])
# Check error if already reconciled # Check error if already reconciled
@ -116,10 +114,6 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
pass pass
def add_transactions(): def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
create_bank_account() create_bank_account()
doc = frappe.get_doc({ doc = frappe.get_doc({
@ -172,14 +166,8 @@ def add_transactions():
}).insert() }).insert()
doc.submit() doc.submit()
frappe.flags.test_bank_transactions_created = True
def add_vouchers(): def add_vouchers():
if frappe.flags.test_payments_created:
return
frappe.set_user("Administrator")
try: try:
frappe.get_doc({ frappe.get_doc({
"doctype": "Supplier", "doctype": "Supplier",
@ -272,13 +260,6 @@ def add_vouchers():
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Fayva Oct 18"
pe.reference_date = "2018-10-29"
pe.insert()
pe.submit()
mode_of_payment = frappe.get_doc({ mode_of_payment = frappe.get_doc({
"doctype": "Mode of Payment", "doctype": "Mode of Payment",
"name": "Cash" "name": "Cash"
@ -291,14 +272,12 @@ def add_vouchers():
}) })
mode_of_payment.save() mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_submit=1) si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1 si.is_pos = 1
si.append("payments", { si.append("payments", {
"mode_of_payment": "Cash", "mode_of_payment": "Cash",
"account": "_Test Bank - _TC", "account": "_Test Bank - _TC",
"amount": 109080 "amount": 109080
}) })
si.save() si.insert()
si.submit() si.submit()
frappe.flags.test_payments_created = True

View File

@ -57,6 +57,7 @@ class CForm(Document):
total = sum([flt(d.grand_total) for d in self.get('invoices')]) total = sum([flt(d.grand_total) for d in self.get('invoices')])
frappe.db.set(self, 'total_invoiced_amount', total) frappe.db.set(self, 'total_invoiced_amount', total)
@frappe.whitelist()
def get_invoice_details(self, invoice_no): def get_invoice_details(self, invoice_no):
""" Pull details from invoices for referrence """ """ Pull details from invoices for referrence """
if invoice_no: if invoice_no:

View File

@ -293,6 +293,11 @@ 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"):
msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
msg += "<br><br>"
msg += _("Alternatively, you can download the template and fill your data in.")
frappe.throw(msg, title=_("Parent Account Missing"))
if account["parent_account"] and accounts_dict.get(account["parent_account"]): if account["parent_account"] and accounts_dict.get(account["parent_account"]):
accounts_dict[account["parent_account"]]["is_group"] = 1 accounts_dict[account["parent_account"]]["is_group"] = 1

View File

@ -50,6 +50,7 @@ class CostCenter(NestedSet):
frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format( frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format(
frappe.bold(self.parent_cost_center))) frappe.bold(self.parent_cost_center)))
@frappe.whitelist()
def convert_group_to_ledger(self): def convert_group_to_ledger(self):
if self.check_if_child_exists(): if self.check_if_child_exists():
frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes")) frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))
@ -60,6 +61,7 @@ class CostCenter(NestedSet):
self.save() self.save()
return 1 return 1
@frappe.whitelist()
def convert_ledger_to_group(self): def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center): if cint(self.enable_distributed_cost_center):
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group")) frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))

View File

@ -27,6 +27,7 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date): if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries")) frappe.throw(_("Please select Company and Posting Date to getting entries"))
@frappe.whitelist()
def get_accounts_data(self, account=None): def get_accounts_data(self, account=None):
accounts = [] accounts = []
self.validate_mandatory() self.validate_mandatory()
@ -95,6 +96,7 @@ class ExchangeRateRevaluation(Document):
message = _("No outstanding invoices found") message = _("No outstanding invoices found")
frappe.msgprint(message) frappe.msgprint(message)
@frappe.whitelist()
def make_jv_entry(self): def make_jv_entry(self):
if self.total_gain_loss == 0: if self.total_gain_loss == 0:
return return

View File

@ -12,6 +12,7 @@ from frappe.model.document import Document
class FiscalYearIncorrectDate(frappe.ValidationError): pass class FiscalYearIncorrectDate(frappe.ValidationError): pass
class FiscalYear(Document): class FiscalYear(Document):
@frappe.whitelist()
def set_as_default(self): def set_as_default(self):
frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name) frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name)
global_defaults = frappe.get_doc("Global Defaults") global_defaults = frappe.get_doc("Global Defaults")
@ -54,7 +55,7 @@ class FiscalYear(Document):
def on_update(self): def on_update(self):
check_duplicate_fiscal_year(self) check_duplicate_fiscal_year(self)
frappe.cache().delete_value("fiscal_years") frappe.cache().delete_value("fiscal_years")
def on_trash(self): def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults") global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name: if global_defaults.current_fiscal_year == self.name:

View File

@ -290,4 +290,8 @@ def rename_temporarily_named_docs(doctype):
oldname = doc.name oldname = doc.name
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc) set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
newname = doc.name newname = doc.name
frappe.db.sql("""UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s""".format(doctype), (newname, oldname)) frappe.db.sql(
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
(newname, oldname),
auto_commit=True
)

View File

@ -125,6 +125,7 @@ class InvoiceDiscounting(AccountsController):
make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No') make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No')
@frappe.whitelist()
def create_disbursement_entry(self): def create_disbursement_entry(self):
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry' je.voucher_type = 'Journal Entry'
@ -174,6 +175,7 @@ class InvoiceDiscounting(AccountsController):
return je return je
@frappe.whitelist()
def close_loan(self): def close_loan(self):
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry' je.voucher_type = 'Journal Entry'

View File

@ -327,18 +327,16 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
}, },
setup_balance_formatter: function() { setup_balance_formatter: function() {
var me = this; const formatter = function(value, df, options, doc) {
$.each(["balance", "party_balance"], function(i, field) { var currency = frappe.meta.get_field_currency(df, doc);
var df = frappe.meta.get_docfield("Journal Entry Account", field, me.frm.doc.name); var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : "";
df.formatter = function(value, df, options, doc) { return "<div style='text-align: right'>"
var currency = frappe.meta.get_field_currency(df, doc); + ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency))
var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : ""; + " " + dr_or_cr
return "<div style='text-align: right'>" + "</div>";
+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency)) };
+ " " + dr_or_cr this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter);
+ "</div>"; this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter);
}
})
}, },
reference_name: function(doc, cdt, cdn) { reference_name: function(doc, cdt, cdn) {
@ -431,15 +429,6 @@ cur_frm.cscript.validate = function(doc,cdt,cdn) {
cur_frm.cscript.update_totals(doc); cur_frm.cscript.update_totals(doc);
} }
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading
cur_frm.pformat.print_heading = doc.select_print_heading;
}
else
cur_frm.pformat.print_heading = __("Journal Entry");
}
frappe.ui.form.on("Journal Entry Account", { frappe.ui.form.on("Journal Entry Account", {
party: function(frm, cdt, cdn) { party: function(frm, cdt, cdn) {
var d = frappe.get_doc(cdt, cdn); var d = frappe.get_doc(cdt, cdn);
@ -511,8 +500,11 @@ $.extend(erpnext.journal_entry, {
}; };
$.each(field_label_map, function (fieldname, label) { $.each(field_label_map, function (fieldname, label) {
var df = frappe.meta.get_docfield("Journal Entry Account", fieldname, frm.doc.name); frm.fields_dict.accounts.grid.update_docfield_property(
df.label = frm.doc.multi_currency ? (label + " in Account Currency") : label; fieldname,
'label',
frm.doc.multi_currency ? (label + " in Account Currency") : label
);
}) })
}, },

View File

@ -564,6 +564,7 @@ class JournalEntry(AccountsController):
if gl_map: if gl_map:
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist()
def get_balance(self): def get_balance(self):
if not self.get('accounts'): if not self.get('accounts'):
msgprint(_("'Entries' cannot be empty"), raise_exception=True) msgprint(_("'Entries' cannot be empty"), raise_exception=True)

View File

@ -8,6 +8,7 @@ from frappe.utils import (flt, add_months)
from frappe.model.document import Document from frappe.model.document import Document
class MonthlyDistribution(Document): class MonthlyDistribution(Document):
@frappe.whitelist()
def get_months(self): def get_months(self):
month_list = ['January','February','March','April','May','June','July','August','September', month_list = ['January','February','March','April','May','June','July','August','September',
'October','November','December'] 'October','November','December']

View File

@ -167,6 +167,7 @@ class OpeningInvoiceCreationTool(Document):
return invoice return invoice
@frappe.whitelist()
def make_invoices(self): def make_invoices(self):
self.validate_company() self.validate_company()
invoices = self.get_invoices() invoices = self.get_invoices()

View File

@ -6,10 +6,12 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
test_dependencies = ["Customer", "Supplier"] from frappe.cache_manager import clear_doctype_cache
from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
test_dependencies = ["Customer", "Supplier"]
class TestOpeningInvoiceCreationTool(unittest.TestCase): class TestOpeningInvoiceCreationTool(unittest.TestCase):
def setUp(self): def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"): if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
@ -24,22 +26,25 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
def test_opening_sales_invoice_creation(self): def test_opening_sales_invoice_creation(self):
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
invoices = self.make_invoices(company="_Test Opening Invoice Company") try:
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
"keys": ["customer", "outstanding_amount", "status"], "keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 300, "Overdue"], 0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"], 1: ["_Test Customer 1", 250, "Overdue"],
} }
self.check_expected_values(invoices, expected_value) self.check_expected_values(invoices, expected_value)
si = frappe.get_doc("Sales Invoice", invoices[0]) si = frappe.get_doc("Sales Invoice", invoices[0])
# Check if update stock is not enabled # Check if update stock is not enabled
self.assertEqual(si.update_stock, 0) self.assertEqual(si.update_stock, 0)
property_setter.delete() finally:
property_setter.delete()
clear_doctype_cache("Sales Invoice")
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@ -143,4 +148,4 @@ def make_customer(customer=None):
customer.insert(ignore_permissions=True) customer.insert(ignore_permissions=True)
return customer.name return customer.name
else: else:
return frappe.db.exists("Customer", customer_name) return frappe.db.exists("Customer", customer_name)

View File

@ -234,8 +234,9 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
}); });
if (invoices) { if (invoices) {
frappe.meta.get_docfield("Payment Reconciliation Payment", "invoice_number", this.frm.fields_dict.payment.grid.update_docfield_property(
me.frm.doc.name).options = "\n" + invoices.join("\n"); 'invoice_number', 'options', "\n" + invoices.join("\n")
);
$.each(me.frm.doc.payments || [], function(i, p) { $.each(me.frm.doc.payments || [], function(i, p) {
if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null; if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null;

View File

@ -11,6 +11,7 @@ from erpnext.accounts.utils import (get_outstanding_invoices,
from erpnext.controllers.accounts_controller import get_advance_payment_entries from erpnext.controllers.accounts_controller import get_advance_payment_entries
class PaymentReconciliation(Document): class PaymentReconciliation(Document):
@frappe.whitelist()
def get_unreconciled_entries(self): def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries() self.get_nonreconciled_payment_entries()
self.get_invoice_entries() self.get_invoice_entries()
@ -147,6 +148,7 @@ class PaymentReconciliation(Document):
ent.currency = e.get('currency') ent.currency = e.get('currency')
ent.outstanding_amount = e.get('outstanding_amount') ent.outstanding_amount = e.get('outstanding_amount')
@frappe.whitelist()
def reconcile(self, args): def reconcile(self, args):
for e in self.get('payments'): for e in self.get('payments'):
e.invoice_type = None e.invoice_type = None
@ -197,6 +199,7 @@ class PaymentReconciliation(Document):
'difference_account': row.difference_account 'difference_account': row.difference_account
}) })
@frappe.whitelist()
def get_difference_amount(self, child_row): def get_difference_amount(self, child_row):
if child_row.get("reference_type") != 'Payment Entry': return if child_row.get("reference_type") != 'Payment Entry': return

View File

@ -18,7 +18,7 @@ class POSClosingEntry(StatusUpdater):
self.validate_pos_closing() self.validate_pos_closing()
self.validate_pos_invoices() self.validate_pos_invoices()
def validate_pos_closing(self): def validate_pos_closing(self):
user = frappe.db.sql(""" user = frappe.db.sql("""
SELECT name FROM `tabPOS Closing Entry` SELECT name FROM `tabPOS Closing Entry`
@ -37,12 +37,12 @@ class POSClosingEntry(StatusUpdater):
bold_user = frappe.bold(self.user) bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period") frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period")) .format(bold_already_exists, bold_user), title=_("Invalid Period"))
def validate_pos_invoices(self): def validate_pos_invoices(self):
invalid_rows = [] invalid_rows = []
for d in self.pos_transactions: for d in self.pos_transactions:
invalid_row = {'idx': d.idx} invalid_row = {'idx': d.idx}
pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice, pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0] ["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
if pos_invoice.consolidated_invoice: if pos_invoice.consolidated_invoice:
invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated"))) invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
@ -68,14 +68,15 @@ class POSClosingEntry(StatusUpdater):
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
@frappe.whitelist()
def get_payment_reconciliation_details(self): def get_payment_reconciliation_details(self):
currency = frappe.get_cached_value('Company', self.company, "default_currency") currency = frappe.get_cached_value('Company', self.company, "default_currency")
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
{"data": self, "currency": currency}) {"data": self, "currency": currency})
def on_submit(self): def on_submit(self):
consolidate_pos_invoices(closing_entry=self) consolidate_pos_invoices(closing_entry=self)
def on_cancel(self): def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self) unconsolidate_pos_invoices(closing_entry=self)

View File

@ -5,12 +5,21 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPOSClosingEntry(unittest.TestCase): class TestPOSClosingEntry(unittest.TestCase):
def setUp(self):
# Make stock available for POS Sales
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
def tearDown(self):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
def test_pos_closing_entry(self): def test_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name) opening_entry = create_opening_entry(pos_profile, test_user.name)
@ -41,9 +50,6 @@ class TestPOSClosingEntry(unittest.TestCase):
self.assertEqual(pcv_doc.total_quantity, 2) self.assertEqual(pcv_doc.total_quantity, 2)
self.assertEqual(pcv_doc.net_total, 6700) self.assertEqual(pcv_doc.net_total, 6700)
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
def test_cancelling_of_pos_closing_entry(self): def test_cancelling_of_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name) opening_entry = create_opening_entry(pos_profile, test_user.name)
@ -84,8 +90,6 @@ class TestPOSClosingEntry(unittest.TestCase):
self.assertEqual(si_doc.docstatus, 2) self.assertEqual(si_doc.docstatus, 2)
self.assertEqual(pos_inv1.status, 'Paid') self.assertEqual(pos_inv1.status, 'Paid')
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
def init_user_and_profile(**args): def init_user_and_profile(**args):
user = 'test@example.com' user = 'test@example.com'
@ -103,4 +107,4 @@ def init_user_and_profile(**args):
pos_profile.save() pos_profile.save()
return test_user, pos_profile return test_user, pos_profile

View File

@ -220,7 +220,7 @@ class POSInvoice(SalesInvoice):
base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total) base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total)
if not flt(self.change_amount) and grand_total < flt(self.paid_amount): if not flt(self.change_amount) and grand_total < flt(self.paid_amount):
self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount)) self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount))
self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount)) self.base_change_amount = flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount)
if flt(self.change_amount) and not self.account_for_change_amount: if flt(self.change_amount) and not self.account_for_change_amount:
frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1) frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
@ -354,6 +354,7 @@ class POSInvoice(SalesInvoice):
return profile return profile
@frappe.whitelist()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
profile = self.set_pos_fields(for_validate) profile = self.set_pos_fields(for_validate)
@ -376,12 +377,20 @@ class POSInvoice(SalesInvoice):
"allow_print_before_pay": profile.get("allow_print_before_pay") "allow_print_before_pay": profile.get("allow_print_before_pay")
} }
@frappe.whitelist()
def reset_mode_of_payments(self):
if self.pos_profile:
pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile)
update_multi_mode_option(self, pos_profile)
self.paid_amount = 0
def set_account_for_mode_of_payment(self): def set_account_for_mode_of_payment(self):
self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default] self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
for pay in self.payments: for pay in self.payments:
if not pay.account: if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
@frappe.whitelist()
def create_payment_request(self): def create_payment_request(self):
for pay in self.payments: for pay in self.payments:
if pay.type == "Phone": if pay.type == "Phone":

View File

@ -9,8 +9,20 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.item.test_item import make_item
class TestPOSInvoice(unittest.TestCase): class TestPOSInvoice(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.sql("delete from `tabTax Rule`")
def tearDown(self):
if frappe.session.user != "Administrator":
frappe.set_user("Administrator")
if frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
frappe.db.set_value("Selling Settings", None, "validate_selling_price", 0)
def test_timestamp_change(self): def test_timestamp_change(self):
w = create_pos_invoice(do_not_save=1) w = create_pos_invoice(do_not_save=1)
w.docstatus = 0 w.docstatus = 0
@ -370,7 +382,6 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv.load_from_db() pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 3470) self.assertEqual(rounded_total, 3470)
frappe.set_user("Administrator")
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
@ -412,7 +423,6 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv.load_from_db() pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 840) self.assertEqual(rounded_total, 840)
frappe.set_user("Administrator")
def test_merging_with_validate_selling_price(self): def test_merging_with_validate_selling_price(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
@ -421,10 +431,12 @@ class TestPOSInvoice(unittest.TestCase):
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1) frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=1, rate=300) item = "Test Selling Price Validation"
make_item(item, {"is_stock_item": 1})
make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
pos_inv.append('payments', { pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
}) })
@ -438,7 +450,7 @@ class TestPOSInvoice(unittest.TestCase):
}) })
self.assertRaises(frappe.ValidationError, pos_inv.submit) self.assertRaises(frappe.ValidationError, pos_inv.submit)
pos_inv2 = create_pos_invoice(rate=400, do_not_submit=1) pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1)
pos_inv2.append('payments', { pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400
}) })
@ -457,8 +469,6 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv2.load_from_db() pos_inv2.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400) self.assertEqual(rounded_total, 400)
frappe.set_user("Administrator")
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 0)
def create_pos_invoice(**args): def create_pos_invoice(**args):
args = frappe._dict(args) args = frappe._dict(args)
@ -508,4 +518,4 @@ def create_pos_invoice(**args):
else: else:
pos_inv.payment_schedule = [] pos_inv.payment_schedule = []
return pos_inv return pos_inv

View File

@ -12,6 +12,7 @@ from frappe.utils.background_jobs import enqueue
from frappe.model.mapper import map_doc, map_child_doc 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
from six import iteritems from six import iteritems
@ -78,8 +79,11 @@ class POSInvoiceMergeLog(Document):
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data) sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1 sales_invoice.is_consolidated = 1
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save() sales_invoice.save()
sales_invoice.submit() sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name self.consolidated_invoice = sales_invoice.name
return sales_invoice.name return sales_invoice.name
@ -91,10 +95,13 @@ class POSInvoiceMergeLog(Document):
credit_note = self.merge_pos_invoice_into(credit_note, data) credit_note = self.merge_pos_invoice_into(credit_note, data)
credit_note.is_consolidated = 1 credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.posting_date = getdate(self.posting_date)
# TODO: return could be against multiple sales invoice which could also have been consolidated? # TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice # credit_note.return_against = self.consolidated_invoice
credit_note.save() credit_note.save()
credit_note.submit() credit_note.submit()
self.consolidated_credit_note = credit_note.name self.consolidated_credit_note = credit_note.name
return credit_note.name return credit_note.name
@ -131,12 +138,14 @@ class POSInvoiceMergeLog(Document):
if t.account_head == tax.account_head and t.cost_center == tax.cost_center: if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount) t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount) t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
update_item_wise_tax_detail(t, tax)
found = True found = True
if not found: if not found:
tax.charge_type = 'Actual' tax.charge_type = 'Actual'
tax.included_in_print_rate = 0 tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
tax.item_wise_tax_detail = tax.item_wise_tax_detail
taxes.append(tax) taxes.append(tax)
for payment in doc.get('payments'): for payment in doc.get('payments'):
@ -168,11 +177,9 @@ class POSInvoiceMergeLog(Document):
sales_invoice = frappe.new_doc('Sales Invoice') sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer sales_invoice.customer = self.customer
sales_invoice.is_pos = 1 sales_invoice.is_pos = 1
# date can be pos closing date?
sales_invoice.posting_date = getdate(nowdate())
return sales_invoice return sales_invoice
def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''): def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
for doc in invoice_docs: for doc in invoice_docs:
doc.load_from_db() doc.load_from_db()
@ -187,6 +194,26 @@ class POSInvoiceMergeLog(Document):
si.flags.ignore_validate = True si.flags.ignore_validate = True
si.cancel() si.cancel()
def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail)
tax_row_detail = json.loads(tax_row.item_wise_tax_detail)
if not consolidated_tax_detail:
consolidated_tax_detail = {}
for item_code, tax_data in tax_row_detail.items():
if consolidated_tax_detail.get(item_code):
consolidated_tax_data = consolidated_tax_detail.get(item_code)
consolidated_tax_detail.update({
item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]]
})
else:
consolidated_tax_detail.update({
item_code: [tax_data[0], tax_data[1]]
})
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':'))
def get_all_unconsolidated_invoices(): def get_all_unconsolidated_invoices():
filters = { filters = {
'consolidated_invoice': [ 'in', [ '', None ]], 'consolidated_invoice': [ 'in', [ '', None ]],
@ -214,7 +241,7 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
if len(invoices) >= 5 and closing_entry: if len(invoices) >= 5 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, closing_entry) enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else: else:
create_merge_logs(invoice_by_customer, closing_entry) create_merge_logs(invoice_by_customer, closing_entry)
@ -227,21 +254,21 @@ def unconsolidate_pos_invoices(closing_entry):
if len(merge_logs) >= 5: if len(merge_logs) >= 5:
closing_entry.set_status(update=True, status='Queued') closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs, closing_entry) enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else: else:
cancel_merge_logs(merge_logs, closing_entry) cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}): def create_merge_logs(invoice_by_customer, closing_entry={}):
for customer, invoices in iteritems(invoice_by_customer): for customer, invoices in 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(nowdate()) merge_log.posting_date = getdate(closing_entry.get('posting_date'))
merge_log.customer = customer merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None) merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.set('pos_invoices', invoices) merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True) merge_log.save(ignore_permissions=True)
merge_log.submit() merge_log.submit()
if closing_entry: if closing_entry:
closing_entry.set_status(update=True, status='Submitted') closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry() closing_entry.update_opening_entry()
@ -256,7 +283,7 @@ def cancel_merge_logs(merge_logs, closing_entry={}):
closing_entry.set_status(update=True, status='Cancelled') closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True) closing_entry.update_opening_entry(for_cancel=True)
def enqueue_job(job, invoice_by_customer, closing_entry): def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None):
check_scheduler_status() check_scheduler_status()
job_name = closing_entry.get("name") job_name = closing_entry.get("name")
@ -269,6 +296,7 @@ def enqueue_job(job, invoice_by_customer, closing_entry):
job_name=job_name, job_name=job_name,
closing_entry=closing_entry, closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer, invoice_by_customer=invoice_by_customer,
merge_logs=merge_logs,
now=frappe.conf.developer_mode or frappe.flags.in_test now=frappe.conf.developer_mode or frappe.flags.in_test
) )

View File

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
import json
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
@ -14,85 +15,136 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
def test_consolidated_invoice_creation(self): def test_consolidated_invoice_creation(self):
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() try:
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append('payments', { pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
}) })
pos_inv.submit() pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append('payments', { pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
}) })
pos_inv2.submit() pos_inv2.submit()
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
pos_inv3.append('payments', { pos_inv3.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
}) })
pos_inv3.submit() pos_inv3.submit()
consolidate_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
pos_inv3.load_from_db() pos_inv3.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidated_credit_note_creation(self): def test_consolidated_credit_note_creation(self):
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile() try:
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1) pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append('payments', { pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
}) })
pos_inv.submit() pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append('payments', { pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
}) })
pos_inv2.submit() pos_inv2.submit()
pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
pos_inv3.append('payments', { pos_inv3.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
}) })
pos_inv3.submit() pos_inv3.submit()
pos_inv_cn = make_sales_return(pos_inv.name) pos_inv_cn = make_sales_return(pos_inv.name)
pos_inv_cn.set("payments", []) pos_inv_cn.set("payments", [])
pos_inv_cn.append('payments', { pos_inv_cn.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
}) })
pos_inv_cn.paid_amount = -300 pos_inv_cn.paid_amount = -300
pos_inv_cn.submit() pos_inv_cn.submit()
consolidate_pos_invoices() consolidate_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
pos_inv3.load_from_db() pos_inv3.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
pos_inv_cn.load_from_db() pos_inv_cn.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return")) self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
frappe.set_user("Administrator") finally:
frappe.db.sql("delete from `tabPOS Profile`") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidated_invoice_item_taxes(self):
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
try:
inv = create_pos_invoice(qty=1, rate=100, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 9
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True)
inv2.get('items')[0].item_code = '_Test Item 2'
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 5
})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail)
tax_rate, amount = item_wise_tax_detail.get('_Test Item')
self.assertEqual(tax_rate, 9)
self.assertEqual(amount, 9)
tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2')
self.assertEqual(tax_rate2, 5)
self.assertEqual(amount2, 5)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@ -16,8 +16,11 @@ frappe.ui.form.on('POS Settings', {
} }
}); });
frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields); frm.fields_dict.invoice_fields.grid.update_docfield_property(
'fieldname', 'options', [""].concat(fields)
);
}); });
} }
}); });

View File

@ -471,7 +471,7 @@ def apply_pricing_rule_on_transaction(doc):
if not d.get(pr_field): continue if not d.get(pr_field): continue
if d.validate_applied_rule and doc.get(field) < d.get(pr_field): if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field):
frappe.msgprint(_("User has not applied rule on the invoice {0}") frappe.msgprint(_("User has not applied rule on the invoice {0}")
.format(doc.name)) .format(doc.name))
else: else:

View File

@ -38,22 +38,22 @@
{% endif %} {% endif %}
</td> </td>
<td style="text-align: right"> <td style="text-align: right">
{{ frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}</td> {{ frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}</td>
<td style="text-align: right"> <td style="text-align: right">
{{ frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}</td> {{ frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}</td>
{% else %} {% else %}
<td></td> <td></td>
<td></td> <td></td>
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td> <td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td>
<td style="text-align: right"> <td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.debit, filters.presentation_currency) }} {{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
</td> </td>
<td style="text-align: right"> <td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.credit, filters.presentation_currency) }} {{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
</td> </td>
{% endif %} {% endif %}
<td style="text-align: right"> <td style="text-align: right">
{{ frappe.utils.fmt_money(row.balance, filters.presentation_currency) }} {{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -9,7 +9,7 @@ from frappe.utils import cstr
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.model.document import Document from frappe.model.document import Document
pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group' pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group',
'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from', 'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from',
'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier', 'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier',
'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules'] 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules']
@ -111,4 +111,4 @@ def get_args_for_pricing_rule(doc):
for d in pricing_rule_fields: for d in pricing_rule_fields:
args[d] = doc.get(d) args[d] = doc.get(d)
return args return args

View File

@ -496,15 +496,6 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc,
} }
} }
cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){
if(doc.select_print_heading){
// print heading
cur_frm.pformat.print_heading = doc.select_print_heading;
}
else
cur_frm.pformat.print_heading = __("Purchase Invoice");
}
frappe.ui.form.on("Purchase Invoice", { frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) { setup: function(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {

View File

@ -127,7 +127,6 @@
"write_off_cost_center", "write_off_cost_center",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"adjust_advance_taxes",
"get_advances", "get_advances",
"advances", "advances",
"payment_schedule_section", "payment_schedule_section",
@ -1326,13 +1325,6 @@
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
}, },
{
"default": "0",
"description": "Taxes paid while advance payment will be adjusted against this invoice",
"fieldname": "adjust_advance_taxes",
"fieldtype": "Check",
"label": "Adjust Advance Taxes"
},
{ {
"depends_on": "eval:doc.is_internal_supplier", "depends_on": "eval:doc.is_internal_supplier",
"description": "Unrealized Profit / Loss account for intra-company transfers", "description": "Unrealized Profit / Loss account for intra-company transfers",
@ -1378,7 +1370,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-09 21:15:30.422084", "modified": "2021-03-30 22:45:58.334107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -898,7 +898,7 @@ class TestPurchaseInvoice(unittest.TestCase):
acc_settings.submit_journal_entries = 1 acc_settings.submit_journal_entries = 1
acc_settings.save() acc_settings.save()
item = create_item("_Test Item for Deferred Accounting") item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
item.enable_deferred_expense = 1 item.enable_deferred_expense = 1
item.deferred_expense_account = deferred_account item.deferred_expense_account = deferred_account
item.save() item.save()

View File

@ -1,9 +1,6 @@
// 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
// print heading
cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %}; {% include 'erpnext/selling/sales_common.js' %};
frappe.provide("erpnext.accounts"); frappe.provide("erpnext.accounts");
@ -916,7 +913,7 @@ frappe.ui.form.on('Sales Invoice Timesheet', {
}, },
callback: function(r, rt) { callback: function(r, rt) {
if(r.message){ if(r.message){
data = r.message; let data = r.message;
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours); frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount); frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail); frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);

View File

@ -118,6 +118,7 @@
"in_words", "in_words",
"total_advance", "total_advance",
"outstanding_amount", "outstanding_amount",
"disable_rounded_total",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"get_advances", "get_advances",
@ -1109,6 +1110,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1120,6 +1122,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounded_total", "fieldname": "base_rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1168,6 +1171,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment", "fieldname": "rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1180,6 +1184,7 @@
}, },
{ {
"bold": 1, "bold": 1,
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounded_total", "fieldname": "rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1945,6 +1950,13 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Set Target Warehouse", "label": "Set Target Warehouse",
"options": "Warehouse" "options": "Warehouse"
},
{
"default": "0",
"depends_on": "grand_total",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -1957,7 +1969,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-03-31 15:42:26.261540", "modified": "2021-04-15 23:57:58.766651",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -46,7 +46,6 @@ class SalesInvoice(SellingController):
'target_parent_dt': 'Sales Order', 'target_parent_dt': 'Sales Order',
'target_parent_field': 'per_billed', 'target_parent_field': 'per_billed',
'source_field': 'amount', 'source_field': 'amount',
'join_field': 'so_detail',
'percent_join_field': 'sales_order', 'percent_join_field': 'sales_order',
'status_field': 'billing_status', 'status_field': 'billing_status',
'keyword': 'Billed', 'keyword': 'Billed',
@ -77,7 +76,7 @@ class SalesInvoice(SellingController):
if not self.is_pos: if not self.is_pos:
self.so_dn_required() self.so_dn_required()
self.set_tax_withholding() self.set_tax_withholding()
self.validate_proj_cust() self.validate_proj_cust()
@ -276,7 +275,7 @@ class SalesInvoice(SellingController):
pluck="pos_closing_entry" pluck="pos_closing_entry"
) )
if pos_closing_entry: if pos_closing_entry:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format( msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
frappe.bold("Consolidated Sales Invoice"), frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0]) get_link_to_form("POS Closing Entry", pos_closing_entry[0])
) )
@ -394,6 +393,7 @@ 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)
@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)
@ -548,12 +548,12 @@ class SalesInvoice(SellingController):
frappe.throw(_("Debit To is required"), title=_("Account Missing")) frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet": if account.report_type != "Balance Sheet":
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To")) msg = _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " "
msg += _("You can change the parent account to a Balance Sheet account or select a different account.") msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
frappe.throw(msg, title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable": if self.customer and account.account_type != "Receivable":
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To")) msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
msg += _("Change the account type to Receivable or select a different account.") msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
@ -733,6 +733,7 @@ class SalesInvoice(SellingController):
else: else:
self.calculate_billing_amount_for_timesheet() self.calculate_billing_amount_for_timesheet()
@frappe.whitelist()
def add_timesheet_data(self): def add_timesheet_data(self):
self.set('timesheets', []) self.set('timesheets', [])
if self.project: if self.project:
@ -1290,6 +1291,7 @@ class SalesInvoice(SellingController):
break break
# Healthcare # Healthcare
@frappe.whitelist()
def set_healthcare_services(self, checked_values): def set_healthcare_services(self, checked_values):
self.set("items", []) self.set("items", [])
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details

View File

@ -1802,6 +1802,15 @@ class TestSalesInvoice(unittest.TestCase):
si.selling_price_list = "_Test Price List Rest of the World" si.selling_price_list = "_Test Price List Rest of the World"
si.update_stock = 1 si.update_stock = 1
si.items[0].target_warehouse = 'Work In Progress - TCP1' si.items[0].target_warehouse = 'Work In Progress - TCP1'
# Add stock to stores for succesful stock transfer
make_stock_entry(
target="Stores - TCP1",
company = "_Test Company with perpetual inventory",
qty=1,
basic_rate=100
)
add_taxes(si) add_taxes(si)
si.save() si.save()
@ -1870,7 +1879,17 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_submission_without_irn(self): def test_einvoice_submission_without_irn(self):
# init # init
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1) einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
einvoice_settings.append('credentials', {
'company': '_Test Company',
'gstin': '27AAECE4835E1ZR',
'username': 'test',
'password': 'test'
})
einvoice_settings.save()
country = frappe.flags.country country = frappe.flags.country
frappe.flags.country = 'India' frappe.flags.country = 'India'
@ -1881,7 +1900,8 @@ class TestSalesInvoice(unittest.TestCase):
si.submit() si.submit()
# reset # reset
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0) einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 0
frappe.flags.country = country frappe.flags.country = country
def test_einvoice_json(self): def test_einvoice_json(self):
@ -2272,4 +2292,4 @@ def add_taxes(doc):
"cost_center": "Main - TCP1", "cost_center": "Main - TCP1",
"description": "Excise Duty", "description": "Excise Duty",
"rate": 12 "rate": 12
}) })

View File

@ -46,5 +46,5 @@ def validate_disabled(doc):
frappe.throw(_("Disabled template must not be default template")) frappe.throw(_("Disabled template must not be default template"))
def validate_for_tax_category(doc): def validate_for_tax_category(doc):
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}): if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))

View File

@ -14,10 +14,15 @@ test_records = frappe.get_test_records('Tax Rule')
from six import iteritems from six import iteritems
class TestTaxRule(unittest.TestCase): class TestTaxRule(unittest.TestCase):
def setUp(self): @classmethod
def setUpClass(cls):
frappe.db.set_value("Shopping Cart Settings", None, "enabled", 0)
@classmethod
def tearDownClass(cls):
frappe.db.sql("delete from `tabTax Rule`") frappe.db.sql("delete from `tabTax Rule`")
def tearDown(self): def setUp(self):
frappe.db.sql("delete from `tabTax Rule`") frappe.db.sql("delete from `tabTax Rule`")
def test_conflict(self): def test_conflict(self):

View File

@ -251,7 +251,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
threshold = tax_details.get('threshold', 0) threshold = tax_details.get('threshold', 0)
cumulative_threshold = tax_details.get('cumulative_threshold', 0) cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if ldc and is_valid_certificate( if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto, ldc.valid_from, ldc.valid_upto,
inv.posting_date, tax_deducted, inv.posting_date, tax_deducted,

View File

@ -87,50 +87,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() d.cancel()
def test_single_threshold_tds_with_previous_vouchers(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
pi.submit()
invoices.append(pi)
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
pi.submit()
invoices.append(pi)
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
self.assertEqual(pi.grand_total, 8000)
# delete invoices to avoid clashing
for d in invoices:
d.cancel()
def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
invoices = []
doc = create_supplier(supplier_name = "Test TDS Supplier ABC",
tax_withholding_category="Single Threshold TDS")
supplier = doc.name
pi = create_purchase_invoice(supplier=supplier)
pi.submit()
invoices.append(pi)
# TDS not applied
pi = create_purchase_invoice(supplier=supplier, do_not_apply_tds=True)
pi.submit()
invoices.append(pi)
pi = create_purchase_invoice(supplier=supplier)
pi.submit()
invoices.append(pi)
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
self.assertEqual(pi.grand_total, 8000)
# delete invoices to avoid clashing
for d in invoices:
d.cancel()
def test_cumulative_threshold_tcs(self): def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = [] invoices = []
@ -177,7 +133,7 @@ def cancel_invoices():
for d in purchase_invoices: for d in purchase_invoices:
frappe.get_doc('Purchase Invoice', d).cancel() frappe.get_doc('Purchase Invoice', d).cancel()
for d in sales_invoices: for d in sales_invoices:
frappe.get_doc('Sales Invoice', d).cancel() frappe.get_doc('Sales Invoice', d).cancel()
@ -229,7 +185,8 @@ def create_sales_invoice(**args):
'qty': args.qty or 1, 'qty': args.qty or 1,
'rate': args.rate or 10000, 'rate': args.rate or 10000,
'cost_center': 'Main - _TC', 'cost_center': 'Main - _TC',
'expense_account': 'Cost of Goods Sold - _TC' 'expense_account': 'Cost of Goods Sold - _TC',
'warehouse': args.warehouse or '_Test Warehouse - _TC'
}] }]
}) })
@ -353,4 +310,4 @@ def create_tax_with_holding_category():
'company': '_Test Company', 'company': '_Test Company',
'account': 'TDS - _TC' 'account': 'TDS - _TC'
}] }]
}).insert() }).insert()

View File

@ -406,9 +406,10 @@ def check_if_advance_entry_modified(args):
throw(_("""Payment Entry has been modified after you pulled it. Please pull it again.""")) throw(_("""Payment Entry has been modified after you pulled it. Please pull it again."""))
def validate_allocated_amount(args): def validate_allocated_amount(args):
precision = args.get('precision') or frappe.db.get_single_value("System Settings", "currency_precision")
if args.get("allocated_amount") < 0: if args.get("allocated_amount") < 0:
throw(_("Allocated amount cannot be negative")) throw(_("Allocated amount cannot be negative"))
elif args.get("allocated_amount") > args.get("unadjusted_amount"): elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision):
throw(_("Allocated amount cannot be greater than unadjusted amount")) throw(_("Allocated amount cannot be greater than unadjusted amount"))
def update_reference_in_journal_entry(d, jv_obj): def update_reference_in_journal_entry(d, jv_obj):

View File

@ -71,6 +71,7 @@ class CropCycle(Document):
"exp_end_date": add_days(start_date, crop_task.get("end_day") - 1) "exp_end_date": add_days(start_date, crop_task.get("end_day") - 1)
}).insert() }).insert()
@frappe.whitelist()
def reload_linked_analysis(self): def reload_linked_analysis(self):
linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis'] linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis']
required_fields = ['location', 'name', 'collection_datetime'] required_fields = ['location', 'name', 'collection_datetime']
@ -87,6 +88,7 @@ class CropCycle(Document):
frappe.publish_realtime("List of Linked Docs", frappe.publish_realtime("List of Linked Docs",
output, user=frappe.session.user) output, user=frappe.session.user)
@frappe.whitelist()
def append_to_child(self, obj_to_append): def append_to_child(self, obj_to_append):
for doctype in obj_to_append: for doctype in obj_to_append:
for doc_name in set(obj_to_append[doctype]): for doc_name in set(obj_to_append[doctype]):

View File

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
class Fertilizer(Document): class Fertilizer(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'})
for doc in docs: for doc in docs:

View File

@ -8,6 +8,7 @@ from frappe.model.naming import make_autoname
from frappe.model.document import Document from frappe.model.document import Document
class PlantAnalysis(Document): class PlantAnalysis(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'})
for doc in docs: for doc in docs:

View File

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
class SoilAnalysis(Document): class SoilAnalysis(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'})
for doc in docs: for doc in docs:

View File

@ -13,6 +13,7 @@ class SoilTexture(Document):
soil_edit_order = [2, 1, 0] soil_edit_order = [2, 1, 0]
soil_types = ['clay_composition', 'sand_composition', 'silt_composition'] soil_types = ['clay_composition', 'sand_composition', 'silt_composition']
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'})
for doc in docs: for doc in docs:
@ -26,6 +27,7 @@ class SoilTexture(Document):
if sum(self.get(soil_type) for soil_type in self.soil_types) != 100: if sum(self.get(soil_type) for soil_type in self.soil_types) != 100:
frappe.throw(_('Soil compositions do not add up to 100')) frappe.throw(_('Soil compositions do not add up to 100'))
@frappe.whitelist()
def update_soil_edit(self, soil_type): def update_soil_edit(self, soil_type):
self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1 self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1
self.soil_type = self.get_soil_type() self.soil_type = self.get_soil_type()
@ -35,8 +37,8 @@ class SoilTexture(Document):
if sum(self.soil_edit_order) < 5: return if sum(self.soil_edit_order) < 5: return
last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order)) last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order))
# set composition of the last edited soil # set composition of the last edited soil
self.set( self.soil_types[last_edit_index], self.set(self.soil_types[last_edit_index],
100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index]))) 100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index])))
# calculate soil type # calculate soil type
@ -67,4 +69,4 @@ class SoilTexture(Document):
elif (c >= 40 and sa <= 45 and si < 40): elif (c >= 40 and sa <= 45 and si < 40):
return 'Clay' return 'Clay'
else: else:
return 'Select' return 'Select'

View File

@ -9,11 +9,13 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
class WaterAnalysis(Document): class WaterAnalysis(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'})
for doc in docs: for doc in docs:
self.append('water_analysis_criteria', {'title': str(doc.name)}) self.append('water_analysis_criteria', {'title': str(doc.name)})
@frappe.whitelist()
def update_lab_result_date(self): def update_lab_result_date(self):
if not self.result_datetime: if not self.result_datetime:
self.result_datetime = self.laboratory_testing_datetime self.result_datetime = self.laboratory_testing_datetime

View File

@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
class Weather(Document): class Weather(Document):
@frappe.whitelist()
def load_contents(self): def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'}) docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'})
for doc in docs: for doc in docs:

View File

@ -553,6 +553,7 @@ class Asset(AccountsController):
make_gl_entries(gl_entries) make_gl_entries(gl_entries)
self.db_set('booked_fixed_asset', 1) self.db_set('booked_fixed_asset', 1)
@frappe.whitelist()
def get_depreciation_rate(self, args, on_validate=False): def get_depreciation_rate(self, args, on_validate=False):
if isinstance(args, string_types): if isinstance(args, string_types):
args = json.loads(args) args = json.loads(args)

View File

@ -13,6 +13,8 @@
"po_required", "po_required",
"pr_required", "pr_required",
"maintain_same_rate", "maintain_same_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"allow_multiple_items", "allow_multiple_items",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
@ -89,6 +91,23 @@
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "Stop",
"depends_on": "maintain_same_rate",
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action If Same Rate is Not Maintained",
"mandatory_depends_on": "maintain_same_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval:doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -96,7 +115,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-03-02 17:34:04.190677", "modified": "2021-04-04 20:01:44.087066",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -133,6 +133,7 @@ class PurchaseOrder(BuyingController):
d.material_request_item, "schedule_date") d.material_request_item, "schedule_date")
@frappe.whitelist()
def get_last_purchase_rate(self): def get_last_purchase_rate(self):
"""get last purchase rates for all items""" """get last purchase rates for all items"""
@ -367,7 +368,6 @@ def make_purchase_receipt(source_name, target_doc=None):
"Purchase Order": { "Purchase Order": {
"doctype": "Purchase Receipt", "doctype": "Purchase Receipt",
"field_map": { "field_map": {
"per_billed": "per_billed",
"supplier_warehouse":"supplier_warehouse" "supplier_warehouse":"supplier_warehouse"
}, },
"validation": { "validation": {

View File

@ -778,7 +778,7 @@ class TestPurchaseOrder(unittest.TestCase):
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",
item_code="_Test Item Home Desktop 100", qty=10, basic_rate=100) item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=100, basic_rate=100) item_code = "Test Extra Item 1", qty=100, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", make_stock_entry(target="_Test Warehouse - _TC",

View File

@ -56,6 +56,8 @@
"base_net_amount", "base_net_amount",
"warehouse_and_reference", "warehouse_and_reference",
"warehouse", "warehouse",
"actual_qty",
"company_total_stock",
"material_request", "material_request",
"material_request_item", "material_request_item",
"sales_order", "sales_order",
@ -743,6 +745,22 @@
"options": "currency", "options": "currency",
"read_only": 1 "read_only": 1
}, },
{
"allow_on_submit": 1,
"fieldname": "actual_qty",
"fieldtype": "Float",
"label": "Available Qty at Warehouse",
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "company_total_stock",
"fieldtype": "Float",
"label": "Available Qty at Company",
"no_copy": 1,
"read_only": 1
},
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "discount_and_margin_section", "fieldname": "discount_and_margin_section",
@ -791,7 +809,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-23 01:00:27.132705", "modified": "2021-03-22 11:46:12.357435",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@ -66,6 +66,7 @@ class RequestforQuotation(BuyingController):
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
@frappe.whitelist()
def get_supplier_email_preview(self, supplier): def get_supplier_email_preview(self, supplier):
"""Returns formatted email preview as string.""" """Returns formatted email preview as string."""
rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers)) rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers))

View File

@ -9,9 +9,7 @@ import unittest
class TestSupplierScorecard(unittest.TestCase): class TestSupplierScorecard(unittest.TestCase):
def test_create_scorecard(self): def test_create_scorecard(self):
delete_test_scorecards() doc = make_supplier_scorecard().insert()
my_doc = make_supplier_scorecard()
doc = my_doc.insert()
self.assertEqual(doc.name, valid_scorecard[0].get("supplier")) self.assertEqual(doc.name, valid_scorecard[0].get("supplier"))
def test_criteria_weight(self): def test_criteria_weight(self):
@ -121,7 +119,8 @@ valid_scorecard = [
{ {
"weight":100.0, "weight":100.0,
"doctype":"Supplier Scorecard Scoring Criteria", "doctype":"Supplier Scorecard Scoring Criteria",
"criteria_name":"Delivery" "criteria_name":"Delivery",
"formula": "100"
} }
], ],
"supplier":"_Test Supplier", "supplier":"_Test Supplier",

View File

@ -517,6 +517,7 @@ class AccountsController(TransactionBase):
frappe.db.sql("""delete from `tab%s` where parentfield=%s and parent = %s frappe.db.sql("""delete from `tab%s` where parentfield=%s and parent = %s
and allocated_amount = 0""" % (childtype, '%s', '%s'), (parentfield, self.name)) and allocated_amount = 0""" % (childtype, '%s', '%s'), (parentfield, self.name))
@frappe.whitelist()
def apply_shipping_rule(self): def apply_shipping_rule(self):
if self.shipping_rule: if self.shipping_rule:
shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule) shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)
@ -537,6 +538,7 @@ class AccountsController(TransactionBase):
return {} return {}
@frappe.whitelist()
def set_advances(self): def set_advances(self):
"""Returns list of advances against Account, Party, Reference""" """Returns list of advances against Account, Party, Reference"""
@ -657,6 +659,7 @@ class AccountsController(TransactionBase):
'dr_or_cr': dr_or_cr, 'dr_or_cr': dr_or_cr,
'unadjusted_amount': flt(d.advance_amount), 'unadjusted_amount': flt(d.advance_amount),
'allocated_amount': flt(d.allocated_amount), 'allocated_amount': flt(d.allocated_amount),
'precision': d.precision('advance_amount'),
'exchange_rate': (self.conversion_rate 'exchange_rate': (self.conversion_rate
if self.party_account_currency != self.company_currency else 1), if self.party_account_currency != self.company_currency else 1),
'grand_total': (self.base_grand_total 'grand_total': (self.base_grand_total
@ -714,7 +717,9 @@ class AccountsController(TransactionBase):
total_billed_amt = abs(total_billed_amt) total_billed_amt = abs(total_billed_amt)
max_allowed_amt = abs(max_allowed_amt) max_allowed_amt = abs(max_allowed_amt)
if total_billed_amt - max_allowed_amt > 0.01: role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt)) .format(item.item_code, item.idx, max_allowed_amt))
@ -1444,7 +1449,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
) )
def get_new_child_item(item_row): def get_new_child_item(item_row):
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row) return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def validate_quantity(child_item, d): def validate_quantity(child_item, d):

View File

@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.utils import flt,cint, cstr, getdate from frappe.utils import flt,cint, cstr, getdate
from six import iteritems from six import iteritems
from collections import OrderedDict
from erpnext.accounts.party import get_party_details from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
@ -391,10 +392,12 @@ class BuyingController(StockController):
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order) qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty: for batch_data in batches_qty:
qty = batch_data['qty'] qty = batch_data['qty']
raw_material.batch_no = batch_data['batch'] raw_material.batch_no = batch_data['batch']
self.append_raw_material_to_be_backflushed(item, raw_material, qty) if qty > 0:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
else: else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty) self.append_raw_material_to_be_backflushed(item, raw_material, qty)
@ -1056,7 +1059,7 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
for batch_data in transferred_batches: for batch_data in transferred_batches:
key = ((batch_data.item_code, fg_item) key = ((batch_data.item_code, fg_item)
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
transferred_batch_qty_map.setdefault(key, {}) transferred_batch_qty_map.setdefault(key, OrderedDict())
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map return transferred_batch_qty_map
@ -1109,8 +1112,14 @@ def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty
if available_qty >= required_qty: if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty}) available_batches.append({'batch': batch, 'qty': required_qty})
break break
else: elif available_qty != 0:
available_batches.append({'batch': batch, 'qty': available_qty}) available_batches.append({'batch': batch, 'qty': available_qty})
required_qty -= available_qty required_qty -= available_qty
for row in available_batches:
if backflushed_batches.get(row.get('batch'), 0) > 0:
backflushed_batches[row.get('batch')] += row.get('qty')
else:
backflushed_batches[row.get('batch')] = row.get('qty')
return available_batches return available_batches

View File

@ -325,7 +325,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
and status not in ("Stopped", "Closed") %(fcond)s and status not in ("Stopped", "Closed") %(fcond)s
and ( and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100) (`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
or `tabDelivery Note`.grand_total = 0 or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
or ( or (
`tabDelivery Note`.is_return = 1 `tabDelivery Note`.is_return = 1
and return_against in (select name from `tabDelivery Note` where per_billed < 100) and return_against in (select name from `tabDelivery Note` where per_billed < 100)
@ -713,7 +713,9 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
return [(d,) for d in set(taxes)] return [(d,) for d in set(taxes)]
def get_fields(doctype, fields=[]): def get_fields(doctype, fields=None):
if fields is None:
fields = []
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
fields.extend(meta.get_search_fields()) fields.extend(meta.get_search_fields())

View File

@ -144,7 +144,7 @@ class SellingController(StockController):
if sales_person.commission_rate: if sales_person.commission_rate:
sales_person.incentives = flt( sales_person.incentives = flt(
sales_person.allocated_amount * flt(sales_person.commission_rate) / 100.0, sales_person.allocated_amount * flt(sales_person.commission_rate) / 100.0,
self.precision("incentives", sales_person)) self.precision("incentives", sales_person))
total += sales_person.allocated_percentage total += sales_person.allocated_percentage
@ -504,4 +504,4 @@ def set_default_income_account_for_item(obj):
for d in obj.get("items"): for d in obj.get("items"):
if d.item_code: if d.item_code:
if getattr(d, "income_account", None): if getattr(d, "income_account", None):
set_item_default(d.item_code, obj.company, 'income_account', d.income_account) set_item_default(d.item_code, obj.company, 'income_account', d.income_account)

View File

@ -201,10 +201,14 @@ class StatusUpdater(Document):
get_allowance_for(item['item_code'], self.item_allowance, get_allowance_for(item['item_code'], self.item_allowance,
self.global_qty_allowance, self.global_amount_allowance, qty_or_amount) self.global_qty_allowance, self.global_amount_allowance, qty_or_amount)
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) / role_allowed_to_over_deliver_receive = frappe.db.get_single_value('Stock Settings', 'role_allowed_to_over_deliver_receive')
item[args['target_ref_field']]) * 100 role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
role = role_allowed_to_over_deliver_receive if qty_or_amount == 'qty' else role_allowed_to_over_bill
if overflow_percent - allowance > 0.01: overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
item[args['target_ref_field']]) * 100
if overflow_percent - allowance > 0.01 and role not in frappe.get_roles():
item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100) item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
item['reduce_by'] = item[args['target_field']] - item['max_allowed'] item['reduce_by'] = item[args['target_field']] - item['max_allowed']
@ -371,10 +375,12 @@ class StatusUpdater(Document):
ref_doc.db_set("per_billed", per_billed) ref_doc.db_set("per_billed", per_billed)
ref_doc.set_status(update=True) ref_doc.set_status(update=True)
def get_allowance_for(item_code, item_allowance={}, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"): def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"):
""" """
Returns the allowance for the item, if not set, returns global allowance Returns the allowance for the item, if not set, returns global allowance
""" """
if item_allowance is None:
item_allowance = {}
if qty_or_amount == "qty": if qty_or_amount == "qty":
if item_allowance.get(item_code, frappe._dict()).get("qty"): if item_allowance.get(item_code, frappe._dict()).get("qty"):
return item_allowance[item_code].qty, item_allowance, global_qty_allowance, global_amount_allowance return item_allowance[item_code].qty, item_allowance, global_qty_allowance, global_amount_allowance

View File

@ -117,7 +117,6 @@ class StockController(AccountsController):
"account": expense_account, "account": expense_account,
"against": warehouse_account[sle.warehouse]["account"], "against": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center, "cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
"remarks": self.get("remarks") or "Accounting Entry for Stock", "remarks": self.get("remarks") or "Accounting Entry for Stock",
"credit": flt(sle.stock_value_difference, precision), "credit": flt(sle.stock_value_difference, precision),
"project": item_row.get("project") or self.get("project"), "project": item_row.get("project") or self.get("project"),
@ -483,7 +482,7 @@ class StockController(AccountsController):
) )
message += "<br><br>" message += "<br><br>"
rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule)
message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link) message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
return message return message
def repost_future_sle_and_gle(self): def repost_future_sle_and_gle(self):

View File

@ -149,7 +149,9 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc) validate_inclusive_tax(tax, self.doc)
tax.item_wise_tax_detail = {} if not self.doc.get('is_consolidated'):
tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount", tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item", "tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] "tax_fraction_for_current_item", "grand_total_fraction_for_current_item"]
@ -289,10 +291,13 @@ class calculate_taxes_and_totals(object):
# set precision in the last item iteration # set precision in the last item iteration
if n == len(self.doc.get("items")) - 1: if n == len(self.doc.get("items")) - 1:
self.round_off_totals(tax) self.round_off_totals(tax)
self._set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"])
self.round_off_base_values(tax)
self.set_cumulative_total(i, tax) self.set_cumulative_total(i, tax)
self._set_in_company_currency(tax, self._set_in_company_currency(tax, ["total"])
["total", "tax_amount", "tax_amount_after_discount_amount"])
# adjust Discount Amount loss in last tax iteration # adjust Discount Amount loss in last tax iteration
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \ if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
@ -339,18 +344,11 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity": elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty current_tax_amount = tax_rate * item.qty
current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount) if not self.doc.get("is_consolidated"):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount return current_tax_amount
def get_final_current_tax_amount(self, tax, current_tax_amount):
# Some countries need individual tax components to be rounded
# Handeled via regional doctypess
if tax.account_head in frappe.flags.round_off_applicable_accounts:
current_tax_amount = round(current_tax_amount, 0)
return current_tax_amount
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount): def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item # store tax breakup for each item
key = item.item_code or item.item_name key = item.item_code or item.item_name
@ -361,10 +359,20 @@ class calculate_taxes_and_totals(object):
tax.item_wise_tax_detail[key] = [tax_rate,flt(item_wise_tax_amount)] tax.item_wise_tax_detail[key] = [tax_rate,flt(item_wise_tax_amount)]
def round_off_totals(self, tax): def round_off_totals(self, tax):
if tax.account_head in frappe.flags.round_off_applicable_accounts:
tax.tax_amount = round(tax.tax_amount, 0)
tax.tax_amount_after_discount_amount = round(tax.tax_amount_after_discount_amount, 0)
tax.tax_amount = flt(tax.tax_amount, tax.precision("tax_amount")) tax.tax_amount = flt(tax.tax_amount, tax.precision("tax_amount"))
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount,
tax.precision("tax_amount")) tax.precision("tax_amount"))
def round_off_base_values(self, tax):
# Round off to nearest integer based on regional settings
if tax.account_head in frappe.flags.round_off_applicable_accounts:
tax.base_tax_amount = round(tax.base_tax_amount, 0)
tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0)
def manipulate_grand_total_for_inclusive_tax(self): def manipulate_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff # if fully inclusive taxes and diff
if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]): if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]):
@ -442,8 +450,9 @@ class calculate_taxes_and_totals(object):
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self): def _cleanup(self):
for tax in self.doc.get("taxes"): if not self.doc.get('is_consolidated'):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) for tax in self.doc.get("taxes"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self): def set_discount_amount(self):
if self.doc.additional_discount_percentage: if self.doc.additional_discount_percentage:
@ -810,4 +819,4 @@ class init_landed_taxes_and_totals(object):
def set_amounts_in_company_currency(self): def set_amounts_in_company_currency(self):
for d in self.doc.get(self.tax_field): for d in self.doc.get(self.tax_field):
d.amount = flt(d.amount, d.precision("amount")) d.amount = flt(d.amount, d.precision("amount"))
d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount")) d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Lead Source', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,62 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:source_name",
"creation": "2016-09-16 01:47:47.382372",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"source_name",
"details"
],
"fields": [
{
"fieldname": "source_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Source Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "details",
"fieldtype": "Text Editor",
"label": "Details"
}
],
"links": [],
"modified": "2021-02-08 12:51:48.971517",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead Source",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class LeadSource(Document): class LeadSource(Document):

View File

@ -1,12 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe # import frappe
import unittest import unittest
# test_records = frappe.get_test_records('Lead Source')
class TestLeadSource(unittest.TestCase): class TestLeadSource(unittest.TestCase):
pass pass

View File

@ -11,7 +11,8 @@ from frappe.utils.file_manager import get_file, get_file_path
from six.moves.urllib.parse import urlencode from six.moves.urllib.parse import urlencode
class LinkedInSettings(Document): class LinkedInSettings(Document):
def get_authorization_url(self): @frappe.whitelist()
def get_authorization_url(self):
params = urlencode({ params = urlencode({
"response_type":"code", "response_type":"code",
"client_id": self.consumer_key, "client_id": self.consumer_key,
@ -35,7 +36,7 @@ class LinkedInSettings(Document):
headers = { headers = {
"Content-Type": "application/x-www-form-urlencoded" "Content-Type": "application/x-www-form-urlencoded"
} }
response = self.http_post(url=url, data=body, headers=headers) response = self.http_post(url=url, data=body, headers=headers)
response = frappe.parse_json(response.content.decode()) response = frappe.parse_json(response.content.decode())
self.db_set("access_token", response["access_token"]) self.db_set("access_token", response["access_token"])

View File

@ -85,6 +85,7 @@ class Opportunity(TransactionBase):
self.opportunity_from = "Lead" self.opportunity_from = "Lead"
self.party_name = lead_name self.party_name = lead_name
@frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_active_quotation(): if not self.has_active_quotation():
frappe.db.set(self, 'status', 'Lost') frappe.db.set(self, 'status', 'Lost')
@ -248,7 +249,6 @@ def make_quotation(source_name, target_doc=None):
"doctype": "Quotation", "doctype": "Quotation",
"field_map": { "field_map": {
"opportunity_from": "quotation_to", "opportunity_from": "quotation_to",
"opportunity_type": "order_type",
"name": "enq_no", "name": "enq_no",
} }
}, },

View File

@ -11,6 +11,7 @@ from frappe.utils import get_url_to_form, get_link_to_form
from tweepy.error import TweepError from tweepy.error import TweepError
class TwitterSettings(Document): class TwitterSettings(Document):
@frappe.whitelist()
def get_authorize_url(self): def get_authorize_url(self):
callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url()) callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url())
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url)
@ -21,12 +22,12 @@ class TwitterSettings(Document):
frappe.msgprint(_("Error! Failed to get request token.")) frappe.msgprint(_("Error! Failed to get request token."))
frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")))
def get_access_token(self, oauth_token, oauth_verifier): def get_access_token(self, oauth_token, oauth_verifier):
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
auth.request_token = { auth.request_token = {
'oauth_token' : oauth_token, 'oauth_token' : oauth_token,
'oauth_token_secret' : oauth_verifier 'oauth_token_secret' : oauth_verifier
} }
try: try:
@ -50,10 +51,10 @@ class TwitterSettings(Document):
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
def get_api(self, access_token, access_token_secret): def get_api(self, access_token, access_token_secret):
# authentication of consumer key and secret # authentication of consumer key and secret
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
# authentication of access token and secret # authentication of access token and secret
auth.set_access_token(access_token, access_token_secret) auth.set_access_token(access_token, access_token_secret)
return tweepy.API(auth) return tweepy.API(auth)
@ -64,7 +65,7 @@ class TwitterSettings(Document):
if media: if media:
media_id = self.upload_image(media) media_id = self.upload_image(media)
return self.send_tweet(text, media_id) return self.send_tweet(text, media_id)
def upload_image(self, media): def upload_image(self, media):
media = get_file_path(media) media = get_file_path(media)
api = self.get_api(self.access_token, self.access_token_secret) api = self.get_api(self.access_token, self.access_token_secret)

View File

@ -41,7 +41,7 @@ class CourseEnrollment(Document):
frappe.throw(_("Student is already enrolled via Course Enrollment {0}").format( frappe.throw(_("Student is already enrolled via Course Enrollment {0}").format(
get_link_to_form("Course Enrollment", enrollment)), title=_('Duplicate Entry')) get_link_to_form("Course Enrollment", enrollment)), title=_('Duplicate Entry'))
def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status): def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status, time_taken):
result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()} result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()}
result_data = [] result_data = []
for key in answers: for key in answers:
@ -66,7 +66,8 @@ class CourseEnrollment(Document):
"activity_date": frappe.utils.datetime.datetime.now(), "activity_date": frappe.utils.datetime.datetime.now(),
"result": result_data, "result": result_data,
"score": score, "score": score,
"status": status "status": status,
"time_taken": time_taken
}).insert(ignore_permissions = True) }).insert(ignore_permissions = True)
def add_activity(self, content_type, content): def add_activity(self, content_type, content):

View File

@ -13,6 +13,7 @@ from erpnext.education.utils import OverlapError
class CourseSchedulingTool(Document): class CourseSchedulingTool(Document):
@frappe.whitelist()
def schedule_course(self): def schedule_course(self):
"""Creates course schedules as per specified parameters""" """Creates course schedules as per specified parameters"""

View File

@ -52,6 +52,7 @@ class FeeSchedule(Document):
self.grand_total = no_of_students*self.total_amount self.grand_total = no_of_students*self.total_amount
self.grand_total_in_words = money_in_words(self.grand_total) self.grand_total_in_words = money_in_words(self.grand_total)
@frappe.whitelist()
def create_fees(self): def create_fees(self):
self.db_set("fee_creation_status", "In Process") self.db_set("fee_creation_status", "In Process")
frappe.publish_realtime("fee_schedule_progress", frappe.publish_realtime("fee_schedule_progress",

View File

@ -91,6 +91,8 @@ class ProgramEnrollment(Document):
(fee, fee) for fee in fee_list] (fee, fee) for fee in fee_list]
msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list))) msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list)))
@frappe.whitelist()
def get_courses(self): def get_courses(self):
return frappe.db.sql('''select course from `tabProgram Course` where parent = %s and required = 1''', (self.program), as_dict=1) return frappe.db.sql('''select course from `tabProgram Course` where parent = %s and required = 1''', (self.program), as_dict=1)

View File

@ -14,6 +14,7 @@ class ProgramEnrollmentTool(Document):
academic_term_reqd = cint(frappe.db.get_single_value('Education Settings', 'academic_term_reqd')) academic_term_reqd = cint(frappe.db.get_single_value('Education Settings', 'academic_term_reqd'))
self.set_onload("academic_term_reqd", academic_term_reqd) self.set_onload("academic_term_reqd", academic_term_reqd)
@frappe.whitelist()
def get_students(self): def get_students(self):
students = [] students = []
if not self.get_students_from: if not self.get_students_from:
@ -49,6 +50,7 @@ class ProgramEnrollmentTool(Document):
else: else:
frappe.throw(_("No students Found")) frappe.throw(_("No students Found"))
@frappe.whitelist()
def enroll_students(self): def enroll_students(self):
total = len(self.students) total = len(self.students)
for i, stud in enumerate(self.students): for i, stud in enumerate(self.students):

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:title", "autoname": "field:title",
@ -12,7 +13,10 @@
"quiz_configuration_section", "quiz_configuration_section",
"passing_score", "passing_score",
"max_attempts", "max_attempts",
"grading_basis" "grading_basis",
"column_break_7",
"is_time_bound",
"duration"
], ],
"fields": [ "fields": [
{ {
@ -58,9 +62,26 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Grading Basis", "label": "Grading Basis",
"options": "Latest Highest Score\nLatest Attempt" "options": "Latest Highest Score\nLatest Attempt"
},
{
"default": "0",
"fieldname": "is_time_bound",
"fieldtype": "Check",
"label": "Is Time-Bound"
},
{
"depends_on": "is_time_bound",
"fieldname": "duration",
"fieldtype": "Duration",
"label": "Duration"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
} }
], ],
"modified": "2019-06-12 12:23:57.020508", "links": [],
"modified": "2020-12-24 15:41:35.043262",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Quiz", "name": "Quiz",

View File

@ -1,490 +1,163 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "format:EDU-QA-{YYYY}-{#####}", "autoname": "format:EDU-QA-{YYYY}-{#####}",
"beta": 1, "beta": 1,
"creation": "2018-10-15 15:48:40.482821", "creation": "2018-10-15 15:48:40.482821",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"enrollment",
"student",
"column_break_3",
"course",
"section_break_5",
"quiz",
"column_break_7",
"status",
"section_break_9",
"result",
"section_break_11",
"activity_date",
"score",
"column_break_14",
"time_taken"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enrollment", "fieldname": "enrollment",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Enrollment", "label": "Enrollment",
"length": 0,
"no_copy": 0,
"options": "Course Enrollment", "options": "Course Enrollment",
"permlevel": 0, "set_only_once": 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": 1,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "enrollment.student", "fetch_from": "enrollment.student",
"fieldname": "student", "fieldname": "student",
"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": "Student", "label": "Student",
"length": 0,
"no_copy": 0,
"options": "Student", "options": "Student",
"permlevel": 0, "read_only": 1
"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": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"fetch_from": "enrollment.course", "fetch_from": "enrollment.course",
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Course", "label": "Course",
"length": 0,
"no_copy": 0,
"options": "Course", "options": "Course",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "set_only_once": 1
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 1,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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": "quiz", "fieldname": "quiz",
"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": "Quiz", "label": "Quiz",
"length": 0,
"no_copy": 0,
"options": "Quiz", "options": "Quiz",
"permlevel": 0, "set_only_once": 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": 1,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7", "fieldname": "column_break_7",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Status", "label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nPass\nFail", "options": "\nPass\nFail",
"permlevel": 0, "read_only": 1
"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": "section_break_9", "fieldname": "section_break_9",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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": "result", "fieldname": "result",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Result", "label": "Result",
"length": 0,
"no_copy": 0,
"options": "Quiz Result", "options": "Quiz Result",
"permlevel": 0, "set_only_once": 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": 1,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "activity_date", "fieldname": "activity_date",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Activity Date", "label": "Activity Date",
"length": 0, "set_only_once": 1
"no_copy": 0,
"permlevel": 0,
"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": 1,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "score", "fieldname": "score",
"fieldtype": "Data", "fieldtype": "Data",
"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": "Score", "label": "Score",
"length": 0, "set_only_once": 1
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "fieldname": "time_taken",
"print_hide": 0, "fieldtype": "Duration",
"print_hide_if_no_value": 0, "label": "Time Taken",
"read_only": 0, "set_only_once": 1
"remember_last_selected_value": 0, },
"report_hide": 0, {
"reqd": 0, "fieldname": "section_break_11",
"search_index": 0, "fieldtype": "Section Break"
"set_only_once": 1, },
"translatable": 0, {
"unique": 0 "fieldname": "column_break_14",
"fieldtype": "Column Break"
} }
], ],
"has_web_view": 0, "links": [],
"hide_heading": 0, "modified": "2020-12-24 15:41:20.085380",
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-11-25 19:05:52.434437",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Quiz Activity", "name": "Quiz Activity",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Academics User", "role": "Academics User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "LMS User", "role": "LMS User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Instructor", "role": "Instructor",
"set_user_permissions": 0, "share": 1
"share": 1,
"submit": 0,
"write": 0
} }
], ],
"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
} }

View File

@ -114,7 +114,7 @@ class Student(Document):
status = check_content_completion(content.name, content.doctype, course_enrollment_name) status = check_content_completion(content.name, content.doctype, course_enrollment_name)
progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status}) progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status})
elif content.doctype == 'Quiz': elif content.doctype == 'Quiz':
status, score, result = check_quiz_completion(content, course_enrollment_name) status, score, result, time_taken = check_quiz_completion(content, course_enrollment_name)
progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status, 'score': score, 'result': result}) progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status, 'score': score, 'result': result})
return progress return progress

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