mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-28 23:18:19 +00:00
Compare commits
436 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dec4a2454 | ||
|
|
7e910f21b8 | ||
|
|
b27c4ac6e2 | ||
|
|
a451c49a42 | ||
|
|
427d8c865a | ||
|
|
5ed44bdb88 | ||
|
|
eb36ebee8f | ||
|
|
159e492053 | ||
|
|
6dbb3cdf75 | ||
|
|
25012399fa | ||
|
|
4ab6994b3d | ||
|
|
e618571fb3 | ||
|
|
c99a14a7f3 | ||
|
|
db24fca8c1 | ||
|
|
e095e27cb9 | ||
|
|
bdfb478b3b | ||
|
|
2428812dcc | ||
|
|
677a3975ba | ||
|
|
5d37c9c38d | ||
|
|
b836d4fe25 | ||
|
|
153fd355bc | ||
|
|
350cac9207 | ||
|
|
c054d1f7ee | ||
|
|
68b337316a | ||
|
|
1ab3714fd0 | ||
|
|
dbb3ea3a6e | ||
|
|
abfccde664 | ||
|
|
293ec00023 | ||
|
|
061a569f4d | ||
|
|
8c0613a600 | ||
|
|
862961d92e | ||
|
|
5ac11a4ba5 | ||
|
|
2bc42759d2 | ||
|
|
9425ca07f5 | ||
|
|
2b868e7f56 | ||
|
|
f99f537b57 | ||
|
|
a7b30d9197 | ||
|
|
b3f36efa8f | ||
|
|
8aa6423093 | ||
|
|
144c8ec904 | ||
|
|
bed7201efe | ||
|
|
e6f2343fb4 | ||
|
|
9da63f2e79 | ||
|
|
f31e52123a | ||
|
|
2c83f18864 | ||
|
|
0fb4e81a30 | ||
|
|
dc82becbd2 | ||
|
|
70dc77305b | ||
|
|
db3eef9a8b | ||
|
|
cc08125f39 | ||
|
|
997ddbea78 | ||
|
|
ebe9599dda | ||
|
|
8535ad5294 | ||
|
|
eeb2402764 | ||
|
|
e7e0359e3e | ||
|
|
c24724ac7a | ||
|
|
5f9fdba49e | ||
|
|
5dd443eeb0 | ||
|
|
de01f0f364 | ||
|
|
4e14d28564 | ||
|
|
03bd68654b | ||
|
|
67689e7374 | ||
|
|
4e4855fed7 | ||
|
|
352c585ef5 | ||
|
|
d2b61bbe8a | ||
|
|
58fd7044b5 | ||
|
|
177a209f50 | ||
|
|
034773a670 | ||
|
|
63ccddfcc9 | ||
|
|
ecba064161 | ||
|
|
895dd68691 | ||
|
|
d361e23a59 | ||
|
|
e8bc4f12a7 | ||
|
|
aba8788ea2 | ||
|
|
3dfe7faf70 | ||
|
|
f7593f599e | ||
|
|
51518cf978 | ||
|
|
f6ee0c70e2 | ||
|
|
abd8291012 | ||
|
|
37bbdc6743 | ||
|
|
24011bc6a4 | ||
|
|
1273221477 | ||
|
|
b96fa03228 | ||
|
|
bbd777a9cb | ||
|
|
384c6234d5 | ||
|
|
540690252f | ||
|
|
43a21f2836 | ||
|
|
2fbaba40b5 | ||
|
|
096e7b9ec5 | ||
|
|
5c8fc3890f | ||
|
|
f44d90e097 | ||
|
|
4305d58273 | ||
|
|
194b7fbf2f | ||
|
|
f9de807b25 | ||
|
|
5ed4b43b73 | ||
|
|
315d542c2a | ||
|
|
bfbcde7595 | ||
|
|
ef782fd35b | ||
|
|
1538076ae4 | ||
|
|
f60ea20145 | ||
|
|
91887f215d | ||
|
|
0e413ef54e | ||
|
|
f336b5d586 | ||
|
|
b27fe43920 | ||
|
|
2be15bd8e0 | ||
|
|
398fa6eb48 | ||
|
|
d9f783f38e | ||
|
|
0da75d745e | ||
|
|
522031fd73 | ||
|
|
927efffcb7 | ||
|
|
454ae7e0ed | ||
|
|
c81cf33dce | ||
|
|
6aaa187a49 | ||
|
|
f2d8d7ab96 | ||
|
|
fde6f13b4b | ||
|
|
2185016f71 | ||
|
|
33b5e1d7e6 | ||
|
|
4fec250cbf | ||
|
|
c1dd3d7d9e | ||
|
|
8e2b96e814 | ||
|
|
398fab4786 | ||
|
|
844b73efcd | ||
|
|
9869b31b31 | ||
|
|
04e3fb85e5 | ||
|
|
1f099bcdfb | ||
|
|
ceba7f2fe4 | ||
|
|
696fc0a482 | ||
|
|
7d130af12b | ||
|
|
29241070c1 | ||
|
|
d32f229483 | ||
|
|
8a9a13cb96 | ||
|
|
d190ea4ad3 | ||
|
|
eb44b16e31 | ||
|
|
b9a42807f8 | ||
|
|
655d8980dc | ||
|
|
e963d0e8e0 | ||
|
|
77cb7ebd40 | ||
|
|
84803f8f21 | ||
|
|
6a53cfea00 | ||
|
|
829ea71ae1 | ||
|
|
25398d017b | ||
|
|
f75d36dcb4 | ||
|
|
56dd25355d | ||
|
|
fa7f5d745e | ||
|
|
dd81d622dd | ||
|
|
4086f4f8d1 | ||
|
|
311b2dd1a9 | ||
|
|
0783b36ba0 | ||
|
|
f491cc92cd | ||
|
|
824eef9354 | ||
|
|
216f45ce11 | ||
|
|
6c45223606 | ||
|
|
93d665b38a | ||
|
|
44d2098530 | ||
|
|
3023757a60 | ||
|
|
add36363ab | ||
|
|
93325c72f0 | ||
|
|
0bb0a9d032 | ||
|
|
f541b5c780 | ||
|
|
dc0d974144 | ||
|
|
8775154fa0 | ||
|
|
b917f33399 | ||
|
|
79ce36c19f | ||
|
|
c8eae9dcef | ||
|
|
69fded250c | ||
|
|
3064d95889 | ||
|
|
d7f16c4924 | ||
|
|
ac2d1a9acc | ||
|
|
8d3f8619cf | ||
|
|
d5d7f67ac2 | ||
|
|
5e3f5d8bcc | ||
|
|
c121ab8ef4 | ||
|
|
0b4b60a6c3 | ||
|
|
7542dd3f15 | ||
|
|
0411bcbd5c | ||
|
|
9a3a679628 | ||
|
|
a3a9920771 | ||
|
|
c2d58ba796 | ||
|
|
5da7e28d20 | ||
|
|
7f5415177e | ||
|
|
ea9756da8f | ||
|
|
a0c2c94cff | ||
|
|
3b0f80b865 | ||
|
|
83ca2422f6 | ||
|
|
de9a0d6a7f | ||
|
|
5db8af8cee | ||
|
|
42d3b0c063 | ||
|
|
ecbd885aad | ||
|
|
cfb43d159d | ||
|
|
f3ddf53d39 | ||
|
|
ef3e4ee61f | ||
|
|
045a505a38 | ||
|
|
eff48299e4 | ||
|
|
8ed138e373 | ||
|
|
a4e897d30b | ||
|
|
38a330b55e | ||
|
|
a08b10d5cf | ||
|
|
eb3d28fcc0 | ||
|
|
103bf19700 | ||
|
|
835448c9e5 | ||
|
|
94a30674df | ||
|
|
2524be6386 | ||
|
|
e28b046cf9 | ||
|
|
f1c08f9b8c | ||
|
|
80b037ed13 | ||
|
|
61681e73ef | ||
|
|
a868fd7df9 | ||
|
|
21c194495c | ||
|
|
884edb45b3 | ||
|
|
81f1b7dfeb | ||
|
|
dcea4f4c96 | ||
|
|
a4210a7c85 | ||
|
|
d01e794558 | ||
|
|
852543cbcd | ||
|
|
464f813677 | ||
|
|
366f663c5a | ||
|
|
b4fee6d065 | ||
|
|
f4a031d94b | ||
|
|
fa66d1d87d | ||
|
|
d71d769c7a | ||
|
|
e05ad8ed10 | ||
|
|
f8761dc5ca | ||
|
|
a0bd82e99f | ||
|
|
00ad0dd24d | ||
|
|
c773051a3e | ||
|
|
746bb6277f | ||
|
|
d9f31770c6 | ||
|
|
c1fbd2308c | ||
|
|
577de39ca6 | ||
|
|
de9429b585 | ||
|
|
819346f69f | ||
|
|
8bb72dece4 | ||
|
|
4343eddd46 | ||
|
|
f35ef60262 | ||
|
|
d0c7d4fc98 | ||
|
|
fbe0442e89 | ||
|
|
055d620ef3 | ||
|
|
495c5126f8 | ||
|
|
23f4b51e3e | ||
|
|
0174aa238c | ||
|
|
ac4e996928 | ||
|
|
d85aef5b2a | ||
|
|
822a8ba47a | ||
|
|
14aca20b00 | ||
|
|
ecf341d1b1 | ||
|
|
4ef9d289e8 | ||
|
|
9b6c2fdfb7 | ||
|
|
203943ef97 | ||
|
|
b561c18502 | ||
|
|
a333635ab6 | ||
|
|
892e63575e | ||
|
|
60ddf39e37 | ||
|
|
dfe2140f70 | ||
|
|
c4dab3eef3 | ||
|
|
e967aafc28 | ||
|
|
0e925c6ea2 | ||
|
|
49c33a67e1 | ||
|
|
7abe7c752b | ||
|
|
d243291e1c | ||
|
|
493af62038 | ||
|
|
b82d4b7577 | ||
|
|
2f98aa121a | ||
|
|
37d2f6fd15 | ||
|
|
6cc3c612ee | ||
|
|
1d87e9d8f6 | ||
|
|
a64efb764b | ||
|
|
d3820f6ce3 | ||
|
|
2f07d38caf | ||
|
|
48f98a68b8 | ||
|
|
485fa4700f | ||
|
|
f90b49a1b5 | ||
|
|
e8e93bdf43 | ||
|
|
1eccb83904 | ||
|
|
5900968bd9 | ||
|
|
fa735a0539 | ||
|
|
b808ef1625 | ||
|
|
06c0313a26 | ||
|
|
f4670a86a0 | ||
|
|
0b07fbd5a2 | ||
|
|
2838dc537e | ||
|
|
1d270dca05 | ||
|
|
3898b19086 | ||
|
|
fe1a66cf54 | ||
|
|
e2b2be82e0 | ||
|
|
fe3ebafea4 | ||
|
|
860a0f4696 | ||
|
|
a34ded79f3 | ||
|
|
d1739eb59a | ||
|
|
84f3f34821 | ||
|
|
50628d416c | ||
|
|
e648a2ccde | ||
|
|
1efce21ba4 | ||
|
|
110a1e5557 | ||
|
|
80f92ee547 | ||
|
|
0be144c193 | ||
|
|
bf92681fff | ||
|
|
3947d859c5 | ||
|
|
0222cd198d | ||
|
|
fca8f17b40 | ||
|
|
fcbcb08fcd | ||
|
|
674a923669 | ||
|
|
8a8648e6ec | ||
|
|
4fdaf51599 | ||
|
|
19154074c1 | ||
|
|
9a32bb6b51 | ||
|
|
cf4cac05ab | ||
|
|
39f7a9de47 | ||
|
|
342658ea70 | ||
|
|
7a5937a98c | ||
|
|
d763b16e9f | ||
|
|
2602fc633b | ||
|
|
79d126f5d0 | ||
|
|
c9a9d0bab8 | ||
|
|
0703c1f996 | ||
|
|
b378a225d0 | ||
|
|
12f5464a3b | ||
|
|
9d575e6e27 | ||
|
|
098b3614e1 | ||
|
|
bbe4bc1fc0 | ||
|
|
0f8a7d7a6c | ||
|
|
a4396d7f31 | ||
|
|
4248bebe48 | ||
|
|
b616443809 | ||
|
|
3c803c46d3 | ||
|
|
dc3a0337d1 | ||
|
|
430617cb68 | ||
|
|
2b1b831560 | ||
|
|
165114ba4c | ||
|
|
964b19e1b1 | ||
|
|
b9e327515b | ||
|
|
e0a88d99f4 | ||
|
|
00b9f1fbef | ||
|
|
59aaef3719 | ||
|
|
fdf5811c27 | ||
|
|
66a5b3f34d | ||
|
|
d8b781bb1e | ||
|
|
6c6785e284 | ||
|
|
fc57ed1ca8 | ||
|
|
aeb7bbc629 | ||
|
|
bdb6c4170b | ||
|
|
701bca5845 | ||
|
|
becb39da90 | ||
|
|
aa58653b75 | ||
|
|
bae184fc2f | ||
|
|
24727dbadb | ||
|
|
ba5f4843c0 | ||
|
|
bf266d82e7 | ||
|
|
e30c52861d | ||
|
|
57b7fe6627 | ||
|
|
c04b62a7dd | ||
|
|
19fd1fb4b1 | ||
|
|
b00f6676f1 | ||
|
|
8be6bd978e | ||
|
|
bffe329ade | ||
|
|
b0bc3994c3 | ||
|
|
d0d82bc34a | ||
|
|
1c2a8c679c | ||
|
|
39fd9ee40d | ||
|
|
f92cdbbbf3 | ||
|
|
3f6d9336b5 | ||
|
|
b2e42aa16c | ||
|
|
6783fc808b | ||
|
|
bcc436e63e | ||
|
|
7eafc9ffe8 | ||
|
|
ff183ee895 | ||
|
|
808e40cc4d | ||
|
|
2777b501b5 | ||
|
|
a6e7b43f0b | ||
|
|
ffdaa849fb | ||
|
|
85abb95df6 | ||
|
|
b60fbf5ba9 | ||
|
|
233f79bf96 | ||
|
|
dd01c0e854 | ||
|
|
de697a7aee | ||
|
|
b59df1118e | ||
|
|
242cc7e23e | ||
|
|
dfe718ef79 | ||
|
|
84404bf6e3 | ||
|
|
56d72764b7 | ||
|
|
84cebca254 | ||
|
|
7e912db4b1 | ||
|
|
3fed740142 | ||
|
|
824c90fd11 | ||
|
|
ef47570977 | ||
|
|
87e53e9e46 | ||
|
|
4107de4514 | ||
|
|
9338843dac | ||
|
|
2d5f477720 | ||
|
|
fa7948b5e5 | ||
|
|
aad7a10a7b | ||
|
|
c12b00cb4b | ||
|
|
0980c2f981 | ||
|
|
4b3ef24378 | ||
|
|
6087d5a603 | ||
|
|
372f91c923 | ||
|
|
0724a148e6 | ||
|
|
d3c62821fb | ||
|
|
c5931e4b1e | ||
|
|
a1d5e0bef6 | ||
|
|
10a95b3a98 | ||
|
|
7c1bfe6b46 | ||
|
|
b33fd6acc7 | ||
|
|
9c2a9c28df | ||
|
|
349785dcbd | ||
|
|
94d3e66428 | ||
|
|
eb36742427 | ||
|
|
fcb08d54ea | ||
|
|
167e8d3dde | ||
|
|
85ded9e7ce | ||
|
|
74ac5e7829 | ||
|
|
804f2ccf3d | ||
|
|
039b1fbb76 | ||
|
|
970cdef911 | ||
|
|
1988e72710 | ||
|
|
cd075ce3be | ||
|
|
bdd43274b2 | ||
|
|
25fd11e24f | ||
|
|
73693ac153 | ||
|
|
a4814f5519 | ||
|
|
a6f9b523d9 | ||
|
|
d0d5236a1a | ||
|
|
b3720f62d8 | ||
|
|
187a69e390 | ||
|
|
55db2f8808 | ||
|
|
1a986f0b11 | ||
|
|
5798687951 | ||
|
|
46a6c8dbfe | ||
|
|
1f41c7fed1 | ||
|
|
1c96c6fe08 | ||
|
|
d000782de0 | ||
|
|
855ae399b5 | ||
|
|
ae875ab021 | ||
|
|
57f41bc30a | ||
|
|
8a4019e501 | ||
|
|
54cac351cd | ||
|
|
db3d2c31a3 |
1
.flake8
1
.flake8
@@ -28,6 +28,7 @@ ignore =
|
||||
B007,
|
||||
B950,
|
||||
W191,
|
||||
E124, # closing bracket, irritating while writing QB code
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
|
||||
10
.github/workflows/patch.yml
vendored
10
.github/workflows/patch.yml
vendored
@@ -5,9 +5,14 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
types: [opened, unlabeled, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
concurrency:
|
||||
group: patch-mariadb-v13-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
@@ -25,6 +30,11 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Check for merge conficts label
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
|
||||
run: |
|
||||
echo "Remove merge conflicts and remove conflict label to run CI"
|
||||
exit 1
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
44
.github/workflows/server-tests.yml
vendored
44
.github/workflows/server-tests.yml
vendored
@@ -5,6 +5,7 @@ on:
|
||||
paths-ignore:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
types: [opened, unlabeled, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
@@ -12,6 +13,10 @@ on:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
|
||||
concurrency:
|
||||
group: server-mariadb-v13-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
@@ -35,6 +40,12 @@ jobs:
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
|
||||
steps:
|
||||
- name: Check for merge conficts label
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
|
||||
run: |
|
||||
echo "Remove merge conflicts and remove conflict label to run CI"
|
||||
exit 1
|
||||
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -89,39 +100,8 @@ jobs:
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
|
||||
- name: Run Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
|
||||
env:
|
||||
TYPE: server
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload Coverage Data
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip3 install coverage==5.5
|
||||
pip3 install coveralls==3.0.1
|
||||
coveralls
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
|
||||
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
|
||||
COVERALLS_PARALLEL: true
|
||||
|
||||
coveralls:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
container: python:3-slim
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Coveralls Finished
|
||||
run: |
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip3 install coverage==5.5
|
||||
pip3 install coveralls==3.0.1
|
||||
coveralls --finish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.github/workflows/ui-tests.yml
vendored
4
.github/workflows/ui-tests.yml
vendored
@@ -6,6 +6,10 @@ on:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ui-v13-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = '13.17.0'
|
||||
__version__ = '13.20.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
@@ -56,9 +56,9 @@ def set_perpetual_inventory(enable=1, company=None):
|
||||
company.enable_perpetual_inventory = enable
|
||||
company.save()
|
||||
|
||||
def encode_company_abbr(name, company):
|
||||
def encode_company_abbr(name, company=None, abbr=None):
|
||||
'''Returns name encoded with company abbreviation'''
|
||||
company_abbr = frappe.get_cached_value('Company', company, "abbr")
|
||||
company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr")
|
||||
parts = name.rsplit(" - ", 1)
|
||||
|
||||
if parts[-1].lower() != company_abbr.lower():
|
||||
|
||||
@@ -255,11 +255,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = "enable_deferred_revenue" \
|
||||
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
|
||||
|
||||
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
|
||||
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
|
||||
if not (start_date and end_date): return
|
||||
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
account_currency = get_account_currency(item.expense_account or item.income_account)
|
||||
if doc.doctype == "Sales Invoice":
|
||||
against, project = doc.customer, doc.project
|
||||
credit_account, debit_account = item.income_account, item.deferred_revenue_account
|
||||
@@ -280,6 +282,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
if not amount:
|
||||
return
|
||||
|
||||
# check if books nor frozen till endate:
|
||||
if getdate(end_date) >= getdate(accounts_frozen_upto):
|
||||
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
|
||||
if via_journal_entry:
|
||||
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
|
||||
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
|
||||
@@ -407,8 +413,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
'account': credit_account,
|
||||
'credit': base_amount,
|
||||
'credit_in_account_currency': amount,
|
||||
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
|
||||
'party': against,
|
||||
'account_currency': account_currency,
|
||||
'reference_name': doc.name,
|
||||
'reference_type': doc.doctype,
|
||||
@@ -421,8 +425,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
'account': debit_account,
|
||||
'debit': base_amount,
|
||||
'debit_in_account_currency': amount,
|
||||
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
|
||||
'party': against,
|
||||
'account_currency': account_currency,
|
||||
'reference_name': doc.name,
|
||||
'reference_type': doc.doctype,
|
||||
|
||||
@@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
|
||||
frm.trigger('add_toolbar_buttons');
|
||||
}
|
||||
if (frm.has_perm('write')) {
|
||||
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
||||
frm.trigger("update_account_number");
|
||||
});
|
||||
frm.add_custom_button(__('Merge Account'), function () {
|
||||
frm.trigger("merge_account");
|
||||
});
|
||||
}, __('Actions'));
|
||||
frm.add_custom_button(__('Update Account Name / Number'), function () {
|
||||
frm.trigger("update_account_number");
|
||||
}, __('Actions'));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -59,11 +59,12 @@ frappe.ui.form.on('Account', {
|
||||
}
|
||||
},
|
||||
add_toolbar_buttons: function(frm) {
|
||||
frm.add_custom_button(__('Chart of Accounts'),
|
||||
function () { frappe.set_route("Tree", "Account"); });
|
||||
frm.add_custom_button(__('Chart of Accounts'), () => {
|
||||
frappe.set_route("Tree", "Account");
|
||||
}, __('View'));
|
||||
|
||||
if (frm.doc.is_group == 1) {
|
||||
frm.add_custom_button(__('Group to Non-Group'), function () {
|
||||
frm.add_custom_button(__('Convert to Non-Group'), function () {
|
||||
return frappe.call({
|
||||
doc: frm.doc,
|
||||
method: 'convert_group_to_ledger',
|
||||
@@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, __('Actions'));
|
||||
|
||||
} else if (cint(frm.doc.is_group) == 0
|
||||
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
|
||||
frm.add_custom_button(__('Ledger'), function () {
|
||||
frm.add_custom_button(__('General Ledger'), function () {
|
||||
frappe.route_options = {
|
||||
"account": frm.doc.name,
|
||||
"from_date": frappe.sys_defaults.year_start_date,
|
||||
@@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
|
||||
"company": frm.doc.company
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
});
|
||||
}, __('View'));
|
||||
|
||||
frm.add_custom_button(__('Non-Group to Group'), function () {
|
||||
frm.add_custom_button(__('Convert to Group'), function () {
|
||||
return frappe.call({
|
||||
doc: frm.doc,
|
||||
method: 'convert_ledger_to_group',
|
||||
@@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, __('Actions'));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
frm.set_query("bank_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: ["in", frm.doc.company],
|
||||
company: frm.doc.company,
|
||||
'is_company_account': 1
|
||||
},
|
||||
};
|
||||
|
||||
@@ -218,6 +218,8 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
# updated clear date of all the vouchers based on the bank transaction
|
||||
vouchers = json.loads(vouchers)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account')
|
||||
|
||||
if transaction.unallocated_amount == 0:
|
||||
frappe.throw(_("This bank transaction is already fully reconciled"))
|
||||
total_amount = 0
|
||||
@@ -226,7 +228,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
total_amount += get_paid_amount(frappe._dict({
|
||||
'payment_document': voucher['payment_doctype'],
|
||||
'payment_entry': voucher['payment_name'],
|
||||
}), transaction.currency)
|
||||
}), transaction.currency, company_account)
|
||||
|
||||
if total_amount > transaction.unallocated_amount:
|
||||
frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
|
||||
@@ -261,7 +263,7 @@ def get_linked_payments(bank_transaction_name, document_types = None):
|
||||
return matching
|
||||
|
||||
def check_matching(bank_account, company, transaction, document_types):
|
||||
# combine all types of vocuhers
|
||||
# combine all types of vouchers
|
||||
subquery = get_queries(bank_account, company, transaction, document_types)
|
||||
filters = {
|
||||
"amount": transaction.unallocated_amount,
|
||||
@@ -343,13 +345,11 @@ def get_pe_matching_query(amount_condition, account_from_to, transaction):
|
||||
def get_je_matching_query(amount_condition, transaction):
|
||||
# get matching journal entry query
|
||||
|
||||
# We have mapping at the bank level
|
||||
# So one bank could have both types of bank accounts like asset and liability
|
||||
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
|
||||
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
|
||||
root_type = frappe.get_value("Account", company_account, "root_type")
|
||||
|
||||
if root_type == "Liability":
|
||||
cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
|
||||
else:
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
|
||||
|
||||
return f"""
|
||||
|
||||
|
||||
@@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
"withdrawal",
|
||||
"description",
|
||||
"reference_number",
|
||||
"bank_account"
|
||||
"bank_account",
|
||||
"currency"
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ from openpyxl.styles import Font
|
||||
from openpyxl.utils import get_column_letter
|
||||
from six import string_types
|
||||
|
||||
INVALID_VALUES = ("", None)
|
||||
|
||||
class BankStatementImport(DataImport):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -96,6 +97,18 @@ def download_errored_template(data_import_name):
|
||||
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
|
||||
data_import.export_errored_rows()
|
||||
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
for i, row in enumerate(raw_data):
|
||||
if all(v in INVALID_VALUES for v in row):
|
||||
# empty row
|
||||
continue
|
||||
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
|
||||
"""This method runs in background job"""
|
||||
|
||||
@@ -105,7 +118,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
file = import_file_path if import_file_path else google_sheets_url
|
||||
|
||||
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
|
||||
data = import_file.raw_data
|
||||
|
||||
data = parse_data_from_template(import_file.raw_data)
|
||||
|
||||
if import_file_path:
|
||||
add_bank_account(data, bank_account)
|
||||
|
||||
@@ -102,7 +102,7 @@ def get_total_allocated_amount(payment_entry):
|
||||
AND
|
||||
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
|
||||
|
||||
def get_paid_amount(payment_entry, currency):
|
||||
def get_paid_amount(payment_entry, currency, bank_account):
|
||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||
|
||||
paid_amount_field = "paid_amount"
|
||||
@@ -115,7 +115,7 @@ def get_paid_amount(payment_entry, currency):
|
||||
payment_entry.payment_entry, paid_amount_field)
|
||||
|
||||
elif payment_entry.payment_document == "Journal Entry":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")
|
||||
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
|
||||
|
||||
elif payment_entry.payment_document == "Expense Claim":
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
|
||||
|
||||
@@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
if(frm.doc.docstatus==1) {
|
||||
frm.add_custom_button(__('Reverse Journal Entry'), function() {
|
||||
return erpnext.journal_entry.reverse_journal_entry(frm);
|
||||
}, __('Make'));
|
||||
}, __('Actions'));
|
||||
}
|
||||
|
||||
if (frm.doc.__islocal) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
"finance_book",
|
||||
"reversal_of",
|
||||
"tax_withholding_category",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
@@ -515,13 +516,21 @@
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Tax Withholding Amount "
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus",
|
||||
"fieldname": "reversal_of",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reversal Of",
|
||||
"options": "Journal Entry",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-09 15:31:14.484029",
|
||||
"modified": "2022-01-04 13:39:36.485954",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -397,13 +397,14 @@ class JournalEntry(AccountsController):
|
||||
debit_or_credit = 'Debit' if d.debit else 'Credit'
|
||||
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
|
||||
debit_or_credit)
|
||||
against_voucher = ['', against_voucher[1]]
|
||||
else:
|
||||
if d.reference_type == "Sales Invoice":
|
||||
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
|
||||
else:
|
||||
party_account = against_voucher[1]
|
||||
|
||||
if (against_voucher[0] != d.party or party_account != d.account):
|
||||
if (against_voucher[0] != cstr(d.party) or party_account != d.account):
|
||||
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
|
||||
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
|
||||
d.reference_type, d.reference_name))
|
||||
@@ -468,13 +469,22 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def set_against_account(self):
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
|
||||
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
|
||||
if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
|
||||
for d in self.get('accounts'):
|
||||
if d.reference_type == 'Sales Invoice':
|
||||
field = 'customer'
|
||||
else:
|
||||
field = 'supplier'
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
|
||||
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
|
||||
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
|
||||
else:
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
|
||||
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
|
||||
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
|
||||
|
||||
def validate_debit_credit_amount(self):
|
||||
for d in self.get('accounts'):
|
||||
@@ -1147,9 +1157,8 @@ def make_inter_company_journal_entry(name, voucher_type, company):
|
||||
def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
def update_accounts(source, target, source_parent):
|
||||
target.reference_type = "Journal Entry"
|
||||
target.reference_name = source_parent.name
|
||||
def post_process(source, target):
|
||||
target.reversal_of = source.name
|
||||
|
||||
doclist = get_mapped_doc("Journal Entry", source_name, {
|
||||
"Journal Entry": {
|
||||
@@ -1167,9 +1176,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
"debit": "credit",
|
||||
"credit_in_account_currency": "debit_in_account_currency",
|
||||
"credit": "debit",
|
||||
},
|
||||
"postprocess": update_accounts,
|
||||
}
|
||||
},
|
||||
}, target_doc)
|
||||
}, target_doc, post_process)
|
||||
|
||||
return doclist
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2019-07-25 14:57:33.187689",
|
||||
"modified": "2022-01-04 16:25:06.053187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool",
|
||||
|
||||
@@ -135,7 +135,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
|
||||
rate = flt(row.outstanding_amount) / flt(row.qty)
|
||||
|
||||
return frappe._dict({
|
||||
item_dict = frappe._dict({
|
||||
"uom": default_uom,
|
||||
"rate": rate or 0.0,
|
||||
"qty": row.qty,
|
||||
@@ -146,6 +146,13 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"cost_center": cost_center
|
||||
})
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
item_dict.update({
|
||||
dimension: row.get(dimension)
|
||||
})
|
||||
|
||||
return item_dict
|
||||
|
||||
item = get_item_dict()
|
||||
|
||||
invoice = frappe._dict({
|
||||
@@ -159,13 +166,14 @@ class OpeningInvoiceCreationTool(Document):
|
||||
frappe.scrub(row.party_type): row.party,
|
||||
"is_pos": 0,
|
||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
||||
"update_stock": 0
|
||||
"update_stock": 0,
|
||||
"invoice_number": row.invoice_number
|
||||
})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
for dimension in accounting_dimension:
|
||||
invoice.update({
|
||||
dimension: item.get(dimension)
|
||||
dimension: self.get(dimension) or item.get(dimension)
|
||||
})
|
||||
|
||||
return invoice
|
||||
@@ -200,10 +208,13 @@ def start_import(invoices):
|
||||
names = []
|
||||
for idx, d in enumerate(invoices):
|
||||
try:
|
||||
invoice_number = None
|
||||
if d.invoice_number:
|
||||
invoice_number = d.invoice_number
|
||||
publish(idx, len(invoices), d.doctype)
|
||||
doc = frappe.get_doc(d)
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert()
|
||||
doc.insert(set_name=invoice_number)
|
||||
doc.submit()
|
||||
frappe.db.commit()
|
||||
names.append(doc.name)
|
||||
|
||||
@@ -7,21 +7,26 @@ import frappe
|
||||
from frappe.cache_manager import clear_doctype_cache
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
create_dimension,
|
||||
disable_dimension,
|
||||
)
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
|
||||
test_dependencies = ["Customer", "Supplier"]
|
||||
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
|
||||
|
||||
class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||
make_company()
|
||||
create_dimension()
|
||||
|
||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
|
||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
|
||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
|
||||
party_1=party_1, party_2=party_2)
|
||||
party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department)
|
||||
doc.update(args)
|
||||
return doc.make_invoices()
|
||||
|
||||
@@ -92,6 +97,33 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
# teardown
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
|
||||
|
||||
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
|
||||
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
|
||||
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
|
||||
|
||||
#teardown
|
||||
for inv in [sales_inv1, sales_inv2]:
|
||||
doc = frappe.get_doc('Sales Invoice', inv)
|
||||
doc.cancel()
|
||||
|
||||
def test_opening_invoice_with_accounting_dimension(self):
|
||||
invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC')
|
||||
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status", "department"],
|
||||
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
|
||||
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
|
||||
|
||||
def tearDown(self):
|
||||
disable_dimension()
|
||||
|
||||
def get_opening_invoice_creation_dict(**args):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
company = args.get("company", "_Test Company")
|
||||
@@ -107,7 +139,8 @@ def get_opening_invoice_creation_dict(**args):
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company)
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": args.get("invoice_number")
|
||||
},
|
||||
{
|
||||
"qty": 2.0,
|
||||
@@ -116,7 +149,8 @@ def get_opening_invoice_creation_dict(**args):
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
"temporary_opening_account": get_temporary_opening_account(company)
|
||||
"temporary_opening_account": get_temporary_opening_account(company),
|
||||
"invoice_number": None
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -132,7 +166,7 @@ def make_company():
|
||||
company.company_name = "_Test Opening Invoice Company"
|
||||
company.abbr = "_TOIC"
|
||||
company.default_currency = "INR"
|
||||
company.country = "India"
|
||||
company.country = "Pakistan"
|
||||
company.insert()
|
||||
return company
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2017-08-29 04:26:36.159247",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"invoice_number",
|
||||
"party_type",
|
||||
"party",
|
||||
"temporary_opening_account",
|
||||
@@ -103,10 +105,18 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Reference number of the invoice from the previous system",
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice Number"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-07-25 15:00:00.460695",
|
||||
"links": [],
|
||||
"modified": "2022-01-04 18:40:15.927675",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool Item",
|
||||
@@ -116,4 +126,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import json
|
||||
from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, scrub, throw
|
||||
@@ -1524,6 +1525,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
|
||||
if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']:
|
||||
pe.project = (doc.get('project') or
|
||||
reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items
|
||||
|
||||
if pe.party_type in ["Customer", "Supplier"]:
|
||||
bank_account = get_party_bank_account(pe.party_type, pe.party)
|
||||
pe.set("bank_account", bank_account)
|
||||
@@ -1709,7 +1714,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
|
||||
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||
total_discount = 0
|
||||
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
|
||||
eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
|
||||
has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
|
||||
|
||||
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||
for term in doc.payment_schedule:
|
||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
||||
if term.discount_type == 'Percentage':
|
||||
|
||||
@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_multi_mode_option,
|
||||
)
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
|
||||
|
||||
|
||||
@@ -125,9 +126,26 @@ class POSInvoice(SalesInvoice):
|
||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
elif invalid_serial_nos:
|
||||
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
|
||||
def validate_pos_reserved_batch_qty(self, item):
|
||||
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
|
||||
|
||||
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
|
||||
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
|
||||
|
||||
bold_item_name = frappe.bold(item.item_name)
|
||||
bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
|
||||
bold_invalid_batch_no = frappe.bold(item.batch_no)
|
||||
|
||||
if (available_batch_qty - reserved_batch_qty) == 0:
|
||||
frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
|
||||
.format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
|
||||
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
|
||||
frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
|
||||
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
|
||||
|
||||
def validate_delivered_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
||||
@@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice):
|
||||
if d.serial_no:
|
||||
self.validate_pos_reserved_serial_nos(d)
|
||||
self.validate_delivered_serial_nos(d)
|
||||
elif d.batch_no:
|
||||
self.validate_pos_reserved_batch_qty(d)
|
||||
else:
|
||||
if allow_negative_stock:
|
||||
return
|
||||
@@ -334,7 +354,6 @@ class POSInvoice(SalesInvoice):
|
||||
if not for_validate and not self.customer:
|
||||
self.customer = profile.customer
|
||||
|
||||
self.ignore_pricing_rule = profile.ignore_pricing_rule
|
||||
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
|
||||
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
||||
|
||||
|
||||
@@ -521,6 +521,72 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 400)
|
||||
|
||||
def test_pos_batch_item_qty_validation(self):
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_batch_item_with_batch,
|
||||
)
|
||||
create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01')
|
||||
item = frappe.get_doc('Item', '_BATCH ITEM')
|
||||
batch = frappe.get_doc('Batch', 'TestBatch 01')
|
||||
batch.submit()
|
||||
item.batch_no = 'TestBatch 01'
|
||||
item.save()
|
||||
|
||||
se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01')
|
||||
|
||||
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
|
||||
pos_inv1.items[0].batch_no = 'TestBatch 01'
|
||||
pos_inv1.save()
|
||||
pos_inv1.submit()
|
||||
|
||||
pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
|
||||
pos_inv2.items[0].batch_no = 'TestBatch 01'
|
||||
pos_inv2.save()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos_inv2.submit)
|
||||
|
||||
#teardown
|
||||
pos_inv1.reload()
|
||||
pos_inv1.cancel()
|
||||
pos_inv1.delete()
|
||||
pos_inv2.reload()
|
||||
pos_inv2.delete()
|
||||
se.cancel()
|
||||
batch.reload()
|
||||
batch.cancel()
|
||||
batch.delete()
|
||||
|
||||
def test_ignore_pricing_rule(self):
|
||||
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
|
||||
|
||||
item_price = frappe.get_doc({
|
||||
'doctype': 'Item Price',
|
||||
'item_code': '_Test Item',
|
||||
'price_list': '_Test Price List',
|
||||
'price_list_rate': '450',
|
||||
})
|
||||
item_price.insert()
|
||||
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
|
||||
pr.save()
|
||||
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
|
||||
# rate shouldn't change
|
||||
self.assertEquals(pos_inv.items[0].rate, 405)
|
||||
|
||||
pos_inv.ignore_pricing_rule = 1
|
||||
pos_inv.items[0].rate = 300
|
||||
pos_inv.save()
|
||||
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
|
||||
# rate should change since pricing rules are ignored
|
||||
self.assertEquals(pos_inv.items[0].rate, 300)
|
||||
|
||||
item_price.delete()
|
||||
pos_inv.delete()
|
||||
pr.delete()
|
||||
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
args = frappe._dict(args)
|
||||
pos_profile = None
|
||||
@@ -557,7 +623,8 @@ def create_pos_invoice(**args):
|
||||
"income_account": args.income_account or "Sales - _TC",
|
||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"serial_no": args.serial_no
|
||||
"serial_no": args.serial_no,
|
||||
"batch_no": args.batch_no
|
||||
})
|
||||
|
||||
if not args.do_not_save:
|
||||
@@ -570,3 +637,8 @@ def create_pos_invoice(**args):
|
||||
pos_inv.payment_schedule = []
|
||||
|
||||
return pos_inv
|
||||
|
||||
def make_batch_item(item_name):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
if not frappe.db.exists(item_name):
|
||||
return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1))
|
||||
@@ -630,6 +630,26 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
def test_multiple_pricing_rules_with_min_qty(self):
|
||||
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
|
||||
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
|
||||
make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
|
||||
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
|
||||
item = si.items[0]
|
||||
item.stock_qty = 1
|
||||
si.save()
|
||||
self.assertFalse(item.discount_percentage)
|
||||
item.qty = 5
|
||||
item.stock_qty = 5
|
||||
si.save()
|
||||
self.assertEqual(item.discount_percentage, 30)
|
||||
si.delete()
|
||||
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
def make_pricing_rule(**args):
|
||||
@@ -652,7 +672,7 @@ def make_pricing_rule(**args):
|
||||
"rate": args.rate or 0.0,
|
||||
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
|
||||
"condition": args.condition or '',
|
||||
"priority": 1,
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.0,
|
||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
|
||||
})
|
||||
@@ -678,6 +698,8 @@ def make_pricing_rule(**args):
|
||||
if args.get(applicable_for):
|
||||
doc.db_set(applicable_for, args.get(applicable_for))
|
||||
|
||||
return doc
|
||||
|
||||
def setup_pricing_rule_data():
|
||||
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
||||
frappe.get_doc({
|
||||
|
||||
@@ -73,7 +73,7 @@ def sorted_by_priority(pricing_rules, args, doc=None):
|
||||
for key in sorted(pricing_rule_dict):
|
||||
pricing_rules_list.extend(pricing_rule_dict.get(key))
|
||||
|
||||
return pricing_rules_list or pricing_rules
|
||||
return pricing_rules_list
|
||||
|
||||
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
|
||||
filtered_pricing_rules = []
|
||||
|
||||
@@ -503,11 +503,11 @@ class PurchaseInvoice(BuyingController):
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
||||
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
|
||||
else self.base_grand_total, self.precision("base_grand_total"))
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
|
||||
self.precision("grand_total"))
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": self.credit_to,
|
||||
@@ -515,8 +515,8 @@ class PurchaseInvoice(BuyingController):
|
||||
"party": self.supplier,
|
||||
"due_date": self.due_date,
|
||||
"against": self.against_expense_account,
|
||||
"credit": grand_total_in_company_currency,
|
||||
"credit_in_account_currency": grand_total_in_company_currency \
|
||||
"credit": base_grand_total,
|
||||
"credit_in_account_currency": base_grand_total \
|
||||
if self.party_account_currency==self.company_currency else grand_total,
|
||||
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
@@ -543,6 +543,11 @@ class PurchaseInvoice(BuyingController):
|
||||
if d.category in ('Valuation', 'Total and Valuation')
|
||||
and flt(d.base_tax_amount_after_discount_amount)]
|
||||
|
||||
provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company,
|
||||
'enable_provisional_accounting_for_non_stock_items'))
|
||||
|
||||
purchase_receipt_doc_map = {}
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount):
|
||||
account_currency = get_account_currency(item.expense_account)
|
||||
@@ -637,19 +642,23 @@ class PurchaseInvoice(BuyingController):
|
||||
else:
|
||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
||||
|
||||
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
|
||||
|
||||
if auto_accounting_for_non_stock_items:
|
||||
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
provisional_account = self.get_company_default("default_provisional_account")
|
||||
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
|
||||
|
||||
if not purchase_receipt_doc:
|
||||
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
|
||||
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
|
||||
|
||||
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
|
||||
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
|
||||
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
|
||||
'account':service_received_but_not_billed_account}, ['name'])
|
||||
'account':provisional_account}, ['name'])
|
||||
|
||||
if expense_booked_in_pr:
|
||||
expense_account = service_received_but_not_billed_account
|
||||
# Intentionally passing purchase invoice item to handle partial billing
|
||||
purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
gl_entries.append(self.get_gl_dict({
|
||||
|
||||
@@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as create_purchase_invoice_from_receipt,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
get_taxes,
|
||||
make_purchase_receipt,
|
||||
@@ -963,7 +968,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = '2019-03-15'
|
||||
pi.posting_date = '2019-01-10'
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2019-01-10"
|
||||
pi.items[0].service_end_date = "2019-03-15"
|
||||
@@ -1124,8 +1129,6 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
# create a new supplier to test
|
||||
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
|
||||
@@ -1198,6 +1201,45 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
payment_entry.load_from_db()
|
||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||
|
||||
def test_provisional_accounting_entry(self):
|
||||
item = create_item("_Test Non Stock Item", is_stock_item=0)
|
||||
provisional_account = create_account(account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
|
||||
company = frappe.get_doc('Company', '_Test Company')
|
||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||
company.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
|
||||
pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, -1)
|
||||
pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
expected_gle = [
|
||||
['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
|
||||
['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 250, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.save()
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
@@ -1213,7 +1255,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
def update_tax_witholding_category(company, account):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021')
|
||||
fiscal_year = get_fiscal_year(date=nowdate())
|
||||
|
||||
if not frappe.db.get_value('Tax Withholding Rate',
|
||||
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
|
||||
|
||||
@@ -651,7 +651,7 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"permlevel": 0,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -2038,7 +2038,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-21 20:19:38.667508",
|
||||
"modified": "2021-12-23 20:19:38.667508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -45,6 +45,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s
|
||||
from erpnext.stock.doctype.batch.batch import set_batch_nos
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
|
||||
from erpnext.stock.utils import calculate_mapped_packed_items_return
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@@ -745,8 +746,11 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def update_packing_list(self):
|
||||
if cint(self.update_stock) == 1:
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
if cint(self.is_return) and self.return_against:
|
||||
calculate_mapped_packed_items_return(self)
|
||||
else:
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
make_packing_list(self)
|
||||
else:
|
||||
self.set('packed_items', [])
|
||||
|
||||
@@ -879,11 +883,11 @@ class SalesInvoice(SellingController):
|
||||
# Checked both rounding_adjustment and rounded_total
|
||||
# because rounded_total had value even before introcution of posting GLE based on rounded total
|
||||
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
|
||||
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
|
||||
else self.base_grand_total, self.precision("base_grand_total"))
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
# Didnot use base_grand_total to book rounding loss gle
|
||||
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
|
||||
self.precision("grand_total"))
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict({
|
||||
"account": self.debit_to,
|
||||
@@ -891,8 +895,8 @@ class SalesInvoice(SellingController):
|
||||
"party": self.customer,
|
||||
"due_date": self.due_date,
|
||||
"against": self.against_income_account,
|
||||
"debit": grand_total_in_company_currency,
|
||||
"debit_in_account_currency": grand_total_in_company_currency \
|
||||
"debit": base_grand_total,
|
||||
"debit_in_account_currency": base_grand_total \
|
||||
if self.party_account_currency==self.company_currency else grand_total,
|
||||
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
|
||||
@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
|
||||
from erpnext.accounts.utils import PaymentEntryUnlinkError
|
||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||
from erpnext.controllers.accounts_controller import update_invoice_status
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
from erpnext.regional.india.utils import get_ewb_data
|
||||
@@ -1800,47 +1801,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
||||
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
item.no_of_months = 12
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2019-01-10"
|
||||
si.items[0].service_end_date = "2019-03-15"
|
||||
si.items[0].deferred_revenue_account = deferred_account
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
|
||||
|
||||
pda1 = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company"
|
||||
))
|
||||
|
||||
pda1.insert()
|
||||
self.assertRaises(frappe.ValidationError, pda1.submit)
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
||||
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
@@ -2217,9 +2177,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48],
|
||||
["2021-06-30", 20000.0, 21311.48],
|
||||
["2021-09-30", 5041.1, 26352.58]
|
||||
["2020-06-30", 1366.12, 1366.12],
|
||||
["2021-06-30", 20000.0, 21366.12],
|
||||
["2021-09-30", 5041.1, 26407.22]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@@ -2267,12 +2227,12 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48, True],
|
||||
["2021-06-30", 20000.0, 21311.48, True],
|
||||
["2022-06-30", 20000.0, 41311.48, False],
|
||||
["2023-06-30", 20000.0, 61311.48, False],
|
||||
["2024-06-30", 20000.0, 81311.48, False],
|
||||
["2025-06-06", 18688.52, 100000.0, False]
|
||||
["2020-06-30", 1366.12, 1366.12, True],
|
||||
["2021-06-30", 20000.0, 21366.12, True],
|
||||
["2022-06-30", 20000.0, 41366.12, False],
|
||||
["2023-06-30", 20000.0, 61366.12, False],
|
||||
["2024-06-30", 20000.0, 81366.12, False],
|
||||
["2025-06-06", 18633.88, 100000.0, False]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@@ -2370,6 +2330,41 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
|
||||
def test_update_invoice_status(self):
|
||||
today = nowdate()
|
||||
|
||||
# Sales Invoice without Payment Schedule
|
||||
si = create_sales_invoice(posting_date=add_days(today, -5))
|
||||
|
||||
# Sales Invoice with Payment Schedule
|
||||
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
|
||||
si_with_payment_schedule.extend("payment_schedule", [
|
||||
{
|
||||
"due_date": add_days(today, -5),
|
||||
"invoice_portion": 50,
|
||||
"payment_amount": si_with_payment_schedule.grand_total / 2
|
||||
},
|
||||
{
|
||||
"due_date": add_days(today, 5),
|
||||
"invoice_portion": 50,
|
||||
"payment_amount": si_with_payment_schedule.grand_total / 2
|
||||
}
|
||||
])
|
||||
si_with_payment_schedule.submit()
|
||||
|
||||
|
||||
for invoice in (si, si_with_payment_schedule):
|
||||
invoice.db_set("status", "Unpaid")
|
||||
update_invoice_status()
|
||||
invoice.reload()
|
||||
self.assertEqual(invoice.status, "Overdue")
|
||||
|
||||
invoice.db_set("status", "Unpaid and Discounted")
|
||||
update_invoice_status()
|
||||
invoice.reload()
|
||||
self.assertEqual(invoice.status, "Overdue and Discounted")
|
||||
|
||||
|
||||
def test_sales_commission(self):
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
item = copy.deepcopy(si.get('items')[0])
|
||||
@@ -2431,6 +2426,74 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
|
||||
|
||||
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
|
||||
acc_settings = frappe.get_single('Accounts Settings')
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 1
|
||||
acc_settings.submit_journal_entries = 1
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
|
||||
item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
|
||||
|
||||
si.set_posting_time = 1
|
||||
si.posting_date = '2019-01-01'
|
||||
si.debit_to = '_Test Receivable USD - _TC'
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2019-01-01"
|
||||
si.items[0].service_end_date = "2019-03-30"
|
||||
si.items[0].deferred_expense_account = deferred_account
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
|
||||
|
||||
pda1 = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company"
|
||||
))
|
||||
|
||||
pda1.insert()
|
||||
pda1.submit()
|
||||
|
||||
expected_gle = [
|
||||
["Sales - _TC", 0.0, 2089.89, "2019-01-28"],
|
||||
[deferred_account, 2089.89, 0.0, "2019-01-28"],
|
||||
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
|
||||
[deferred_account, 1887.64, 0.0, "2019-02-28"],
|
||||
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
|
||||
[deferred_account, 2022.47, 0.0, "2019-03-15"]
|
||||
]
|
||||
|
||||
gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||
from `tabGL Entry`
|
||||
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
|
||||
order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[i][0], gle.account)
|
||||
self.assertEqual(expected_gle[i][1], gle.credit)
|
||||
self.assertEqual(expected_gle[i][2], gle.debit)
|
||||
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
acc_settings = frappe.get_single('Accounts Settings')
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 0
|
||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
|
||||
@@ -71,7 +71,8 @@ class ShippingRule(Document):
|
||||
if doc.currency != doc.company_currency:
|
||||
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
|
||||
|
||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||
if shipping_amount:
|
||||
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
|
||||
|
||||
def get_shipping_amount_from_rules(self, value):
|
||||
for condition in self.get("conditions"):
|
||||
|
||||
@@ -24,6 +24,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
|
||||
|
||||
class Subscription(Document):
|
||||
@@ -356,7 +357,10 @@ class Subscription(Document):
|
||||
if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
|
||||
invoice.apply_tds = 1
|
||||
|
||||
## Add dimensions in invoice for subscription:
|
||||
# Add party currency to invoice
|
||||
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
|
||||
|
||||
# Add dimensions in invoice for subscription:
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
|
||||
@@ -60,15 +60,38 @@ def create_plan():
|
||||
plan.billing_interval_count = 3
|
||||
plan.insert()
|
||||
|
||||
if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'):
|
||||
plan = frappe.new_doc('Subscription Plan')
|
||||
plan.plan_name = '_Test Plan Multicurrency'
|
||||
plan.item = '_Test Non Stock Item'
|
||||
plan.price_determination = "Fixed Rate"
|
||||
plan.cost = 50
|
||||
plan.currency = 'USD'
|
||||
plan.billing_interval = 'Month'
|
||||
plan.billing_interval_count = 1
|
||||
plan.insert()
|
||||
|
||||
def create_parties():
|
||||
if not frappe.db.exists('Supplier', '_Test Supplier'):
|
||||
supplier = frappe.new_doc('Supplier')
|
||||
supplier.supplier_name = '_Test Supplier'
|
||||
supplier.supplier_group = 'All Supplier Groups'
|
||||
supplier.insert()
|
||||
|
||||
if not frappe.db.exists('Customer', '_Test Subscription Customer'):
|
||||
customer = frappe.new_doc('Customer')
|
||||
customer.customer_name = '_Test Subscription Customer'
|
||||
customer.billing_currency = 'USD'
|
||||
customer.append('accounts', {
|
||||
'company': '_Test Company',
|
||||
'account': '_Test Receivable USD - _TC'
|
||||
})
|
||||
customer.insert()
|
||||
|
||||
class TestSubscription(unittest.TestCase):
|
||||
def setUp(self):
|
||||
create_plan()
|
||||
create_parties()
|
||||
|
||||
def test_create_subscription_with_trial_with_correct_period(self):
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
@@ -637,3 +660,22 @@ class TestSubscription(unittest.TestCase):
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
def test_multicurrency_subscription(self):
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
subscription.party_type = 'Customer'
|
||||
subscription.party = '_Test Subscription Customer'
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
subscription.company = '_Test Company'
|
||||
# select subscription start date as '2018-01-15'
|
||||
subscription.start_date = '2018-01-01'
|
||||
subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1})
|
||||
subscription.save()
|
||||
|
||||
subscription.process()
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.status, 'Unpaid')
|
||||
|
||||
# Check the currency of the created invoice
|
||||
currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency')
|
||||
self.assertEqual(currency, 'USD')
|
||||
@@ -75,7 +75,8 @@
|
||||
"fieldname": "cost",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Cost"
|
||||
"label": "Cost",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
|
||||
@@ -147,7 +148,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-08-13 10:53:44.205774",
|
||||
"modified": "2021-12-10 15:24:15.794477",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription Plan",
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "single_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Single Transaction Threshold"
|
||||
},
|
||||
{
|
||||
"columns": 3,
|
||||
"fieldname": "cumulative_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Cumulative Transaction Threshold"
|
||||
},
|
||||
@@ -59,7 +59,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-31 11:42:12.213977",
|
||||
"modified": "2022-01-13 12:04:42.904263",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Rate",
|
||||
@@ -68,5 +68,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -59,7 +59,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
|
||||
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
|
||||
|
||||
party = frappe.get_doc(party_type, party)
|
||||
currency = party.default_currency if party.get("default_currency") else get_company_currency(company)
|
||||
currency = party.get("default_currency") or currency or get_company_currency(company)
|
||||
|
||||
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
|
||||
set_contact_details(party_details, party, party_type)
|
||||
|
||||
@@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
"label": __("Show Future Payments"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname":"show_gl_balance",
|
||||
"label": __("Show GL Balance"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
],
|
||||
|
||||
onload: function(report) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, flt
|
||||
from six import iteritems
|
||||
|
||||
from erpnext.accounts.party import get_partywise_advanced_payment_amount
|
||||
@@ -37,6 +37,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
|
||||
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {}
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date)
|
||||
|
||||
for party, party_dict in iteritems(self.party_total):
|
||||
if party_dict.outstanding == 0:
|
||||
continue
|
||||
@@ -56,6 +59,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
# but in summary report advance shown in separate column
|
||||
row.paid -= row.advance
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
row.gl_balance = gl_balance_map.get(party)
|
||||
row.diff = flt(row.outstanding) - flt(row.gl_balance)
|
||||
|
||||
self.data.append(row)
|
||||
|
||||
def get_party_total(self, args):
|
||||
@@ -115,6 +122,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(_(credit_debit_label), fieldname='credit_note')
|
||||
self.add_column(_('Outstanding Amount'), fieldname='outstanding')
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
self.add_column(_('GL Balance'), fieldname='gl_balance')
|
||||
self.add_column(_('Difference'), fieldname='diff')
|
||||
|
||||
self.setup_ageing_columns()
|
||||
|
||||
if self.party_type == "Customer":
|
||||
@@ -141,3 +152,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
# Add column for total due amount
|
||||
self.add_column(label="Total Amount Due", fieldname='total_due')
|
||||
|
||||
def get_gl_balance(report_date):
|
||||
return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'],
|
||||
filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1))
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
"label": __("Filter Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": ["Fiscal Year"],
|
||||
"reqd": 1,
|
||||
on_change: function() {
|
||||
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
|
||||
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
|
||||
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
|
||||
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
|
||||
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
|
||||
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"from_fiscal_year",
|
||||
"label": __("Start Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"to_fiscal_year",
|
||||
"label": __("End Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "periodicity",
|
||||
"label": __("Periodicity"),
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
{ "value": "Monthly", "label": __("Monthly") },
|
||||
{ "value": "Quarterly", "label": __("Quarterly") },
|
||||
{ "value": "Half-Yearly", "label": __("Half-Yearly") },
|
||||
{ "value": "Yearly", "label": __("Yearly") }
|
||||
],
|
||||
"default": "Monthly",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "type",
|
||||
"label": __("Invoice Type"),
|
||||
"fieldtype": "Select",
|
||||
"options": [
|
||||
{ "value": "Revenue", "label": __("Revenue") },
|
||||
{ "value": "Expense", "label": __("Expense") }
|
||||
],
|
||||
"default": "Revenue",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname" : "with_upcoming_postings",
|
||||
"label": __("Show with upcoming revenue/expense"),
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
}
|
||||
]
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["Deferred Revenue and Expense"] = {
|
||||
"filters": get_filters(),
|
||||
"formatter": function(value, row, column, data, default_formatter){
|
||||
return default_formatter(value, row, column, data);
|
||||
},
|
||||
onload: function(report){
|
||||
let fiscal_year = frappe.defaults.get_user_default("fiscal_year");
|
||||
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-12-10 19:27:14.654220",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-12-10 19:27:14.654220",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Deferred Revenue and Expense",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Deferred Revenue and Expense",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Column, functions
|
||||
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded
|
||||
|
||||
from erpnext.accounts.report.financial_statements import get_period_list
|
||||
|
||||
|
||||
class Deferred_Item(object):
|
||||
"""
|
||||
Helper class for processing items with deferred revenue/expense
|
||||
"""
|
||||
|
||||
def __init__(self, item, inv, gle_entries):
|
||||
self.name = item
|
||||
self.parent = inv.name
|
||||
self.item_name = gle_entries[0].item_name
|
||||
self.service_start_date = gle_entries[0].service_start_date
|
||||
self.service_end_date = gle_entries[0].service_end_date
|
||||
self.base_net_amount = gle_entries[0].base_net_amount
|
||||
self.filters = inv.filters
|
||||
self.period_list = inv.period_list
|
||||
|
||||
if gle_entries[0].deferred_revenue_account:
|
||||
self.type = "Deferred Sale Item"
|
||||
self.deferred_account = gle_entries[0].deferred_revenue_account
|
||||
elif gle_entries[0].deferred_expense_account:
|
||||
self.type = "Deferred Purchase Item"
|
||||
self.deferred_account = gle_entries[0].deferred_expense_account
|
||||
|
||||
self.gle_entries = []
|
||||
# holds period wise total for item
|
||||
self.period_total = []
|
||||
self.last_entry_date = self.service_start_date
|
||||
|
||||
if gle_entries:
|
||||
self.gle_entries = gle_entries
|
||||
for x in self.gle_entries:
|
||||
if self.get_amount(x):
|
||||
self.last_entry_date = x.gle_posting_date
|
||||
|
||||
def report_data(self):
|
||||
"""
|
||||
Generate report data for output
|
||||
"""
|
||||
ret_data = frappe._dict({"name": self.item_name})
|
||||
for period in self.period_total:
|
||||
ret_data[period.key] = period.total
|
||||
ret_data.indent = 1
|
||||
return ret_data
|
||||
|
||||
def get_amount(self, entry):
|
||||
"""
|
||||
For a given GL/Journal posting, get balance based on item type
|
||||
"""
|
||||
if self.type == "Deferred Sale Item":
|
||||
return entry.debit - entry.credit
|
||||
elif self.type == "Deferred Purchase Item":
|
||||
return -(entry.credit - entry.debit)
|
||||
return 0
|
||||
|
||||
def get_item_total(self):
|
||||
"""
|
||||
Helper method - calculate booked amount. Includes simulated postings as well
|
||||
"""
|
||||
total = 0
|
||||
for gle_posting in self.gle_entries:
|
||||
total += self.get_amount(gle_posting)
|
||||
|
||||
return total
|
||||
|
||||
def calculate_amount(self, start_date, end_date):
|
||||
"""
|
||||
start_date, end_date - datetime.datetime.date
|
||||
return - estimated amount to post for given period
|
||||
Calculated based on already booked amount and item service period
|
||||
"""
|
||||
total_months = (
|
||||
(self.service_end_date.year - self.service_start_date.year) * 12
|
||||
+ (self.service_end_date.month - self.service_start_date.month)
|
||||
+ 1
|
||||
)
|
||||
|
||||
prorate = date_diff(self.service_end_date, self.service_start_date) / date_diff(
|
||||
get_last_day(self.service_end_date), get_first_day(self.service_start_date)
|
||||
)
|
||||
|
||||
actual_months = rounded(total_months * prorate, 1)
|
||||
|
||||
already_booked_amount = self.get_item_total()
|
||||
base_amount = self.base_net_amount / actual_months
|
||||
|
||||
if base_amount + already_booked_amount > self.base_net_amount:
|
||||
base_amount = self.base_net_amount - already_booked_amount
|
||||
|
||||
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
|
||||
partial_month = flt(date_diff(end_date, start_date)) / flt(
|
||||
date_diff(get_last_day(end_date), get_first_day(start_date))
|
||||
)
|
||||
base_amount *= rounded(partial_month, 1)
|
||||
|
||||
return base_amount
|
||||
|
||||
def make_dummy_gle(self, name, date, amount):
|
||||
"""
|
||||
return - frappe._dict() of a dummy gle entry
|
||||
"""
|
||||
entry = frappe._dict(
|
||||
{"name": name, "gle_posting_date": date, "debit": 0, "credit": 0, "posted": "not"}
|
||||
)
|
||||
if self.type == "Deferred Sale Item":
|
||||
entry.debit = amount
|
||||
elif self.type == "Deferred Purchase Item":
|
||||
entry.credit = amount
|
||||
return entry
|
||||
|
||||
def simulate_future_posting(self):
|
||||
"""
|
||||
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
||||
"""
|
||||
if self.service_start_date != self.service_end_date:
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
|
||||
def calculate_item_revenue_expense_for_period(self):
|
||||
"""
|
||||
calculate item postings for each period and update period_total list
|
||||
"""
|
||||
for period in self.period_list:
|
||||
period_sum = 0
|
||||
actual = 0
|
||||
for posting in self.gle_entries:
|
||||
# if period.from_date <= posting.posting_date <= period.to_date:
|
||||
if period.from_date <= posting.gle_posting_date <= period.to_date:
|
||||
period_sum += self.get_amount(posting)
|
||||
if posting.posted == "posted":
|
||||
actual += self.get_amount(posting)
|
||||
|
||||
self.period_total.append(
|
||||
frappe._dict({"key": period.key, "total": period_sum, "actual": actual})
|
||||
)
|
||||
return self.period_total
|
||||
|
||||
|
||||
class Deferred_Invoice(object):
|
||||
def __init__(self, invoice, items, filters, period_list):
|
||||
"""
|
||||
Helper class for processing invoices with deferred revenue/expense items
|
||||
invoice - string : invoice name
|
||||
items - list : frappe._dict() with item details. Refer Deferred_Item for required fields
|
||||
"""
|
||||
self.name = invoice
|
||||
self.posting_date = items[0].posting_date
|
||||
self.filters = filters
|
||||
self.period_list = period_list
|
||||
# holds period wise total for invoice
|
||||
self.period_total = []
|
||||
|
||||
if items[0].deferred_revenue_account:
|
||||
self.type = "Sales"
|
||||
elif items[0].deferred_expense_account:
|
||||
self.type = "Purchase"
|
||||
|
||||
self.items = []
|
||||
# for each uniq items
|
||||
self.uniq_items = set([x.item for x in items])
|
||||
for item in self.uniq_items:
|
||||
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
|
||||
|
||||
def calculate_invoice_revenue_expense_for_period(self):
|
||||
"""
|
||||
calculate deferred revenue/expense for all items in invoice
|
||||
"""
|
||||
# initialize period_total list for invoice
|
||||
for period in self.period_list:
|
||||
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
|
||||
|
||||
for item in self.items:
|
||||
item_total = item.calculate_item_revenue_expense_for_period()
|
||||
# update invoice total
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
self.period_total[idx].total += item_total[idx].total
|
||||
self.period_total[idx].actual += item_total[idx].actual
|
||||
return self.period_total
|
||||
|
||||
def estimate_future(self):
|
||||
"""
|
||||
create dummy GL entries for upcoming months for all items in invoice
|
||||
"""
|
||||
[item.simulate_future_posting() for item in self.items]
|
||||
|
||||
def report_data(self):
|
||||
"""
|
||||
generate report data for invoice, includes invoice total
|
||||
"""
|
||||
ret_data = []
|
||||
inv_total = frappe._dict({"name": self.name})
|
||||
for x in self.period_total:
|
||||
inv_total[x.key] = x.total
|
||||
inv_total.indent = 0
|
||||
ret_data.append(inv_total)
|
||||
list(map(lambda item: ret_data.append(item.report_data()), self.items))
|
||||
return ret_data
|
||||
|
||||
|
||||
class Deferred_Revenue_and_Expense_Report(object):
|
||||
def __init__(self, filters=None):
|
||||
"""
|
||||
Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided
|
||||
"""
|
||||
|
||||
# If no filters are provided, get user defaults
|
||||
if not filters:
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"filter_based_on": "Fiscal Year",
|
||||
"period_start_date": fiscal_year.year_start_date,
|
||||
"period_end_date": fiscal_year.year_end_date,
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Revenue",
|
||||
"with_upcoming_postings": True,
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.filters = frappe._dict(filters)
|
||||
|
||||
self.period_list = None
|
||||
self.deferred_invoices = []
|
||||
# holds period wise total for report
|
||||
self.period_total = []
|
||||
|
||||
def get_period_list(self):
|
||||
"""
|
||||
Figure out selected period based on filters
|
||||
"""
|
||||
self.period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
self.filters.period_start_date,
|
||||
self.filters.period_end_date,
|
||||
self.filters.filter_based_on,
|
||||
self.filters.periodicity,
|
||||
company=self.filters.company,
|
||||
)
|
||||
|
||||
def get_invoices(self):
|
||||
"""
|
||||
Get all sales and purchase invoices which has deferred revenue/expense items
|
||||
"""
|
||||
gle = qb.DocType("GL Entry")
|
||||
# column doesn't have an alias option
|
||||
posted = Column("posted")
|
||||
|
||||
if self.filters.type == "Revenue":
|
||||
inv = qb.DocType("Sales Invoice")
|
||||
inv_item = qb.DocType("Sales Invoice Item")
|
||||
deferred_flag_field = inv_item["enable_deferred_revenue"]
|
||||
deferred_account_field = inv_item["deferred_revenue_account"]
|
||||
|
||||
elif self.filters.type == "Expense":
|
||||
inv = qb.DocType("Purchase Invoice")
|
||||
inv_item = qb.DocType("Purchase Invoice Item")
|
||||
deferred_flag_field = inv_item["enable_deferred_expense"]
|
||||
deferred_account_field = inv_item["deferred_expense_account"]
|
||||
|
||||
query = (
|
||||
qb.from_(inv_item)
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
inv.posting_date,
|
||||
inv_item.name.as_("item"),
|
||||
inv_item.item_name,
|
||||
inv_item.service_start_date,
|
||||
inv_item.service_end_date,
|
||||
inv_item.base_net_amount,
|
||||
deferred_account_field,
|
||||
gle.posting_date.as_("gle_posting_date"),
|
||||
functions.Sum(gle.debit).as_("debit"),
|
||||
functions.Sum(gle.credit).as_("credit"),
|
||||
posted,
|
||||
)
|
||||
.where(
|
||||
(inv.docstatus == 1)
|
||||
& (deferred_flag_field == 1)
|
||||
& (
|
||||
(
|
||||
(self.period_list[0].from_date >= inv_item.service_start_date)
|
||||
& (inv_item.service_end_date >= self.period_list[0].from_date)
|
||||
)
|
||||
| (
|
||||
(inv_item.service_start_date >= self.period_list[0].from_date)
|
||||
& (inv_item.service_start_date <= self.period_list[-1].to_date)
|
||||
)
|
||||
)
|
||||
)
|
||||
.groupby(inv.name, inv_item.name, gle.posting_date)
|
||||
.orderby(gle.posting_date)
|
||||
)
|
||||
self.invoices = query.run(as_dict=True)
|
||||
|
||||
uniq_invoice = set([x.doc for x in self.invoices])
|
||||
for inv in uniq_invoice:
|
||||
self.deferred_invoices.append(
|
||||
Deferred_Invoice(
|
||||
inv, [x for x in self.invoices if x.doc == inv], self.filters, self.period_list
|
||||
)
|
||||
)
|
||||
|
||||
def estimate_future(self):
|
||||
"""
|
||||
For all Invoices estimate upcoming postings
|
||||
"""
|
||||
for x in self.deferred_invoices:
|
||||
x.estimate_future()
|
||||
|
||||
def calculate_revenue_and_expense(self):
|
||||
"""
|
||||
calculate the deferred revenue/expense for all invoices
|
||||
"""
|
||||
# initialize period_total list for report
|
||||
for period in self.period_list:
|
||||
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
|
||||
|
||||
for inv in self.deferred_invoices:
|
||||
inv_total = inv.calculate_invoice_revenue_expense_for_period()
|
||||
# calculate total for whole report
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
self.period_total[idx].total += inv_total[idx].total
|
||||
self.period_total[idx].actual += inv_total[idx].actual
|
||||
|
||||
def get_columns(self):
|
||||
columns = []
|
||||
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
|
||||
for period in self.period_list:
|
||||
columns.append(
|
||||
{
|
||||
"label": _(period.label),
|
||||
"fieldname": period.key,
|
||||
"fieldtype": "Currency",
|
||||
"read_only": 1,
|
||||
})
|
||||
return columns
|
||||
|
||||
def generate_report_data(self):
|
||||
"""
|
||||
Generate report data for all invoices. Adds total rows for revenue and expense
|
||||
"""
|
||||
ret = []
|
||||
|
||||
for inv in self.deferred_invoices:
|
||||
ret += inv.report_data()
|
||||
|
||||
# empty row for padding
|
||||
ret += [{}]
|
||||
|
||||
# add total row
|
||||
if ret is not []:
|
||||
if self.filters.type == "Revenue":
|
||||
total_row = frappe._dict({"name": "Total Deferred Income"})
|
||||
elif self.filters.type == "Expense":
|
||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
total_row[period.key] = self.period_total[idx].total
|
||||
ret.append(total_row)
|
||||
|
||||
return ret
|
||||
|
||||
def prepare_chart(self):
|
||||
chart = {
|
||||
"data": {
|
||||
"labels": [period.label for period in self.period_list],
|
||||
"datasets": [
|
||||
{
|
||||
"name": "Actual Posting",
|
||||
"chartType": "bar",
|
||||
"values": [x.actual for x in self.period_total],
|
||||
}
|
||||
],
|
||||
},
|
||||
"type": "axis-mixed",
|
||||
"height": 500,
|
||||
"axisOptions": {"xAxisMode": "Tick", "xIsSeries": True},
|
||||
"barOptions": {"stacked": False, "spaceRatio": 0.5},
|
||||
}
|
||||
|
||||
if self.filters.with_upcoming_postings:
|
||||
chart["data"]["datasets"].append({
|
||||
"name": "Expected",
|
||||
"chartType": "line",
|
||||
"values": [x.total for x in self.period_total]
|
||||
})
|
||||
|
||||
return chart
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
"""
|
||||
Run report and generate data
|
||||
"""
|
||||
self.deferred_invoices.clear()
|
||||
self.get_period_list()
|
||||
self.get_invoices()
|
||||
|
||||
if self.filters.with_upcoming_postings:
|
||||
self.estimate_future()
|
||||
self.calculate_revenue_and_expense()
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=filters)
|
||||
report.run()
|
||||
|
||||
columns = report.get_columns()
|
||||
data = report.generate_report_data()
|
||||
message = []
|
||||
chart = report.prepare_chart()
|
||||
|
||||
return columns, data, message, chart
|
||||
@@ -0,0 +1,345 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
|
||||
Deferred_Revenue_and_Expense_Report,
|
||||
)
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
clear_accounts_and_items()
|
||||
create_company()
|
||||
self.maxDiff = None
|
||||
|
||||
def clear_old_entries(self):
|
||||
sinv = qb.DocType("Sales Invoice")
|
||||
sinv_item = qb.DocType("Sales Invoice Item")
|
||||
pinv = qb.DocType("Purchase Invoice")
|
||||
pinv_item = qb.DocType("Purchase Invoice Item")
|
||||
|
||||
# delete existing invoices with deferred items
|
||||
deferred_invoices = (
|
||||
qb.from_(sinv)
|
||||
.join(sinv_item)
|
||||
.on(sinv.name == sinv_item.parent)
|
||||
.select(sinv.name)
|
||||
.where(sinv_item.enable_deferred_revenue == 1)
|
||||
.run()
|
||||
)
|
||||
if deferred_invoices:
|
||||
qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
|
||||
|
||||
deferred_invoices = (
|
||||
qb.from_(pinv)
|
||||
.join(pinv_item)
|
||||
.on(pinv.name == pinv_item.parent)
|
||||
.select(pinv.name)
|
||||
.where(pinv_item.enable_deferred_expense == 1)
|
||||
.run()
|
||||
)
|
||||
if deferred_invoices:
|
||||
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
|
||||
|
||||
def test_deferred_revenue(self):
|
||||
self.clear_old_entries()
|
||||
|
||||
# created deferred expense accounts, if not found
|
||||
deferred_revenue_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Customer DR"
|
||||
customer.type = "Individual"
|
||||
customer.insert()
|
||||
|
||||
item = create_item(
|
||||
"_Test Internet Subscription",
|
||||
is_stock_item=0,
|
||||
warehouse="All Warehouses - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_revenue_account
|
||||
item.no_of_months = 3
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item.name,
|
||||
company="_Test Company DR",
|
||||
customer="_Test Customer DR",
|
||||
debit_to="Debtors - _CD",
|
||||
posting_date="2021-05-01",
|
||||
parent_cost_center="Main - _CD",
|
||||
cost_center="Main - _CD",
|
||||
do_not_submit=True,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
)
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2021-05-01"
|
||||
si.items[0].service_end_date = "2021-08-01"
|
||||
si.items[0].deferred_revenue_account = deferred_revenue_account
|
||||
si.items[0].income_account = "Sales - _CD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2021-05-01",
|
||||
end_date="2021-08-01",
|
||||
type="Income",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2021-05-01",
|
||||
"period_end_date": "2021-08-01",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Revenue",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
expected = [
|
||||
{"key": "may_2021", "total": 100.0, "actual": 100.0},
|
||||
{"key": "jun_2021", "total": 100.0, "actual": 100.0},
|
||||
{"key": "jul_2021", "total": 100.0, "actual": 100.0},
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
def test_deferred_expense(self):
|
||||
self.clear_old_entries()
|
||||
|
||||
# created deferred expense accounts, if not found
|
||||
deferred_expense_account = create_account(
|
||||
account_name="Deferred Expense",
|
||||
parent_account="Current Assets - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
supplier = create_supplier(
|
||||
supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
|
||||
)
|
||||
supplier.save()
|
||||
|
||||
item = create_item(
|
||||
"_Test Office Desk",
|
||||
is_stock_item=0,
|
||||
warehouse="All Warehouses - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_expense_account = deferred_expense_account
|
||||
item.no_of_months_exp = 3
|
||||
item.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=item.name,
|
||||
company="_Test Company DR",
|
||||
supplier="_Test Furniture Supplier",
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
posting_date=frappe.utils.datetime.date(2021, 5, 1),
|
||||
parent_cost_center="Main - _CD",
|
||||
cost_center="Main - _CD",
|
||||
do_not_save=True,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
warehouse="All Warehouses - _CD",
|
||||
qty=1,
|
||||
)
|
||||
pi.set_posting_time = True
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2021-05-01"
|
||||
pi.items[0].service_end_date = "2021-08-01"
|
||||
pi.items[0].deferred_expense_account = deferred_expense_account
|
||||
pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2021-05-01",
|
||||
end_date="2021-08-01",
|
||||
type="Expense",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2021-05-01",
|
||||
"period_end_date": "2021-08-01",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Expense",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
expected = [
|
||||
{"key": "may_2021", "total": -100.0, "actual": -100.0},
|
||||
{"key": "jun_2021", "total": -100.0, "actual": -100.0},
|
||||
{"key": "jul_2021", "total": -100.0, "actual": -100.0},
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
def test_zero_months(self):
|
||||
self.clear_old_entries()
|
||||
# created deferred expense accounts, if not found
|
||||
deferred_revenue_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Customer DR"
|
||||
customer.type = "Individual"
|
||||
customer.insert()
|
||||
|
||||
item = create_item(
|
||||
"_Test Internet Subscription",
|
||||
is_stock_item=0,
|
||||
warehouse="All Warehouses - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_revenue_account
|
||||
item.no_of_months = 0
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item.name,
|
||||
company="_Test Company DR",
|
||||
customer="_Test Customer DR",
|
||||
debit_to="Debtors - _CD",
|
||||
posting_date="2021-05-01",
|
||||
parent_cost_center="Main - _CD",
|
||||
cost_center="Main - _CD",
|
||||
do_not_submit=True,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
)
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].deferred_revenue_account = deferred_revenue_account
|
||||
si.items[0].income_account = "Sales - _CD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pda = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date=nowdate(),
|
||||
start_date="2021-05-01",
|
||||
end_date="2021-08-01",
|
||||
type="Income",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
)
|
||||
pda.insert()
|
||||
pda.submit()
|
||||
|
||||
# execute report
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2021-05-01",
|
||||
"period_end_date": "2021-08-01",
|
||||
"from_fiscal_year": fiscal_year.year,
|
||||
"to_fiscal_year": fiscal_year.year,
|
||||
"periodicity": "Monthly",
|
||||
"type": "Revenue",
|
||||
"with_upcoming_postings": False,
|
||||
}
|
||||
)
|
||||
|
||||
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||
report.run()
|
||||
expected = [
|
||||
{"key": "may_2021", "total": 300.0, "actual": 300.0},
|
||||
{"key": "jun_2021", "total": 0, "actual": 0},
|
||||
{"key": "jul_2021", "total": 0, "actual": 0},
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
def create_company():
|
||||
company = frappe.db.exists("Company", "_Test Company DR")
|
||||
if not company:
|
||||
company = frappe.new_doc("Company")
|
||||
company.company_name = "_Test Company DR"
|
||||
company.default_currency = "INR"
|
||||
company.chart_of_accounts = "Standard"
|
||||
company.insert()
|
||||
|
||||
|
||||
def clear_accounts_and_items():
|
||||
item = qb.DocType("Item")
|
||||
account = qb.DocType("Account")
|
||||
customer = qb.DocType("Customer")
|
||||
supplier = qb.DocType("Supplier")
|
||||
|
||||
qb.from_(account).delete().where(
|
||||
(account.account_name == "Deferred Revenue")
|
||||
| (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
|
||||
).run()
|
||||
qb.from_(item).delete().where(
|
||||
(item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
|
||||
).run()
|
||||
qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
|
||||
qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
|
||||
@@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname": "include_dimensions",
|
||||
"label": __("Consider Accounting Dimensions"),
|
||||
"fieldtype": "Check",
|
||||
"default": 0
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_opening_entries",
|
||||
|
||||
@@ -449,9 +449,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
|
||||
elif group_by_voucher_consolidated:
|
||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
if filters.get("include_dimensions"):
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
consolidated_gle.setdefault(key, gle)
|
||||
@@ -595,14 +597,14 @@ def get_columns(filters):
|
||||
"fieldname": dim.fieldname,
|
||||
"width": 100
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
columns.append({
|
||||
"label": _("Cost Center"),
|
||||
"options": "Cost Center",
|
||||
"fieldname": "cost_center",
|
||||
"width": 100
|
||||
},
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
"label": _("Against Voucher Type"),
|
||||
"fieldname": "against_voucher_type",
|
||||
|
||||
@@ -186,83 +186,85 @@ class Asset(AccountsController):
|
||||
if not self.available_for_use_date:
|
||||
return
|
||||
|
||||
for d in self.get('finance_books'):
|
||||
self.validate_asset_finance_books(d)
|
||||
start = self.clear_depreciation_schedule()
|
||||
|
||||
start = self.clear_depreciation_schedule()
|
||||
for finance_book in self.get('finance_books'):
|
||||
self.validate_asset_finance_books(finance_book)
|
||||
|
||||
# value_after_depreciation - current Asset value
|
||||
if self.docstatus == 1 and d.value_after_depreciation:
|
||||
value_after_depreciation = flt(d.value_after_depreciation)
|
||||
if self.docstatus == 1 and finance_book.value_after_depreciation:
|
||||
value_after_depreciation = flt(finance_book.value_after_depreciation)
|
||||
else:
|
||||
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||
flt(self.opening_accumulated_depreciation))
|
||||
|
||||
d.value_after_depreciation = value_after_depreciation
|
||||
finance_book.value_after_depreciation = value_after_depreciation
|
||||
|
||||
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
|
||||
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
|
||||
cint(self.number_of_depreciations_booked)
|
||||
|
||||
has_pro_rata = self.check_is_pro_rata(d)
|
||||
has_pro_rata = self.check_is_pro_rata(finance_book)
|
||||
|
||||
if has_pro_rata:
|
||||
number_of_pending_depreciations += 1
|
||||
|
||||
skip_row = False
|
||||
for n in range(start, number_of_pending_depreciations):
|
||||
|
||||
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row: continue
|
||||
|
||||
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
|
||||
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
|
||||
|
||||
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
|
||||
schedule_date = add_months(d.depreciation_start_date,
|
||||
n * cint(d.frequency_of_depreciation))
|
||||
schedule_date = add_months(finance_book.depreciation_start_date,
|
||||
n * cint(finance_book.frequency_of_depreciation))
|
||||
|
||||
# schedule date will be a year later from start date
|
||||
# so monthly schedule date is calculated by removing 11 months from it
|
||||
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
|
||||
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_sale:
|
||||
from_date = self.get_from_date(d.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||
from_date, date_of_sale)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self.append("schedules", {
|
||||
"schedule_date": date_of_sale,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
# For first row
|
||||
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
self.available_for_use_date, d.depreciation_start_date)
|
||||
from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||
from_date, finance_book.depreciation_start_date)
|
||||
|
||||
# For first depr schedule date will be the start date
|
||||
# so monthly schedule date is calculated by removing month difference between use date and start date
|
||||
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
|
||||
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
|
||||
|
||||
# For last row
|
||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||
if not self.flags.increase_in_asset_life:
|
||||
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
|
||||
self.to_date = add_months(self.available_for_use_date,
|
||||
(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
|
||||
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d,
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
|
||||
depreciation_amount, schedule_date, self.to_date)
|
||||
|
||||
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
|
||||
depreciation_amount, d.finance_book)
|
||||
depreciation_amount, finance_book.finance_book)
|
||||
|
||||
monthly_schedule_date = add_months(schedule_date, 1)
|
||||
schedule_date = add_days(schedule_date, days)
|
||||
@@ -273,10 +275,10 @@ class Asset(AccountsController):
|
||||
self.precision("gross_purchase_amount"))
|
||||
|
||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||
and value_after_depreciation != d.expected_value_after_useful_life)
|
||||
or value_after_depreciation < d.expected_value_after_useful_life):
|
||||
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
|
||||
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||
and value_after_depreciation != finance_book.expected_value_after_useful_life)
|
||||
or value_after_depreciation < finance_book.expected_value_after_useful_life):
|
||||
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
|
||||
skip_row = True
|
||||
|
||||
if depreciation_amount > 0:
|
||||
@@ -286,7 +288,7 @@ class Asset(AccountsController):
|
||||
# In pro rata case, for first and last depreciation, month range would be different
|
||||
month_range = months \
|
||||
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
|
||||
else d.frequency_of_depreciation
|
||||
else finance_book.frequency_of_depreciation
|
||||
|
||||
for r in range(month_range):
|
||||
if (has_pro_rata and n == 0):
|
||||
@@ -312,27 +314,52 @@ class Asset(AccountsController):
|
||||
self.append("schedules", {
|
||||
"schedule_date": date,
|
||||
"depreciation_amount": amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
else:
|
||||
self.append("schedules", {
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"finance_book": d.finance_book,
|
||||
"finance_book_id": d.idx
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx
|
||||
})
|
||||
|
||||
# used when depreciation schedule needs to be modified due to increase in asset life
|
||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||
# JE: Journal Entry, FB: Finance Book
|
||||
def clear_depreciation_schedule(self):
|
||||
start = 0
|
||||
for n in range(len(self.schedules)):
|
||||
if not self.schedules[n].journal_entry:
|
||||
del self.schedules[n:]
|
||||
start = n
|
||||
break
|
||||
start = []
|
||||
num_of_depreciations_completed = 0
|
||||
depr_schedule = []
|
||||
|
||||
for schedule in self.get('schedules'):
|
||||
|
||||
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
|
||||
if len(start) == (int(schedule.finance_book_id) - 2):
|
||||
start.append(num_of_depreciations_completed)
|
||||
num_of_depreciations_completed = 0
|
||||
|
||||
# to ensure that start will only be updated once for each FB
|
||||
if len(start) == (int(schedule.finance_book_id) - 1):
|
||||
if schedule.journal_entry:
|
||||
num_of_depreciations_completed += 1
|
||||
depr_schedule.append(schedule)
|
||||
else:
|
||||
start.append(num_of_depreciations_completed)
|
||||
num_of_depreciations_completed = 0
|
||||
|
||||
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
|
||||
if len(start) == (len(self.finance_books) - 1):
|
||||
start.append(num_of_depreciations_completed)
|
||||
|
||||
# when the Depreciation Schedule is being created for the first time
|
||||
if start == []:
|
||||
start = [0] * len(self.finance_books)
|
||||
else:
|
||||
self.schedules = depr_schedule
|
||||
|
||||
return start
|
||||
|
||||
def get_from_date(self, finance_book):
|
||||
@@ -349,7 +376,9 @@ class Asset(AccountsController):
|
||||
|
||||
if from_date:
|
||||
return from_date
|
||||
return self.available_for_use_date
|
||||
|
||||
# since depr for available_for_use_date is not yet booked
|
||||
return add_days(self.available_for_use_date, -1)
|
||||
|
||||
# if it returns True, depreciation_amount will not be equal for the first and last rows
|
||||
def check_is_pro_rata(self, row):
|
||||
@@ -583,7 +612,17 @@ class Asset(AccountsController):
|
||||
return purchase_document
|
||||
|
||||
def get_fixed_asset_account(self):
|
||||
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
|
||||
fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
|
||||
if not fixed_asset_account:
|
||||
frappe.throw(
|
||||
_("Set {0} in asset category {1} for company {2}").format(
|
||||
frappe.bold("Fixed Asset Account"),
|
||||
frappe.bold(self.asset_category),
|
||||
frappe.bold(self.company),
|
||||
),
|
||||
title=_("Account not Found"),
|
||||
)
|
||||
return fixed_asset_account
|
||||
|
||||
def get_cwip_account(self, cwip_enabled=False):
|
||||
cwip_account = None
|
||||
|
||||
@@ -207,9 +207,9 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
|
||||
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
|
||||
("Debtors - _TC", 25000.0, 0.0)
|
||||
)
|
||||
|
||||
@@ -491,10 +491,10 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2030-12-31", 27534.25, 27534.25],
|
||||
["2031-12-31", 30000.0, 57534.25],
|
||||
["2032-12-31", 30000.0, 87534.25],
|
||||
["2033-01-30", 2465.75, 90000.0]
|
||||
['2030-12-31', 27616.44, 27616.44],
|
||||
['2031-12-31', 30000.0, 57616.44],
|
||||
['2032-12-31', 30000.0, 87616.44],
|
||||
['2033-01-30', 2383.56, 90000.0]
|
||||
]
|
||||
|
||||
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
|
||||
@@ -544,10 +544,10 @@ class TestDepreciationMethods(AssetSetup):
|
||||
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
|
||||
|
||||
expected_schedules = [
|
||||
["2030-12-31", 28493.15, 28493.15],
|
||||
["2031-12-31", 35753.43, 64246.58],
|
||||
["2032-12-31", 17876.71, 82123.29],
|
||||
["2033-06-06", 5376.71, 87500.0]
|
||||
['2030-12-31', 28630.14, 28630.14],
|
||||
['2031-12-31', 35684.93, 64315.07],
|
||||
['2032-12-31', 17842.47, 82157.54],
|
||||
['2033-06-06', 5342.46, 87500.0]
|
||||
]
|
||||
|
||||
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
|
||||
@@ -580,10 +580,10 @@ class TestDepreciationMethods(AssetSetup):
|
||||
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
|
||||
|
||||
expected_schedules = [
|
||||
["2030-12-31", 11780.82, 11780.82],
|
||||
["2031-12-31", 44109.59, 55890.41],
|
||||
["2032-12-31", 22054.8, 77945.21],
|
||||
["2033-07-12", 9554.79, 87500.0]
|
||||
["2030-12-31", 11849.32, 11849.32],
|
||||
["2031-12-31", 44075.34, 55924.66],
|
||||
["2032-12-31", 22037.67, 77962.33],
|
||||
["2033-07-12", 9537.67, 87500.0]
|
||||
]
|
||||
|
||||
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
|
||||
@@ -642,7 +642,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
calculate_depreciation = 1,
|
||||
available_for_use_date = getdate("2019-12-31"),
|
||||
available_for_use_date = getdate("2020-01-01"),
|
||||
total_number_of_depreciations = 3,
|
||||
expected_value_after_useful_life = 10000,
|
||||
depreciation_start_date = getdate("2020-07-01"),
|
||||
@@ -653,7 +653,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
["2020-07-01", 15000, 15000],
|
||||
["2021-07-01", 30000, 45000],
|
||||
["2022-07-01", 30000, 75000],
|
||||
["2022-12-31", 15000, 90000]
|
||||
["2023-01-01", 15000, 90000]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@@ -976,6 +976,82 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
self.assertEqual(len(asset.schedules), 1)
|
||||
|
||||
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
available_for_use_date = "2019-12-31",
|
||||
do_not_save = 1
|
||||
)
|
||||
|
||||
asset.calculate_depreciation = 1
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 1,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-01-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 1,
|
||||
"total_number_of_depreciations": 6,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-01-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.submit()
|
||||
|
||||
post_depreciation_entries(date="2020-04-01")
|
||||
asset.load_from_db()
|
||||
|
||||
asset.clear_depreciation_schedule()
|
||||
|
||||
self.assertEqual(len(asset.schedules), 6)
|
||||
|
||||
for schedule in asset.schedules:
|
||||
if schedule.idx <= 3:
|
||||
self.assertEqual(schedule.finance_book_id, "1")
|
||||
else:
|
||||
self.assertEqual(schedule.finance_book_id, "2")
|
||||
|
||||
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
available_for_use_date = "2019-12-31",
|
||||
do_not_save = 1
|
||||
)
|
||||
|
||||
asset.calculate_depreciation = 1
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 3,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.append("finance_books", {
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 6,
|
||||
"expected_value_after_useful_life": 10000,
|
||||
"depreciation_start_date": "2020-12-31"
|
||||
})
|
||||
asset.save()
|
||||
|
||||
self.assertEqual(len(asset.schedules), 9)
|
||||
|
||||
for schedule in asset.schedules:
|
||||
if schedule.idx <= 3:
|
||||
self.assertEqual(schedule.finance_book_id, 1)
|
||||
else:
|
||||
self.assertEqual(schedule.finance_book_id, 2)
|
||||
|
||||
def test_depreciation_entry_cancellation(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
|
||||
@@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
dialog.show()
|
||||
},
|
||||
|
||||
schedule_date(frm) {
|
||||
if(frm.doc.schedule_date){
|
||||
frm.doc.items.forEach((item) => {
|
||||
item.schedule_date = frm.doc.schedule_date;
|
||||
})
|
||||
}
|
||||
refresh_field("items");
|
||||
},
|
||||
preview: (frm) => {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Preview Email'),
|
||||
@@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
dialog.show();
|
||||
}
|
||||
})
|
||||
|
||||
frappe.ui.form.on("Request for Quotation Item", {
|
||||
items_add(frm, cdt, cdn) {
|
||||
if (frm.doc.schedule_date) {
|
||||
frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date);
|
||||
}
|
||||
}
|
||||
});
|
||||
frappe.ui.form.on("Request for Quotation Supplier",{
|
||||
supplier: function(frm, cdt, cdn) {
|
||||
var d = locals[cdt][cdn]
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"vendor",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"status",
|
||||
"amended_from",
|
||||
"suppliers_section",
|
||||
@@ -246,16 +247,22 @@
|
||||
"fieldname": "sec_break_email_2",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Required Date"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-05 22:04:29.017134",
|
||||
"modified": "2021-11-24 17:47:49.909000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
43
erpnext/change_log/v13/v13_18_0.md
Normal file
43
erpnext/change_log/v13/v13_18_0.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Version 13.18.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Deferred Revenue and Expense report with actual and upcoming postings ([#28822](https://github.com/frappe/erpnext/pull/28822))
|
||||
- 'Invoice Number' field in Opening Invoice Creation Tool ([#29147](https://github.com/frappe/erpnext/pull/29147))
|
||||
- Added required_date field to set date in child table ([#28432](https://github.com/frappe/erpnext/pull/28432))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Enable ksa POS Invoice print format ([#28911](https://github.com/frappe/erpnext/pull/28911))
|
||||
- Rename non existent doctype field to the right one ([#29055](https://github.com/frappe/erpnext/pull/29055))
|
||||
- Mapped accounting dimensions for Bank Entry against Payroll Entry ([#29142](https://github.com/frappe/erpnext/pull/29142))
|
||||
- Validate Finished Goods for independent Manufacture entries ([#28555](https://github.com/frappe/erpnext/pull/28555))
|
||||
- Incorrect posting time fetching incorrect stock quantity in stock reconciliation ([#29103](https://github.com/frappe/erpnext/pull/29103))
|
||||
- Stock Ageing Report - Negative Opening Stock ([#28966](https://github.com/frappe/erpnext/pull/28966))
|
||||
- Can't change valuation_method on item ([#28876](https://github.com/frappe/erpnext/pull/28876))
|
||||
- Optimize rate updation on changing price list ([#28953](https://github.com/frappe/erpnext/pull/28953))
|
||||
- Added filter for dispatch address ([#28937](https://github.com/frappe/erpnext/pull/28937))
|
||||
- Convert Item links to Website Item links in `Item Card Group` template data ([#28985](https://github.com/frappe/erpnext/pull/28985))
|
||||
- Earned Leave allocation from Leave Policy Assignment ([#29163](https://github.com/frappe/erpnext/pull/29163))
|
||||
- Items not mapped when trying to create a Maintenance Visit via Maintenance Schedule ([#28917](https://github.com/frappe/erpnext/pull/28917))
|
||||
- For performance improvement, removed forcing of posting sort index on stock balance report ([#28902](https://github.com/frappe/erpnext/pull/28902))
|
||||
- Future recurring period calculation ([#29083](https://github.com/frappe/erpnext/pull/29083))
|
||||
- Nonstock items are showing in the Itemwise Recommended Reorder Level report ([#28873](https://github.com/frappe/erpnext/pull/28873))
|
||||
- Incorrect amount based on payment days in timesheet salary slip ([#28845](https://github.com/frappe/erpnext/pull/28845))
|
||||
- Currency fix for `cost` field in subscription plan ([#28821](https://github.com/frappe/erpnext/pull/28821))
|
||||
- Fetch selling price with pricing rule ([#28951](https://github.com/frappe/erpnext/pull/28951))
|
||||
- Filter out Claimed employee advances in Expense Claim ([#29046](https://github.com/frappe/erpnext/pull/29046))
|
||||
- Tax and Charges template not getting fetched based on tax category assigned ([#29092](https://github.com/frappe/erpnext/pull/29092))
|
||||
- Ignore links while setting default notification templates in Settings ([#29042](https://github.com/frappe/erpnext/pull/29042))
|
||||
- Reset "Value After Depreciation" on reversing journal entry during Asset return ([#28975](https://github.com/frappe/erpnext/pull/28975))
|
||||
- Multicurrency invoices using subscription ([#28916](https://github.com/frappe/erpnext/pull/28916))
|
||||
- Fetch the appointment letter content in the same order as template ([#28968](https://github.com/frappe/erpnext/pull/28968))
|
||||
- Incorrect serial no valuation report showing cancelled entries ([#29172](https://github.com/frappe/erpnext/pull/29172))
|
||||
- Start date validation for deferred invoices ([#29009](https://github.com/frappe/erpnext/pull/29009))
|
||||
- HSN-Wise summary report is incorrect if an invoice has same item code multiple times ([#28783](https://github.com/frappe/erpnext/pull/28783))
|
||||
- Incorrect logic for the "Reserved Qty for Production" field in BIN ([#28880](https://github.com/frappe/erpnext/pull/28880))
|
||||
- Issues in Bank Reconciliation tool ([#28996](https://github.com/frappe/erpnext/pull/28996))
|
||||
- Hide Raw Material table in the Job Card if material transfer is against work order ([#28746](https://github.com/frappe/erpnext/pull/28746))
|
||||
- Added "Is Reverse Charge" checkbox in Tax Category for Indian Companies ([#28935](https://github.com/frappe/erpnext/pull/28935))
|
||||
- Updates in term loan processing ([#28034](https://github.com/frappe/erpnext/pull/28034))
|
||||
- Incorrect bin qty on backdated reconciliation ([#28588](https://github.com/frappe/erpnext/pull/28588))
|
||||
52
erpnext/change_log/v13/v13_19_0.md
Normal file
52
erpnext/change_log/v13/v13_19_0.md
Normal file
@@ -0,0 +1,52 @@
|
||||
## Version 13.19.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Allow user to change the parent company ([#28983](https://github.com/frappe/erpnext/pull/28983))
|
||||
- Option to exclude holidays while marking monthly attendance ([#29185](https://github.com/frappe/erpnext/pull/29185))
|
||||
- Early payment discount on sales & purchase orders ([#29101](https://github.com/frappe/erpnext/pull/29101))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Filter query in bank reconciliation tool ([#29098](https://github.com/frappe/erpnext/pull/29098))
|
||||
- Compute batch ledger in python ([#29324](https://github.com/frappe/erpnext/pull/29324))
|
||||
- GL Entries for loan repayment via Salary ([#29169](https://github.com/frappe/erpnext/pull/29169))
|
||||
- Group by Cost Center in General Ledger report only if include_dimensions is checked ([#28883](https://github.com/frappe/erpnext/pull/28883))
|
||||
- Filter for leave period in Bulk Leave Policy Assignment ([#29272](https://github.com/frappe/erpnext/pull/29272))
|
||||
- Update idx after updating items in so/po ([#29134](https://github.com/frappe/erpnext/pull/29134))
|
||||
- Avoid resetting default warehouse fields for Manufacture Entry ([#29257](https://github.com/frappe/erpnext/pull/29257))
|
||||
- Don't validate FG in repack entry ([#29271](https://github.com/frappe/erpnext/pull/29271))
|
||||
- Map Accounting Dimensions for Bank Entry against Payroll Entry ([#29142](https://github.com/frappe/erpnext/pull/29142))
|
||||
- Ignore cancelled SLEs ([#29303](https://github.com/frappe/erpnext/pull/29303))
|
||||
- Show work order progress bar even it is closed ([#29312](https://github.com/frappe/erpnext/pull/29312))
|
||||
- Incorrect serial no valuation report showing cancelled entries ([#29172](https://github.com/frappe/erpnext/pull/29172))
|
||||
- Not able to make a reverse journal entry ([#29125](https://github.com/frappe/erpnext/pull/29125))
|
||||
- Show ledger balance in Accounts Receivable and Payable summary ([#29135](https://github.com/frappe/erpnext/pull/29135))
|
||||
- Add stock queue in SLE for FIFO valuation method ([#29302](https://github.com/frappe/erpnext/pull/29302))
|
||||
- Threshold fields shows incorrect currency ([#29270](https://github.com/frappe/erpnext/pull/29270))
|
||||
- Added patch to trim whitespace from the serial numbers ([#29306](https://github.com/frappe/erpnext/pull/29306))
|
||||
- Task Depends on not removed from Gantt chart ([#28309](https://github.com/frappe/erpnext/pull/28309))
|
||||
- Earned Leave allocation from Leave Policy Assignment ([#29163](https://github.com/frappe/erpnext/pull/29163))
|
||||
- Exclude existing serial numbers while auto creating new serial numbers ([#29292](https://github.com/frappe/erpnext/pull/29292))
|
||||
- Deferred revenue booking for multi currency invoices via Journal Entry ([#29115](https://github.com/frappe/erpnext/pull/29115))
|
||||
- Fixed autoname generated for Job Applicant ([#29260](https://github.com/frappe/erpnext/pull/29260))
|
||||
- Incorrect scrap item quantity calculated in the Manufacture type stock entry ([#29179](https://github.com/frappe/erpnext/pull/29179))
|
||||
- Inconsistency in calculating outstanding amount ([#29176](https://github.com/frappe/erpnext/pull/29176))
|
||||
- Accounts are coming from different company in the dropdown ([#29280](https://github.com/frappe/erpnext/pull/29280))
|
||||
- Can't create debit note with zero quantity ([#28994](https://github.com/frappe/erpnext/pull/28994))
|
||||
- "Update Cost" should ignore overridden routing times ([#29154](https://github.com/frappe/erpnext/pull/29154))
|
||||
- Modifying Opening invoice creation tool timestamp ([#29127](https://github.com/frappe/erpnext/pull/29127))
|
||||
- Future recurring period calculation ([#29083](https://github.com/frappe/erpnext/pull/29083))
|
||||
- India localization: NIL Rated, Exempted and Non GST Invoices in GSTR-1 report ([#29208](https://github.com/frappe/erpnext/pull/29208))
|
||||
- Purchase to Stock UOM conversion on Production Plan ([#28570](https://github.com/frappe/erpnext/pull/28570))
|
||||
- Validation in POS for item batch no stock quantity ([#28907](https://github.com/frappe/erpnext/pull/28907))
|
||||
- Shopping cart total quantity ([#29076](https://github.com/frappe/erpnext/pull/29076))
|
||||
- POS items added to cart despite low quantity ([#29126](https://github.com/frappe/erpnext/pull/29126))
|
||||
- Exclude unpublished items while fetching items from other item groups ([#29211](https://github.com/frappe/erpnext/pull/29211))
|
||||
- Get project from PO into payment entry ([#29182](https://github.com/frappe/erpnext/pull/29182))
|
||||
- Cover case when all material needs to be bought ([#29326](https://github.com/frappe/erpnext/pull/29326))
|
||||
- Validate setup on clicking Mark Attendance button in Shift Type ([#29146](https://github.com/frappe/erpnext/pull/29146))
|
||||
- Can't ignore pricing rule for one particular POS invoice ([#29222](https://github.com/frappe/erpnext/pull/29222))
|
||||
- Cart & Popup Logic of Item variant without Website Item ([#29383](https://github.com/frappe/erpnext/pull/29383))
|
||||
- Not able to submit salary slips from amended payroll entry. ([#29228](https://github.com/frappe/erpnext/pull/29228))
|
||||
- Tax and Charges template not getting fetched based on tax category assigned ([#29092](https://github.com/frappe/erpnext/pull/29092))
|
||||
32
erpnext/change_log/v13/v13_20_0.md
Normal file
32
erpnext/change_log/v13/v13_20_0.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## Version 13.20.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Provisional accounting for expenses ([#29451](https://github.com/frappe/erpnext/pull/29451))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Incorrect number of items fetched while creating delivery note ([#29454](https://github.com/frappe/erpnext/pull/29454))
|
||||
- Incorrect raw materials quantity in manufacture stock entry ([#29419](https://github.com/frappe/erpnext/pull/29419))
|
||||
- Refactored the update_serial_no function for old Maintenance Visits ([#28843](https://github.com/frappe/erpnext/pull/28843))
|
||||
- Ignore empty customer/supplier in item query ([#29610](https://github.com/frappe/erpnext/pull/29610))
|
||||
- The "Bypass Credit Limit Check" from customer has not fetched in the Customer Credit Balance report ([#29367](https://github.com/frappe/erpnext/pull/29367))
|
||||
- Reset conversion facture after changing the Stock UOM ([#29062](https://github.com/frappe/erpnext/pull/29062))
|
||||
- Cart Items rendering issue ([#29398](https://github.com/frappe/erpnext/pull/29398))
|
||||
- Honour 'include holidays' setting while marking attendance for leave application ([#29425](https://github.com/frappe/erpnext/pull/29425))
|
||||
- Cost of poor quality report time filters not working ([#28958](https://github.com/frappe/erpnext/pull/28958))
|
||||
- Incorrect packing items getting fetched on Sales Return / Credit Note ([#28607](https://github.com/frappe/erpnext/pull/28607))
|
||||
- Regenerate packing items on newly mapped doc ([#29642](https://github.com/frappe/erpnext/pull/29642))
|
||||
- From Time and To Time not updated in drag and drop action for Course Schedule ([#29114](https://github.com/frappe/erpnext/pull/29114))
|
||||
- Employee: set user image and validate user id only if user data is found ([#29452](https://github.com/frappe/erpnext/pull/29452))
|
||||
- Clear Depreciation Schedule before modification ([#28507](https://github.com/frappe/erpnext/pull/28507))
|
||||
- Fixed shopping cart qty badge ([#29077](https://github.com/frappe/erpnext/pull/29077))
|
||||
- Fetch "transfer material against" from BOM ([#29435](https://github.com/frappe/erpnext/pull/29435))
|
||||
- Cart & Popup Logic of Item variant without Website Item ([#29383](https://github.com/frappe/erpnext/pull/29383))
|
||||
- Timesheets: calculate to time based on from time and hours ([#28589](https://github.com/frappe/erpnext/pull/28589))
|
||||
- Dynamically compute BOM Level ([#29522](https://github.com/frappe/erpnext/pull/29522))
|
||||
- Contact duplication on converting lead to customer ([#29337](https://github.com/frappe/erpnext/pull/29337))
|
||||
- Fixed populate practitioner selected in form to check availability popup ([#29405](https://github.com/frappe/erpnext/pull/29405))
|
||||
- Compute batch ledger in python ([#29324](https://github.com/frappe/erpnext/pull/29324))
|
||||
- Incorrect packing list for recurring items & code cleanup ([#29456](https://github.com/frappe/erpnext/pull/29456))
|
||||
- Opening invoice creation tool can fetch multiple accounting dimension ([#29407](https://github.com/frappe/erpnext/pull/29407))
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -113,7 +114,7 @@ class AccountsController(TransactionBase):
|
||||
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
|
||||
|
||||
def validate(self):
|
||||
if not self.get('is_return'):
|
||||
if not self.get('is_return') and not self.get('is_debit_note'):
|
||||
self.validate_qty_is_not_zero()
|
||||
|
||||
if self.get("_action") and self._action != "update_after_submit":
|
||||
@@ -1690,58 +1691,69 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
||||
def update_invoice_status():
|
||||
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
||||
today = getdate()
|
||||
|
||||
payment_schedule = frappe.qb.DocType("Payment Schedule")
|
||||
for doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
frappe.db.sql("""
|
||||
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
|
||||
WHERE invoice.docstatus = 1
|
||||
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
|
||||
AND invoice.outstanding_amount > 0
|
||||
AND (
|
||||
{or_condition}
|
||||
(
|
||||
(
|
||||
CASE
|
||||
WHEN invoice.party_account_currency = invoice.currency
|
||||
THEN (
|
||||
CASE
|
||||
WHEN invoice.disable_rounded_total
|
||||
THEN invoice.grand_total
|
||||
ELSE invoice.rounded_total
|
||||
END
|
||||
)
|
||||
ELSE (
|
||||
CASE
|
||||
WHEN invoice.disable_rounded_total
|
||||
THEN invoice.base_grand_total
|
||||
ELSE invoice.base_rounded_total
|
||||
END
|
||||
)
|
||||
END
|
||||
) - invoice.outstanding_amount
|
||||
) < (
|
||||
SELECT SUM(
|
||||
CASE
|
||||
WHEN invoice.party_account_currency = invoice.currency
|
||||
THEN ps.payment_amount
|
||||
ELSE ps.base_payment_amount
|
||||
END
|
||||
)
|
||||
FROM `tabPayment Schedule` ps
|
||||
WHERE ps.parent = invoice.name
|
||||
AND ps.due_date < %(today)s
|
||||
)
|
||||
)
|
||||
""".format(
|
||||
doctype=doctype,
|
||||
or_condition=(
|
||||
"invoice.is_pos AND invoice.due_date < %(today)s OR"
|
||||
if doctype == "Sales Invoice"
|
||||
else ""
|
||||
)
|
||||
), {"today": today}
|
||||
invoice = frappe.qb.DocType(doctype)
|
||||
|
||||
consider_base_amount = invoice.party_account_currency != invoice.currency
|
||||
payment_amount = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(consider_base_amount, payment_schedule.base_payment_amount)
|
||||
.else_(payment_schedule.payment_amount)
|
||||
)
|
||||
|
||||
payable_amount = (
|
||||
frappe.qb.from_(payment_schedule)
|
||||
.select(Sum(payment_amount))
|
||||
.where(
|
||||
(payment_schedule.parent == invoice.name)
|
||||
& (payment_schedule.due_date < today)
|
||||
)
|
||||
)
|
||||
|
||||
total = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(invoice.disable_rounded_total, invoice.grand_total)
|
||||
.else_(invoice.rounded_total)
|
||||
)
|
||||
|
||||
base_total = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(invoice.disable_rounded_total, invoice.base_grand_total)
|
||||
.else_(invoice.base_rounded_total)
|
||||
)
|
||||
|
||||
total_amount = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(consider_base_amount, base_total)
|
||||
.else_(total)
|
||||
)
|
||||
|
||||
is_overdue = total_amount - invoice.outstanding_amount < payable_amount
|
||||
|
||||
conditions = (
|
||||
(invoice.docstatus == 1)
|
||||
& (invoice.outstanding_amount > 0)
|
||||
& (
|
||||
invoice.status.like("Unpaid%")
|
||||
| invoice.status.like("Partly Paid%")
|
||||
)
|
||||
& (
|
||||
((invoice.is_pos & invoice.due_date < today) | is_overdue)
|
||||
if doctype == "Sales Invoice"
|
||||
else is_overdue
|
||||
)
|
||||
)
|
||||
|
||||
status = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(invoice.status.like("%Discounted"), "Overdue and Discounted")
|
||||
.else_("Overdue")
|
||||
)
|
||||
|
||||
frappe.qb.update(invoice).set("status", status).where(conditions).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
||||
if not terms_template:
|
||||
@@ -2111,6 +2123,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_status_updater()
|
||||
else:
|
||||
parent.check_credit_limit()
|
||||
|
||||
# reset index of child table
|
||||
for idx, row in enumerate(parent.get(child_docname), start=1):
|
||||
row.idx = idx
|
||||
|
||||
parent.save()
|
||||
|
||||
if parent_doctype == 'Purchase Order':
|
||||
|
||||
@@ -249,6 +249,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
del filters['customer']
|
||||
else:
|
||||
del filters['supplier']
|
||||
else:
|
||||
filters.pop('customer', None)
|
||||
filters.pop('supplier', None)
|
||||
|
||||
|
||||
description_cond = ''
|
||||
|
||||
@@ -385,7 +385,7 @@ class SellingController(StockController):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get('stock_qty') or d.get('actual_qty'))
|
||||
|
||||
if not d.incoming_rate:
|
||||
if not (self.get("is_return") and d.incoming_rate):
|
||||
d.incoming_rate = get_incoming_rate({
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.warehouse,
|
||||
|
||||
@@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import (
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate
|
||||
from erpnext.stock.stock_ledger import get_items_to_be_repost
|
||||
|
||||
|
||||
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
||||
@@ -40,7 +40,10 @@ class StockController(AccountsController):
|
||||
if self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
||||
provisional_accounting_for_non_stock_items = \
|
||||
cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))
|
||||
|
||||
if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items:
|
||||
warehouse_account = get_warehouse_account_map(self.company)
|
||||
|
||||
if self.docstatus==1:
|
||||
@@ -77,17 +80,17 @@ class StockController(AccountsController):
|
||||
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
|
||||
|
||||
def clean_serial_nos(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
|
||||
|
||||
for row in self.get("items"):
|
||||
if hasattr(row, "serial_no") and row.serial_no:
|
||||
# replace commas by linefeed
|
||||
row.serial_no = row.serial_no.replace(",", "\n")
|
||||
# remove extra whitespace and store one serial no on each line
|
||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||
|
||||
# strip preceeding and succeeding spaces for each SN
|
||||
# (SN could have valid spaces in between e.g. SN - 123 - 2021)
|
||||
serial_no_list = row.serial_no.split("\n")
|
||||
serial_no_list = [sn.strip() for sn in serial_no_list]
|
||||
|
||||
row.serial_no = "\n".join(serial_no_list)
|
||||
for row in self.get('packed_items') or []:
|
||||
if hasattr(row, "serial_no") and row.serial_no:
|
||||
# remove extra whitespace and store one serial no on each line
|
||||
row.serial_no = clean_serial_no_string(row.serial_no)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
|
||||
default_cost_center=None):
|
||||
@@ -111,17 +114,6 @@ class StockController(AccountsController):
|
||||
|
||||
self.check_expense_account(item_row)
|
||||
|
||||
# If the item does not have the allow zero valuation rate flag set
|
||||
# and ( valuation rate not mentioned in an incoming entry
|
||||
# or incoming entry not found while delivering the item),
|
||||
# try to pick valuation rate from previous sle or Item master and update in SLE
|
||||
# Otherwise, throw an exception
|
||||
|
||||
if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \
|
||||
and not item_row.get("allow_zero_valuation_rate"):
|
||||
|
||||
sle = self.update_stock_ledger_entries(sle)
|
||||
|
||||
# expense account/ target_warehouse / source_warehouse
|
||||
if item_row.get('target_warehouse'):
|
||||
warehouse = item_row.get('target_warehouse')
|
||||
@@ -164,26 +156,6 @@ class StockController(AccountsController):
|
||||
|
||||
return frappe.flags.debit_field_precision
|
||||
|
||||
def update_stock_ledger_entries(self, sle):
|
||||
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
self.doctype, self.name, currency=self.company_currency, company=self.company)
|
||||
|
||||
sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate)
|
||||
sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate)
|
||||
|
||||
if sle.name:
|
||||
frappe.db.sql("""
|
||||
update
|
||||
`tabStock Ledger Entry`
|
||||
set
|
||||
stock_value = %(stock_value)s,
|
||||
valuation_rate = %(valuation_rate)s,
|
||||
stock_value_difference = %(stock_value_difference)s
|
||||
where
|
||||
name = %(name)s""", (sle))
|
||||
|
||||
return sle
|
||||
|
||||
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
|
||||
if self.doctype == "Stock Reconciliation":
|
||||
reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose")
|
||||
@@ -287,11 +259,7 @@ class StockController(AccountsController):
|
||||
for d in self.items:
|
||||
if not d.batch_no: continue
|
||||
|
||||
serial_nos = [sr.name for sr in frappe.get_all("Serial No",
|
||||
{'batch_no': d.batch_no, 'status': 'Inactive'})]
|
||||
|
||||
if serial_nos:
|
||||
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
|
||||
frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None)
|
||||
|
||||
d.batch_no = None
|
||||
d.db_set("batch_no", None)
|
||||
|
||||
@@ -139,6 +139,8 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
if not item.qty and self.doc.get("is_return"):
|
||||
item.amount = flt(-1 * item.rate, item.precision("amount"))
|
||||
elif not item.qty and self.doc.get("is_debit_note"):
|
||||
item.amount = flt(item.rate, item.precision("amount"))
|
||||
else:
|
||||
item.amount = flt(item.rate * item.qty, item.precision("amount"))
|
||||
|
||||
@@ -594,13 +596,14 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
grand_total = self.doc.rounded_total or self.doc.grand_total
|
||||
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
|
||||
|
||||
if self.doc.party_account_currency == self.doc.currency:
|
||||
total_amount_to_pay = flt(grand_total - self.doc.total_advance
|
||||
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
|
||||
else:
|
||||
total_amount_to_pay = flt(flt(grand_total *
|
||||
self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance
|
||||
- flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
|
||||
total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance
|
||||
- flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total"))
|
||||
|
||||
self.doc.round_floats_in(self.doc, ["paid_amount"])
|
||||
change_amount = 0
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import unittest
|
||||
from functools import partial
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers import queries
|
||||
|
||||
|
||||
@@ -54,6 +56,12 @@ class TestQueries(unittest.TestCase):
|
||||
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
|
||||
self.assertEqual(len(bundled_stock_items), 0)
|
||||
|
||||
# empty customer/supplier should be stripped of instead of failure
|
||||
query(txt="", filters={"customer": None})
|
||||
query(txt="", filters={"customer": ""})
|
||||
query(txt="", filters={"supplier": None})
|
||||
query(txt="", filters={"supplier": ""})
|
||||
|
||||
def test_bom_qury(self):
|
||||
query = add_default_params(queries.bom, "BOM")
|
||||
|
||||
@@ -85,3 +93,6 @@ class TestQueries(unittest.TestCase):
|
||||
|
||||
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
|
||||
self.assertGreaterEqual(len(wh), 1)
|
||||
|
||||
def test_default_uoms(self):
|
||||
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
|
||||
|
||||
@@ -4,19 +4,72 @@ import frappe
|
||||
|
||||
|
||||
class TestUtils(unittest.TestCase):
|
||||
def test_reset_default_field_value(self):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Purchase Receipt",
|
||||
"set_warehouse": "Warehouse 1",
|
||||
})
|
||||
def test_reset_default_field_value(self):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Purchase Receipt",
|
||||
"set_warehouse": "Warehouse 1",
|
||||
})
|
||||
|
||||
# Same values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
||||
# Same values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
||||
|
||||
# Mixed values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, None)
|
||||
# Mixed values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, None)
|
||||
|
||||
def test_reset_default_field_value_in_mfg_stock_entry(self):
|
||||
# manufacture stock entry with rows having blank source/target wh
|
||||
se = frappe.get_doc(
|
||||
doctype="Stock Entry",
|
||||
purpose="Manufacture",
|
||||
stock_entry_type="Manufacture",
|
||||
company="_Test Company",
|
||||
from_warehouse="_Test Warehouse - _TC",
|
||||
to_warehouse="_Test Warehouse 1 - _TC",
|
||||
items=[
|
||||
frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
|
||||
frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1)
|
||||
]
|
||||
)
|
||||
se.save()
|
||||
|
||||
# default fields must be untouched
|
||||
self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
|
||||
self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC")
|
||||
|
||||
se.delete()
|
||||
|
||||
def test_reset_default_field_value_in_transfer_stock_entry(self):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Stock Entry",
|
||||
"purpose": "Material Receipt",
|
||||
"from_warehouse": "Warehouse 1",
|
||||
"to_warehouse": "Warehouse 2",
|
||||
})
|
||||
|
||||
# Same values
|
||||
doc.items = [
|
||||
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
|
||||
]
|
||||
|
||||
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||
self.assertEqual(doc.from_warehouse, "Warehouse 1")
|
||||
self.assertEqual(doc.to_warehouse, "Warehouse 2")
|
||||
|
||||
# Mixed values in source wh
|
||||
doc.items = [
|
||||
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||
{"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"},
|
||||
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
|
||||
]
|
||||
|
||||
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||
self.assertEqual(doc.from_warehouse, None)
|
||||
self.assertEqual(doc.to_warehouse, "Warehouse 2")
|
||||
@@ -46,12 +46,26 @@ class TestWebsiteItem(unittest.TestCase):
|
||||
]
|
||||
})
|
||||
elif self._testMethodName in WEBITEM_PRICE_TESTS:
|
||||
create_user_and_customer_if_not_exists("test_contact_customer@example.com", "_Test Contact For _Test Customer")
|
||||
create_regular_web_item()
|
||||
make_web_item_price(item_code="Test Mobile Phone")
|
||||
|
||||
# Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass.
|
||||
# This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor,
|
||||
# when testing for logged-in user the test will get the previous pricing rule because "selling" is still true.
|
||||
#
|
||||
# I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test.
|
||||
make_web_pricing_rule(
|
||||
title="Test Pricing Rule for Test Mobile Phone",
|
||||
item_code="Test Mobile Phone",
|
||||
selling=1)
|
||||
make_web_pricing_rule(
|
||||
title="Test Pricing Rule for Test Mobile Phone (Customer)",
|
||||
item_code="Test Mobile Phone",
|
||||
selling=1,
|
||||
discount_percentage="25",
|
||||
applicable_for="Customer",
|
||||
customer="_Test Customer")
|
||||
|
||||
def test_index_creation(self):
|
||||
"Check if index is getting created in db."
|
||||
@@ -188,22 +202,27 @@ class TestWebsiteItem(unittest.TestCase):
|
||||
|
||||
# price and pricing rule added via setUp
|
||||
|
||||
# login as customer with pricing rule
|
||||
frappe.set_user("test_contact_customer@example.com")
|
||||
|
||||
# check if price and slashed price is fetched correctly
|
||||
frappe.local.shopping_cart_settings = None
|
||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||
self.assertTrue(bool(data.product_info["price"]))
|
||||
|
||||
price_object = data.product_info["price"]
|
||||
self.assertEqual(price_object.get("discount_percent"), 10)
|
||||
self.assertEqual(price_object.get("price_list_rate"), 900)
|
||||
self.assertEqual(price_object.get("discount_percent"), 25)
|
||||
self.assertEqual(price_object.get("price_list_rate"), 750)
|
||||
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
|
||||
self.assertEqual(price_object.get("formatted_price"), "₹ 900.00")
|
||||
self.assertEqual(price_object.get("formatted_discount_percent"), "10%")
|
||||
self.assertEqual(price_object.get("formatted_price"), "₹ 750.00")
|
||||
self.assertEqual(price_object.get("formatted_discount_percent"), "25%")
|
||||
|
||||
# disable show price
|
||||
# switch to admin and disable show price
|
||||
frappe.set_user("Administrator")
|
||||
setup_e_commerce_settings({"show_price": 0})
|
||||
|
||||
# price should not be fetched
|
||||
# price should not be fetched for logged in user.
|
||||
frappe.set_user("test_contact_customer@example.com")
|
||||
frappe.local.shopping_cart_settings = None
|
||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
||||
self.assertFalse(bool(data.product_info["price"]))
|
||||
@@ -485,10 +504,34 @@ def make_web_pricing_rule(**kwargs):
|
||||
"discount_percentage": kwargs.get("discount_percentage") or 10,
|
||||
"company": kwargs.get("company") or "_Test Company",
|
||||
"currency": kwargs.get("currency") or "INR",
|
||||
"for_price_list": kwargs.get("price_list") or "_Test Price List India"
|
||||
"for_price_list": kwargs.get("price_list") or "_Test Price List India",
|
||||
"applicable_for": kwargs.get("applicable_for") or "",
|
||||
"customer": kwargs.get("customer") or "",
|
||||
})
|
||||
pricing_rule.insert()
|
||||
else:
|
||||
pricing_rule = frappe.get_doc("Pricing Rule", {"title": title})
|
||||
|
||||
return pricing_rule
|
||||
return pricing_rule
|
||||
|
||||
|
||||
def create_user_and_customer_if_not_exists(email, first_name = None):
|
||||
if frappe.db.exists("User", email):
|
||||
return
|
||||
|
||||
frappe.get_doc({
|
||||
"doctype": "User",
|
||||
"user_type": "Website User",
|
||||
"email": email,
|
||||
"send_welcome_email": 0,
|
||||
"first_name": first_name or email.split("@")[0]
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
contact = frappe.get_last_doc("Contact", filters={"email_id": email})
|
||||
link = contact.append('links', {})
|
||||
link.link_doctype = "Customer"
|
||||
link.link_name = "_Test Customer"
|
||||
link.link_title = "_Test Customer"
|
||||
contact.save()
|
||||
|
||||
test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import itertools
|
||||
import json
|
||||
|
||||
import frappe
|
||||
@@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator):
|
||||
context.body_class = "product-page"
|
||||
|
||||
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
|
||||
self.attributes = frappe.get_all("Item Variant Attribute",
|
||||
self.attributes = frappe.get_all(
|
||||
"Item Variant Attribute",
|
||||
fields=["attribute", "attribute_value"],
|
||||
filters={"parent": self.item_code})
|
||||
filters={"parent": self.item_code}
|
||||
)
|
||||
|
||||
if self.slideshow:
|
||||
context.update(get_slideshow(self))
|
||||
|
||||
self.set_variant_context(context)
|
||||
self.set_attribute_context(context)
|
||||
self.set_disabled_attributes(context)
|
||||
self.set_metatags(context)
|
||||
self.set_shopping_cart_data(context)
|
||||
|
||||
@@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
return context
|
||||
|
||||
def set_variant_context(self, context):
|
||||
if not self.has_variants:
|
||||
return
|
||||
|
||||
context.no_cache = True
|
||||
variant = frappe.form_dict.variant
|
||||
|
||||
# load variants
|
||||
# also used in set_attribute_context
|
||||
context.variants = frappe.get_all(
|
||||
"Item",
|
||||
filters={
|
||||
"variant_of": self.item_code,
|
||||
"published_in_website": 1
|
||||
},
|
||||
order_by="name asc")
|
||||
|
||||
# the case when the item is opened for the first time from its list
|
||||
if not variant and context.variants:
|
||||
variant = context.variants[0]
|
||||
|
||||
if variant:
|
||||
context.variant = frappe.get_doc("Item", variant)
|
||||
fields = ("website_image", "website_image_alt", "web_long_description", "description",
|
||||
"website_specifications")
|
||||
|
||||
for fieldname in fields:
|
||||
if context.variant.get(fieldname):
|
||||
value = context.variant.get(fieldname)
|
||||
if isinstance(value, list):
|
||||
value = [d.as_dict() for d in value]
|
||||
|
||||
context[fieldname] = value
|
||||
|
||||
if self.slideshow and context.variant and context.variant.slideshow:
|
||||
context.update(get_slideshow(context.variant))
|
||||
|
||||
|
||||
def set_attribute_context(self, context):
|
||||
if not self.has_variants:
|
||||
return
|
||||
|
||||
attribute_values_available = {}
|
||||
context.attribute_values = {}
|
||||
context.selected_attributes = {}
|
||||
|
||||
# load attributes
|
||||
self.set_selected_attributes(context.variants, context, attribute_values_available)
|
||||
|
||||
# filter attributes, order based on attribute table
|
||||
item = frappe.get_cached_doc("Item", self.item_code)
|
||||
self.set_attribute_values(item.attributes, context, attribute_values_available)
|
||||
|
||||
context.variant_info = json.dumps(context.variants)
|
||||
|
||||
def set_selected_attributes(self, variants, context, attribute_values_available):
|
||||
for variant in variants:
|
||||
variant.attributes = frappe.get_all(
|
||||
@@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator):
|
||||
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
||||
values.append(attr_value.attribute_value)
|
||||
|
||||
def set_disabled_attributes(self, context):
|
||||
"""Disable selection options of attribute combinations that do not result in a variant"""
|
||||
|
||||
if not self.attributes or not self.has_variants:
|
||||
return
|
||||
|
||||
context.disabled_attributes = {}
|
||||
attributes = [attr.attribute for attr in self.attributes]
|
||||
|
||||
def find_variant(combination):
|
||||
for variant in context.variants:
|
||||
if len(variant.attributes) < len(attributes):
|
||||
continue
|
||||
|
||||
if "combination" not in variant:
|
||||
ref_combination = []
|
||||
|
||||
for attr in variant.attributes:
|
||||
idx = attributes.index(attr.attribute)
|
||||
ref_combination.insert(idx, attr.attribute_value)
|
||||
|
||||
variant["combination"] = ref_combination
|
||||
|
||||
if not (set(combination) - set(variant["combination"])):
|
||||
# check if the combination is a subset of a variant combination
|
||||
# eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
|
||||
return True
|
||||
|
||||
for i, attr in enumerate(self.attributes):
|
||||
if i == 0:
|
||||
continue
|
||||
|
||||
combination_source = []
|
||||
|
||||
# loop through previous attributes
|
||||
for prev_attr in self.attributes[:i]:
|
||||
combination_source.append([context.selected_attributes.get(prev_attr.attribute)])
|
||||
|
||||
combination_source.append(context.attribute_values[attr.attribute])
|
||||
|
||||
for combination in itertools.product(*combination_source):
|
||||
if not find_variant(combination):
|
||||
context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
|
||||
|
||||
def set_metatags(self, context):
|
||||
context.metatags = frappe._dict({})
|
||||
|
||||
|
||||
@@ -197,7 +197,10 @@ class ProductQuery:
|
||||
website_item_groups = frappe.db.get_all(
|
||||
"Website Item",
|
||||
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
|
||||
filters=[["Website Item Group", "item_group", "=", item_group]]
|
||||
filters=[
|
||||
["Website Item Group", "item_group", "=", item_group],
|
||||
["published", "=", 1]
|
||||
]
|
||||
)
|
||||
return website_item_groups
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ def set_cart_count(quotation=None):
|
||||
if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
|
||||
if not quotation:
|
||||
quotation = _get_cart_quotation()
|
||||
cart_count = cstr(len(quotation.get("items")))
|
||||
cart_count = cstr(cint(quotation.get("total_qty")))
|
||||
|
||||
if hasattr(frappe.local, "cookie_manager"):
|
||||
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
|
||||
@@ -276,10 +276,29 @@ def guess_territory():
|
||||
|
||||
def decorate_quotation_doc(doc):
|
||||
for d in doc.get("items", []):
|
||||
item_code = d.item_code
|
||||
fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
|
||||
|
||||
# Variant Item
|
||||
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
||||
variant_data = frappe.db.get_values(
|
||||
"Item",
|
||||
filters={"item_code": item_code},
|
||||
fieldname=["variant_of", "item_name", "image"],
|
||||
as_dict=True
|
||||
)[0]
|
||||
item_code = variant_data.variant_of
|
||||
fields = fields[1:]
|
||||
d.web_item_name = variant_data.item_name
|
||||
|
||||
if variant_data.image: # get image from variant or template web item
|
||||
d.thumbnail = variant_data.image
|
||||
fields = fields[2:]
|
||||
|
||||
d.update(frappe.db.get_value(
|
||||
"Website Item",
|
||||
{"item_code": d.item_code},
|
||||
["web_item_name", "thumbnail", "website_image", "description", "route"],
|
||||
{"item_code": item_code},
|
||||
fields,
|
||||
as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -9,8 +9,13 @@ from frappe.utils import add_months, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
|
||||
from erpnext.tests.utils import create_test_contact_and_address
|
||||
from erpnext.e_commerce.shopping_cart.cart import (
|
||||
_get_cart_quotation,
|
||||
get_cart_quotation,
|
||||
get_party,
|
||||
update_cart,
|
||||
)
|
||||
from erpnext.tests.utils import change_settings, create_test_contact_and_address
|
||||
|
||||
|
||||
class TestShoppingCart(unittest.TestCase):
|
||||
@@ -34,6 +39,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.set_user("Administrator")
|
||||
self.disable_shopping_cart()
|
||||
|
||||
@@ -128,6 +134,43 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
self.remove_test_quotation(quotation)
|
||||
|
||||
@change_settings("E Commerce Settings",{
|
||||
"company": "_Test Company",
|
||||
"enabled": 1,
|
||||
"default_customer_group": "_Test Customer Group",
|
||||
"price_list": "_Test Price List India",
|
||||
"show_price": 1
|
||||
})
|
||||
def test_add_item_variant_without_web_item_to_cart(self):
|
||||
"Test adding Variants having no Website Items in cart via Template Web Item."
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
template_item = make_item("Test-Tshirt-Temp", {
|
||||
"has_variant": 1,
|
||||
"variant_based_on": "Item Attribute",
|
||||
"attributes": [
|
||||
{"attribute": "Test Size"},
|
||||
{"attribute": "Test Colour"}
|
||||
]
|
||||
})
|
||||
variant = create_variant("Test-Tshirt-Temp", {
|
||||
"Test Size": "Small", "Test Colour": "Red"
|
||||
})
|
||||
variant.save()
|
||||
make_website_item(template_item) # publish template not variant
|
||||
|
||||
update_cart("Test-Tshirt-Temp-S-R", 1)
|
||||
|
||||
cart = get_cart_quotation() # test if cart page gets data without errors
|
||||
doc = cart.get("doc")
|
||||
|
||||
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
|
||||
|
||||
# test if items are rendered without error
|
||||
frappe.render_template("templates/includes/cart/cart_items.html", cart)
|
||||
|
||||
def create_tax_rule(self):
|
||||
tax_rule = frappe.get_test_records("Tax Rule")[0]
|
||||
try:
|
||||
|
||||
@@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
|
||||
val = frappe.cache().get_value('ordered_attribute_values_map')
|
||||
if val: return val
|
||||
|
||||
all_attribute_values = frappe.db.get_all('Item Attribute Value',
|
||||
all_attribute_values = frappe.get_all('Item Attribute Value',
|
||||
['attribute_value', 'idx', 'parent'], order_by='idx asc')
|
||||
|
||||
ordered_attribute_values_map = frappe._dict({})
|
||||
@@ -57,25 +57,34 @@ class ItemVariantsCacheManager:
|
||||
def build_cache(self):
|
||||
parent_item_code = self.item_code
|
||||
|
||||
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
|
||||
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
|
||||
attributes = [
|
||||
a.attribute for a in frappe.get_all(
|
||||
'Item Variant Attribute',
|
||||
{'parent': parent_item_code},
|
||||
['attribute'],
|
||||
order_by='idx asc'
|
||||
)
|
||||
]
|
||||
|
||||
item_variants_data = frappe.db.get_all('Item Variant Attribute',
|
||||
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
|
||||
# join with Website Item
|
||||
item_variants_data = frappe.get_all(
|
||||
'Item Variant Attribute',
|
||||
{'variant_of': parent_item_code},
|
||||
['parent', 'attribute', 'attribute_value'],
|
||||
order_by='name',
|
||||
as_list=1
|
||||
)
|
||||
|
||||
unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
|
||||
disabled_items = set(
|
||||
[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
|
||||
)
|
||||
|
||||
attribute_value_item_map = frappe._dict({})
|
||||
item_attribute_value_map = frappe._dict({})
|
||||
attribute_value_item_map = frappe._dict()
|
||||
item_attribute_value_map = frappe._dict()
|
||||
|
||||
# dont consider variants that are unpublished
|
||||
# (either have no Website Item or are unpublished in Website Item)
|
||||
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
|
||||
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
|
||||
# dont consider variants that are disabled
|
||||
# pull all other variants
|
||||
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
|
||||
|
||||
for row in item_variants_data:
|
||||
item_code, attribute, attribute_value = row
|
||||
|
||||
@@ -1,11 +1,119 @@
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
# from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
||||
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
||||
setup_e_commerce_settings,
|
||||
)
|
||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
||||
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
class TestVariantSelector(unittest.TestCase):
|
||||
# TODO: Variant Selector Tests
|
||||
pass
|
||||
class TestVariantSelector(ERPNextTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
template_item = make_item("Test-Tshirt-Temp", {
|
||||
"has_variant": 1,
|
||||
"variant_based_on": "Item Attribute",
|
||||
"attributes": [
|
||||
{"attribute": "Test Size"},
|
||||
{"attribute": "Test Colour"}
|
||||
]
|
||||
})
|
||||
|
||||
# create L-R, L-G, M-R, M-G and S-R
|
||||
for size in ("Large", "Medium",):
|
||||
for colour in ("Red", "Green",):
|
||||
variant = create_variant("Test-Tshirt-Temp", {
|
||||
"Test Size": size, "Test Colour": colour
|
||||
})
|
||||
variant.save()
|
||||
|
||||
variant = create_variant("Test-Tshirt-Temp", {
|
||||
"Test Size": "Small", "Test Colour": "Red"
|
||||
})
|
||||
variant.save()
|
||||
|
||||
make_website_item(template_item) # publish template not variants
|
||||
|
||||
def test_item_attributes(self):
|
||||
"""
|
||||
Test if the right attributes are fetched in the popup.
|
||||
(Attributes must only come from active items)
|
||||
|
||||
Attribute selection must not be linked to Website Items.
|
||||
"""
|
||||
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
|
||||
|
||||
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||
|
||||
self.assertEqual(attr_data[0]["attribute"], "Test Size")
|
||||
self.assertEqual(attr_data[1]["attribute"], "Test Colour")
|
||||
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
|
||||
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
|
||||
|
||||
# disable small red tshirt, now there are no small tshirts.
|
||||
# but there are some red tshirts
|
||||
small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
|
||||
small_variant.disabled = 1
|
||||
small_variant.save() # trigger cache rebuild
|
||||
|
||||
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
|
||||
|
||||
# Only L and M attribute values must be fetched since S is disabled
|
||||
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
|
||||
|
||||
# teardown
|
||||
small_variant.disabled = 0
|
||||
small_variant.save()
|
||||
|
||||
def test_next_item_variant_values(self):
|
||||
"""
|
||||
Test if on selecting an attribute value, the next possible values
|
||||
are filtered accordingly.
|
||||
Values that dont apply should not be fetched.
|
||||
E.g.
|
||||
There is a ** Small-Red ** Tshirt. No other colour in this size.
|
||||
On selecting ** Small **, only ** Red ** should be selectable next.
|
||||
"""
|
||||
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
|
||||
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
|
||||
filtered_items = next_values["filtered_items"]
|
||||
|
||||
self.assertEqual(len(next_colours), 1)
|
||||
self.assertEqual(next_colours.pop(), "Red")
|
||||
self.assertEqual(len(filtered_items), 1)
|
||||
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
|
||||
|
||||
def test_exact_match_with_price(self):
|
||||
"""
|
||||
Test price fetching and matching of variant without Website Item
|
||||
"""
|
||||
from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
setup_e_commerce_settings({
|
||||
"company": "_Test Company",
|
||||
"enabled": 1,
|
||||
"default_customer_group": "_Test Customer Group",
|
||||
"price_list": "_Test Price List India",
|
||||
"show_price": 1
|
||||
})
|
||||
|
||||
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
|
||||
next_values = get_next_attribute_and_values(
|
||||
"Test-Tshirt-Temp",
|
||||
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
|
||||
)
|
||||
print(">>>>", next_values)
|
||||
price_info = next_values["product_info"]["price"]
|
||||
|
||||
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
|
||||
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
|
||||
self.assertEqual(price_info["price_list_rate"], 100.0)
|
||||
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")
|
||||
@@ -1,7 +1,12 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
||||
get_shopping_cart_settings,
|
||||
)
|
||||
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
|
||||
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
|
||||
from erpnext.utilities.product import get_price
|
||||
|
||||
|
||||
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
|
||||
@@ -143,14 +148,13 @@ def get_next_attribute_and_values(item_code, selected_attributes):
|
||||
filtered_items_count = len(filtered_items)
|
||||
|
||||
# get product info if exact match
|
||||
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||
# from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||
if exact_match:
|
||||
data = get_product_info_for_website(exact_match[0])
|
||||
product_info = data.product_info
|
||||
cart_settings = get_shopping_cart_settings()
|
||||
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
|
||||
|
||||
if product_info:
|
||||
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
|
||||
if not data.cart_settings.show_price:
|
||||
product_info = None
|
||||
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
|
||||
else:
|
||||
product_info = None
|
||||
|
||||
@@ -195,3 +199,20 @@ def get_item_attributes(item_code):
|
||||
|
||||
return attributes
|
||||
|
||||
def get_item_variant_price_dict(item_code, cart_settings):
|
||||
if cart_settings.enabled and cart_settings.show_price:
|
||||
is_guest = frappe.session.user == "Guest"
|
||||
# Show Price if logged in.
|
||||
# If not logged in, check if price is hidden for guest.
|
||||
if not is_guest or not cart_settings.hide_price_for_guest:
|
||||
price_list = _set_price_list(cart_settings, None)
|
||||
price = get_price(
|
||||
item_code,
|
||||
price_list,
|
||||
cart_settings.default_customer_group,
|
||||
cart_settings.company
|
||||
)
|
||||
return {"price": price}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
|
||||
{%- set item = values['card_' + index + '_item'] -%}
|
||||
{%- if item -%}
|
||||
{%- set item = frappe.get_doc("Item", item) -%}
|
||||
{%- set web_item = frappe.get_doc("Website Item", item) -%}
|
||||
{{ item_card(
|
||||
item, is_featured=values['card_' + index + '_featured'],
|
||||
web_item, is_featured=values['card_' + index + '_featured'],
|
||||
is_full_width=True, align="Center"
|
||||
) }}
|
||||
{%- endif -%}
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
{
|
||||
"fieldname": "card_1_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -56,8 +56,8 @@
|
||||
{
|
||||
"fieldname": "card_2_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -76,8 +76,8 @@
|
||||
{
|
||||
"fieldname": "card_3_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -95,8 +95,8 @@
|
||||
{
|
||||
"fieldname": "card_4_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -114,8 +114,8 @@
|
||||
{
|
||||
"fieldname": "card_5_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -133,8 +133,8 @@
|
||||
{
|
||||
"fieldname": "card_6_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -152,8 +152,8 @@
|
||||
{
|
||||
"fieldname": "card_7_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -171,8 +171,8 @@
|
||||
{
|
||||
"fieldname": "card_8_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -190,8 +190,8 @@
|
||||
{
|
||||
"fieldname": "card_9_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -209,8 +209,8 @@
|
||||
{
|
||||
"fieldname": "card_10_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -228,8 +228,8 @@
|
||||
{
|
||||
"fieldname": "card_11_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -247,8 +247,8 @@
|
||||
{
|
||||
"fieldname": "card_12_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"options": "Item",
|
||||
"label": "Website Item",
|
||||
"options": "Website Item",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -259,7 +259,7 @@
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2021-02-24 16:05:31.242342",
|
||||
"modified": "2021-12-21 14:44:59.821335",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Item Card Group",
|
||||
|
||||
@@ -201,8 +201,8 @@ def get_course_schedule_events(start, end, filters=None):
|
||||
conditions = get_event_conditions("Course Schedule", filters)
|
||||
|
||||
data = frappe.db.sql("""select name, course, color,
|
||||
timestamp(schedule_date, from_time) as from_datetime,
|
||||
timestamp(schedule_date, to_time) as to_datetime,
|
||||
timestamp(schedule_date, from_time) as from_time,
|
||||
timestamp(schedule_date, to_time) as to_time,
|
||||
room, student_group, 0 as 'allDay'
|
||||
from `tabCourse Schedule`
|
||||
where ( schedule_date between %(start)s and %(end)s )
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
@@ -30,6 +32,14 @@ class CourseSchedule(Document):
|
||||
if self.from_time > self.to_time:
|
||||
frappe.throw(_("From Time cannot be greater than To Time."))
|
||||
|
||||
"""Handles specicfic case to update schedule date in calendar """
|
||||
if isinstance(self.from_time, str):
|
||||
try:
|
||||
datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S')
|
||||
self.schedule_date = datetime_obj
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def validate_overlap(self):
|
||||
"""Validates overlap for Student Group, Instructor, Room"""
|
||||
|
||||
@@ -47,4 +57,4 @@ class CourseSchedule(Document):
|
||||
validate_overlap_for(self, "Assessment Plan", "student_group")
|
||||
|
||||
validate_overlap_for(self, "Assessment Plan", "room")
|
||||
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
|
||||
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
|
||||
@@ -1,11 +1,10 @@
|
||||
frappe.views.calendar["Course Schedule"] = {
|
||||
field_map: {
|
||||
// from_datetime and to_datetime don't exist as docfields but are used in onload
|
||||
"start": "from_datetime",
|
||||
"end": "to_datetime",
|
||||
"start": "from_time",
|
||||
"end": "to_time",
|
||||
"id": "name",
|
||||
"title": "course",
|
||||
"allDay": "allDay"
|
||||
"allDay": "allDay",
|
||||
},
|
||||
gantt: false,
|
||||
order_by: "schedule_date",
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import to_timedelta, today
|
||||
from frappe.utils.data import add_to_date
|
||||
|
||||
from erpnext.education.utils import OverlapError
|
||||
|
||||
@@ -39,6 +40,11 @@ class TestCourseSchedule(unittest.TestCase):
|
||||
make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
|
||||
student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
|
||||
|
||||
def test_update_schedule_date(self):
|
||||
doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1))
|
||||
doc.schedule_date = add_to_date(doc.schedule_date, days=1)
|
||||
doc.save()
|
||||
|
||||
def make_course_schedule_test_record(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
|
||||
@@ -99,7 +99,6 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "inpatient_record.patient",
|
||||
"fieldname": "patient",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
@@ -559,7 +558,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-30 11:04:17.195848",
|
||||
"modified": "2022-01-20 12:37:07.943153",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Lab Test",
|
||||
|
||||
@@ -143,7 +143,7 @@ class Patient(Document):
|
||||
age = self.age
|
||||
if not age:
|
||||
return
|
||||
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
|
||||
age_str = str(age.years) + ' ' + _("Year(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
|
||||
return age_str
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -337,9 +337,13 @@ let check_and_set_availability = function(frm) {
|
||||
});
|
||||
|
||||
d.fields_dict['department'].df.onchange = () => {
|
||||
d.set_values({
|
||||
'practitioner': ''
|
||||
});
|
||||
if (d.get_value('department') == frm.doc.department) {
|
||||
d.set_value('practitioner', frm.doc.practitioner);
|
||||
} else {
|
||||
d.set_value('practitioner', '');
|
||||
d.fields_dict.available_slots.html('');
|
||||
d.get_primary_btn().attr('disabled', true);
|
||||
}
|
||||
let department = d.get_value('department');
|
||||
if (department) {
|
||||
d.fields_dict.practitioner.get_query = function() {
|
||||
@@ -426,7 +430,8 @@ let check_and_set_availability = function(frm) {
|
||||
|
||||
slot_details.forEach((slot_info) => {
|
||||
slot_html += `<div class="slot-info">
|
||||
<span> <b> ${__('Practitioner Schedule:')} </b> ${slot_info.slot_name} </span><br>
|
||||
<span> <b> ${slot_info.practitioner_name} </b> </span><br>
|
||||
<span> <b> ${__('Schedule:')} </b> ${slot_info.slot_name} </span><br>
|
||||
<span> <b> ${__('Service Unit:')} </b> ${slot_info.service_unit} </span>`;
|
||||
|
||||
if (slot_info.service_unit_capacity) {
|
||||
|
||||
@@ -388,7 +388,8 @@ def get_available_slots(practitioner_doc, date):
|
||||
fields=['name', 'appointment_time', 'duration', 'status'])
|
||||
|
||||
slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots,
|
||||
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity})
|
||||
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity,
|
||||
'practitioner_name': practitioner_doc.practitioner_name})
|
||||
|
||||
return slot_details
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "inpatient_record.patient",
|
||||
"fieldname": "patient",
|
||||
"fieldtype": "Link",
|
||||
"hide_days": 1,
|
||||
@@ -224,7 +223,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-30 16:53:13.076104",
|
||||
"modified": "2022-01-20 12:38:55.382621",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Sample Collection",
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "inpatient_record.patient",
|
||||
"fieldname": "patient",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
@@ -259,7 +258,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-17 22:23:24.632286",
|
||||
"modified": "2022-01-20 12:30:07.515185",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Healthcare",
|
||||
"name": "Vital Signs",
|
||||
|
||||
@@ -388,7 +388,7 @@ scheduler_events = {
|
||||
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
|
||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
||||
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
|
||||
"erpnext.non_profit.doctype.membership.membership.set_expired_status",
|
||||
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
|
||||
],
|
||||
"daily_long": [
|
||||
|
||||
@@ -12,14 +12,15 @@ class AppointmentLetter(Document):
|
||||
@frappe.whitelist()
|
||||
def get_appointment_letter_details(template):
|
||||
body = []
|
||||
intro= frappe.get_list("Appointment Letter Template",
|
||||
fields = ['introduction', 'closing_notes'],
|
||||
filters={'name': template
|
||||
})[0]
|
||||
content = frappe.get_list("Appointment Letter content",
|
||||
fields = ['title', 'description'],
|
||||
filters={'parent': template
|
||||
})
|
||||
intro = frappe.get_list('Appointment Letter Template',
|
||||
fields=['introduction', 'closing_notes'],
|
||||
filters={'name': template}
|
||||
)[0]
|
||||
content = frappe.get_all('Appointment Letter content',
|
||||
fields=['title', 'description'],
|
||||
filters={'parent': template},
|
||||
order_by='idx'
|
||||
)
|
||||
body.append(intro)
|
||||
body.append({'description': content})
|
||||
return body
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cstr, formatdate, get_datetime, getdate, nowdate
|
||||
from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
|
||||
|
||||
from erpnext.hr.utils import validate_active_employee
|
||||
from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
|
||||
|
||||
|
||||
class Attendance(Document):
|
||||
@@ -171,7 +171,7 @@ def get_month_map():
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unmarked_days(employee, month):
|
||||
def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||
import calendar
|
||||
month_map = get_month_map()
|
||||
|
||||
@@ -191,6 +191,11 @@ def get_unmarked_days(employee, month):
|
||||
])
|
||||
|
||||
marked_days = [get_datetime(record.attendance_date) for record in records]
|
||||
if cint(exclude_holidays):
|
||||
holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
|
||||
holidays = [get_datetime(record) for record in holiday_dates]
|
||||
marked_days.extend(holidays)
|
||||
|
||||
unmarked_days = []
|
||||
|
||||
for date in dates_of_month:
|
||||
|
||||
@@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = {
|
||||
onchange: function() {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||
dialog.set_df_property("status", "hidden", 1);
|
||||
dialog.set_df_property("exclude_holidays", "hidden", 1);
|
||||
dialog.set_df_property("month", "value", '');
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
@@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = {
|
||||
onchange: function() {
|
||||
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||
dialog.set_df_property("status", "hidden", 0);
|
||||
dialog.set_df_property("exclude_holidays", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
|
||||
me.get_multi_select_options(
|
||||
dialog.fields_dict.employee.value,
|
||||
dialog.fields_dict.month.value,
|
||||
dialog.fields_dict.exclude_holidays.get_value()
|
||||
).then(options => {
|
||||
if (options.length > 0) {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", options);
|
||||
@@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = {
|
||||
reqd: 1,
|
||||
|
||||
},
|
||||
{
|
||||
label: __("Exclude Holidays"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "exclude_holidays",
|
||||
hidden: 1,
|
||||
onchange: function() {
|
||||
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||
dialog.set_df_property("status", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", []);
|
||||
dialog.no_unmarked_days_left = false;
|
||||
me.get_multi_select_options(
|
||||
dialog.fields_dict.employee.value,
|
||||
dialog.fields_dict.month.value,
|
||||
dialog.fields_dict.exclude_holidays.get_value()
|
||||
).then(options => {
|
||||
if (options.length > 0) {
|
||||
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||
dialog.set_df_property("unmarked_days", "options", options);
|
||||
} else {
|
||||
dialog.no_unmarked_days_left = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: __("Unmarked Attendance for days"),
|
||||
fieldname: "unmarked_days",
|
||||
@@ -105,7 +136,7 @@ frappe.listview_settings['Attendance'] = {
|
||||
});
|
||||
},
|
||||
|
||||
get_multi_select_options: function(employee, month) {
|
||||
get_multi_select_options: function(employee, month, exclude_holidays) {
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
||||
@@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = {
|
||||
args: {
|
||||
employee: employee,
|
||||
month: month,
|
||||
exclude_holidays: exclude_holidays
|
||||
}
|
||||
}).then(r => {
|
||||
var options = [];
|
||||
|
||||
@@ -68,12 +68,18 @@ class Employee(NestedSet):
|
||||
self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name]))
|
||||
|
||||
def validate_user_details(self):
|
||||
data = frappe.db.get_value('User',
|
||||
self.user_id, ['enabled', 'user_image'], as_dict=1)
|
||||
if data.get("user_image") and self.image == '':
|
||||
self.image = data.get("user_image")
|
||||
self.validate_for_enabled_user_id(data.get("enabled", 0))
|
||||
self.validate_duplicate_user_id()
|
||||
if self.user_id:
|
||||
data = frappe.db.get_value("User",
|
||||
self.user_id, ["enabled", "user_image"], as_dict=1)
|
||||
|
||||
if not data:
|
||||
self.user_id = None
|
||||
return
|
||||
|
||||
if data.get("user_image") and self.image == "":
|
||||
self.image = data.get("user_image")
|
||||
self.validate_for_enabled_user_id(data.get("enabled", 0))
|
||||
self.validate_duplicate_user_id()
|
||||
|
||||
def update_nsm_model(self):
|
||||
frappe.utils.nestedset.update_nsm(self)
|
||||
|
||||
@@ -17,7 +17,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
def test_employee_onboarding_incomplete_task(self):
|
||||
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
|
||||
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
|
||||
_set_up()
|
||||
frappe.db.sql("delete from `tabEmployee Onboarding`")
|
||||
project = "Employee Onboarding : test@researcher.com"
|
||||
frappe.db.sql("delete from tabProject where name=%s", project)
|
||||
frappe.db.sql("delete from tabTask where project=%s", project)
|
||||
applicant = get_job_applicant()
|
||||
|
||||
job_offer = create_job_offer(job_applicant=applicant.name)
|
||||
@@ -42,7 +45,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
onboarding.submit()
|
||||
|
||||
project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
|
||||
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
|
||||
self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com')
|
||||
|
||||
# don't allow making employee if onboarding is not complete
|
||||
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
|
||||
@@ -65,8 +68,8 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
self.assertEqual(employee.employee_name, 'Test Researcher')
|
||||
|
||||
def get_job_applicant():
|
||||
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
|
||||
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
|
||||
if frappe.db.exists('Job Applicant', 'test@researcher.com'):
|
||||
return frappe.get_doc('Job Applicant', 'test@researcher.com')
|
||||
applicant = frappe.new_doc('Job Applicant')
|
||||
applicant.applicant_name = 'Test Researcher'
|
||||
applicant.email_id = 'test@researcher.com'
|
||||
|
||||
@@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", {
|
||||
['docstatus', '=', 1],
|
||||
['employee', '=', frm.doc.employee],
|
||||
['paid_amount', '>', 0],
|
||||
['paid_amount', '>', 'claimed_amount']
|
||||
['status', '!=', 'Claimed']
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,15 +10,17 @@ from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
|
||||
|
||||
test_records = frappe.get_test_records('Expense Claim')
|
||||
test_dependencies = ['Employee']
|
||||
company_name = '_Test Company 4'
|
||||
company_name = '_Test Company 3'
|
||||
|
||||
|
||||
class TestExpenseClaim(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_total_expense_claim_for_project(self):
|
||||
frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """)
|
||||
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
|
||||
frappe.db.sql("""delete from `tabTask`""")
|
||||
frappe.db.sql("""delete from `tabProject`""")
|
||||
frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
|
||||
|
||||
project = frappe.get_doc({
|
||||
@@ -37,12 +39,12 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
task_name = task.name
|
||||
payable_account = get_payable_account(company_name)
|
||||
|
||||
make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name)
|
||||
make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
|
||||
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
|
||||
|
||||
expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name)
|
||||
expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
|
||||
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700)
|
||||
@@ -54,7 +56,7 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
|
||||
def test_expense_claim_status(self):
|
||||
payable_account = get_payable_account(company_name)
|
||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4")
|
||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3")
|
||||
|
||||
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
|
||||
je = frappe.get_doc(je_dict)
|
||||
@@ -73,7 +75,7 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
def test_expense_claim_gl_entry(self):
|
||||
payable_account = get_payable_account(company_name)
|
||||
taxes = generate_taxes()
|
||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
|
||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3",
|
||||
do_not_submit=True, taxes=taxes)
|
||||
expense_claim.submit()
|
||||
|
||||
@@ -84,9 +86,9 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
expected_values = dict((d[0], d) for d in [
|
||||
['Output Tax CGST - _TC4',18.0, 0.0],
|
||||
['Output Tax CGST - _TC3',18.0, 0.0],
|
||||
[payable_account, 0.0, 218.0],
|
||||
["Travel Expenses - _TC4", 200.0, 0.0]
|
||||
["Travel Expenses - _TC3", 200.0, 0.0]
|
||||
])
|
||||
|
||||
for gle in gl_entries:
|
||||
@@ -102,7 +104,7 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
"payable_account": payable_account,
|
||||
"approval_status": "Rejected",
|
||||
"expenses":
|
||||
[{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }]
|
||||
[{"expense_type": "Travel", "default_account": "Travel Expenses - _TC3", "amount": 300, "sanctioned_amount": 200}]
|
||||
})
|
||||
expense_claim.submit()
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -192,10 +192,11 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-29 23:06:10.904260",
|
||||
"modified": "2022-01-12 16:28:53.196881",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Job Applicant",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -210,10 +211,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "applicant_name",
|
||||
"search_fields": "applicant_name, email_id, job_title, phone_number",
|
||||
"sender_field": "email_id",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"subject_field": "notes",
|
||||
"title_field": "applicant_name"
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
from erpnext.hr.doctype.interview.interview import get_interviewers
|
||||
@@ -21,10 +22,11 @@ class JobApplicant(Document):
|
||||
self.get("__onload").job_offer = job_offer[0].name
|
||||
|
||||
def autoname(self):
|
||||
keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
|
||||
if not keys:
|
||||
frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
|
||||
self.name = " - ".join(keys)
|
||||
self.name = self.email_id
|
||||
|
||||
# applicant can apply more than once for a different job title or reapply
|
||||
if frappe.db.exists("Job Applicant", self.name):
|
||||
self.name = append_number_if_name_exists("Job Applicant", self.name)
|
||||
|
||||
def validate(self):
|
||||
if self.email_id:
|
||||
|
||||
@@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
|
||||
|
||||
|
||||
class TestJobApplicant(unittest.TestCase):
|
||||
pass
|
||||
def test_job_applicant_naming(self):
|
||||
applicant = frappe.get_doc({
|
||||
"doctype": "Job Applicant",
|
||||
"status": "Open",
|
||||
"applicant_name": "_Test Applicant",
|
||||
"email_id": "job_applicant_naming@example.com"
|
||||
}).insert()
|
||||
self.assertEqual(applicant.name, 'job_applicant_naming@example.com')
|
||||
|
||||
applicant = frappe.get_doc({
|
||||
"doctype": "Job Applicant",
|
||||
"status": "Open",
|
||||
"applicant_name": "_Test Applicant",
|
||||
"email_id": "job_applicant_naming@example.com"
|
||||
}).insert()
|
||||
self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1')
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def create_job_applicant(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -5,6 +5,7 @@ import frappe
|
||||
from frappe.utils import add_days, add_months, getdate, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
|
||||
@@ -14,16 +15,19 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
def setUpClass(cls):
|
||||
frappe.db.sql("delete from `tabLeave Period`")
|
||||
|
||||
def test_overlapping_allocation(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
emp_id = make_employee("test_emp_leave_allocation@salary.com")
|
||||
cls.employee = frappe.get_doc("Employee", emp_id)
|
||||
|
||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_overlapping_allocation(self):
|
||||
leaves = [
|
||||
{
|
||||
"doctype": "Leave Allocation",
|
||||
"__islocal": 1,
|
||||
"employee": employee.name,
|
||||
"employee_name": employee.employee_name,
|
||||
"employee": self.employee.name,
|
||||
"employee_name": self.employee.employee_name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"from_date": getdate("2015-10-01"),
|
||||
"to_date": getdate("2015-10-31"),
|
||||
@@ -33,8 +37,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
{
|
||||
"doctype": "Leave Allocation",
|
||||
"__islocal": 1,
|
||||
"employee": employee.name,
|
||||
"employee_name": employee.employee_name,
|
||||
"employee": self.employee.name,
|
||||
"employee_name": self.employee.employee_name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"from_date": getdate("2015-09-01"),
|
||||
"to_date": getdate("2015-11-30"),
|
||||
@@ -46,40 +50,36 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save)
|
||||
|
||||
def test_invalid_period(self):
|
||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Leave Allocation",
|
||||
"__islocal": 1,
|
||||
"employee": employee.name,
|
||||
"employee_name": employee.employee_name,
|
||||
"employee": self.employee.name,
|
||||
"employee_name": self.employee.employee_name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"from_date": getdate("2015-09-30"),
|
||||
"to_date": getdate("2015-09-1"),
|
||||
"new_leaves_allocated": 5
|
||||
})
|
||||
|
||||
#invalid period
|
||||
# invalid period
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
|
||||
def test_allocated_leave_days_over_period(self):
|
||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Leave Allocation",
|
||||
"__islocal": 1,
|
||||
"employee": employee.name,
|
||||
"employee_name": employee.employee_name,
|
||||
"employee": self.employee.name,
|
||||
"employee_name": self.employee.employee_name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"from_date": getdate("2015-09-1"),
|
||||
"to_date": getdate("2015-09-30"),
|
||||
"new_leaves_allocated": 35
|
||||
})
|
||||
#allocated leave more than period
|
||||
|
||||
# allocated leave more than period
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
|
||||
def test_carry_forward_calculation(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
||||
leave_type.maximum_carry_forwarded_leaves = 10
|
||||
leave_type.max_leaves_allowed = 30
|
||||
@@ -87,6 +87,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
|
||||
# initial leave allocation = 15
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name,
|
||||
leave_type="_Test_CF_leave",
|
||||
from_date=add_months(nowdate(), -12),
|
||||
to_date=add_months(nowdate(), -1),
|
||||
@@ -96,6 +98,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
# carry forwarded leaves considering maximum_carry_forwarded_leaves
|
||||
# new_leaves = 15, carry_forwarded = 10
|
||||
leave_allocation_1 = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name,
|
||||
leave_type="_Test_CF_leave",
|
||||
carry_forward=1)
|
||||
leave_allocation_1.submit()
|
||||
@@ -107,6 +111,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
# carry forwarded leaves considering max_leave_allowed
|
||||
# max_leave_allowed = 30, new_leaves = 25, carry_forwarded = 5
|
||||
leave_allocation_2 = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name,
|
||||
leave_type="_Test_CF_leave",
|
||||
carry_forward=1,
|
||||
new_leaves_allocated=25)
|
||||
@@ -115,8 +121,6 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
self.assertEqual(leave_allocation_2.unused_leaves, 5)
|
||||
|
||||
def test_carry_forward_leaves_expiry(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
leave_type = create_leave_type(
|
||||
leave_type_name="_Test_CF_leave_expiry",
|
||||
is_carry_forward=1,
|
||||
@@ -125,6 +129,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
|
||||
# initial leave allocation
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name,
|
||||
leave_type="_Test_CF_leave_expiry",
|
||||
from_date=add_months(nowdate(), -24),
|
||||
to_date=add_months(nowdate(), -12),
|
||||
@@ -132,6 +138,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
leave_allocation.submit()
|
||||
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name,
|
||||
leave_type="_Test_CF_leave_expiry",
|
||||
from_date=add_days(nowdate(), -90),
|
||||
to_date=add_days(nowdate(), 100),
|
||||
@@ -143,6 +151,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
|
||||
# leave allocation with carry forward of only new leaves allocated
|
||||
leave_allocation_1 = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name,
|
||||
leave_type="_Test_CF_leave_expiry",
|
||||
carry_forward=1,
|
||||
from_date=add_months(nowdate(), 6),
|
||||
@@ -152,9 +162,10 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated)
|
||||
|
||||
def test_creation_of_leave_ledger_entry_on_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name
|
||||
)
|
||||
leave_allocation.submit()
|
||||
|
||||
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name))
|
||||
@@ -169,10 +180,10 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
|
||||
|
||||
def test_leave_addition_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name
|
||||
)
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
leave_allocation.new_leaves_allocated = 40
|
||||
@@ -180,44 +191,55 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
||||
|
||||
def test_leave_subtraction_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name
|
||||
)
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
leave_allocation.new_leaves_allocated = 10
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
||||
|
||||
def test_against_leave_application_validation_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
def test_validation_against_leave_application_after_submit(self):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||
|
||||
leave_allocation = create_leave_allocation()
|
||||
make_holiday_list()
|
||||
frappe.db.set_value("Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List")
|
||||
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name
|
||||
)
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||
|
||||
leave_application = frappe.get_doc({
|
||||
"doctype": 'Leave Application',
|
||||
"employee": employee.name,
|
||||
"employee": self.employee.name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"from_date": add_months(nowdate(), 2),
|
||||
"to_date": add_months(add_days(nowdate(), 10), 2),
|
||||
"company": erpnext.get_default_company() or "_Test Company",
|
||||
"company": self.employee.company,
|
||||
"docstatus": 1,
|
||||
"status": "Approved",
|
||||
"leave_approver": 'test@example.com'
|
||||
})
|
||||
leave_application.submit()
|
||||
leave_allocation.new_leaves_allocated = 8
|
||||
leave_allocation.total_leaves_allocated = 8
|
||||
leave_application.reload()
|
||||
|
||||
# allocate less leaves than the ones which are already approved
|
||||
leave_allocation.new_leaves_allocated = leave_application.total_leave_days - 1
|
||||
leave_allocation.total_leaves_allocated = leave_application.total_leave_days - 1
|
||||
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
|
||||
|
||||
def create_leave_allocation(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||
leave_allocation = frappe.get_doc({
|
||||
emp_id = make_employee("test_emp_leave_allocation@salary.com")
|
||||
employee = frappe.get_doc("Employee", emp_id)
|
||||
|
||||
return frappe.get_doc({
|
||||
"doctype": "Leave Allocation",
|
||||
"__islocal": 1,
|
||||
"employee": args.employee or employee.name,
|
||||
@@ -228,6 +250,5 @@ def create_leave_allocation(**args):
|
||||
"carry_forward": args.carry_forward or 0,
|
||||
"to_date": args.to_date or add_months(nowdate(), 12)
|
||||
})
|
||||
return leave_allocation
|
||||
|
||||
test_dependencies = ["Employee", "Leave Type"]
|
||||
|
||||
@@ -22,6 +22,7 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
||||
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
|
||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
|
||||
from erpnext.hr.utils import (
|
||||
get_holiday_dates_for_employee,
|
||||
get_leave_period,
|
||||
set_employee_name,
|
||||
share_doc_with_approver,
|
||||
@@ -159,33 +160,57 @@ class LeaveApplication(Document):
|
||||
.format(formatdate(future_allocation[0].from_date), future_allocation[0].name))
|
||||
|
||||
def update_attendance(self):
|
||||
if self.status == "Approved":
|
||||
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
|
||||
date = dt.strftime("%Y-%m-%d")
|
||||
status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
|
||||
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
|
||||
attendance_date = date, docstatus = ('!=', 2)))
|
||||
if self.status != "Approved":
|
||||
return
|
||||
|
||||
holiday_dates = []
|
||||
if not frappe.db.get_value("Leave Type", self.leave_type, "include_holiday"):
|
||||
holiday_dates = get_holiday_dates_for_employee(self.employee, self.from_date, self.to_date)
|
||||
|
||||
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
|
||||
date = dt.strftime("%Y-%m-%d")
|
||||
attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee,
|
||||
attendance_date = date, docstatus = ('!=', 2)))
|
||||
|
||||
# don't mark attendance for holidays
|
||||
# if leave type does not include holidays within leaves as leaves
|
||||
if date in holiday_dates:
|
||||
if attendance_name:
|
||||
# update existing attendance, change absent to on leave
|
||||
doc = frappe.get_doc('Attendance', attendance_name)
|
||||
if doc.status != status:
|
||||
doc.db_set('status', status)
|
||||
doc.db_set('leave_type', self.leave_type)
|
||||
doc.db_set('leave_application', self.name)
|
||||
else:
|
||||
# make new attendance and submit it
|
||||
doc = frappe.new_doc("Attendance")
|
||||
doc.employee = self.employee
|
||||
doc.employee_name = self.employee_name
|
||||
doc.attendance_date = date
|
||||
doc.company = self.company
|
||||
doc.leave_type = self.leave_type
|
||||
doc.leave_application = self.name
|
||||
doc.status = status
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.submit()
|
||||
# cancel and delete existing attendance for holidays
|
||||
attendance = frappe.get_doc("Attendance", attendance_name)
|
||||
attendance.flags.ignore_permissions = True
|
||||
if attendance.docstatus == 1:
|
||||
attendance.cancel()
|
||||
frappe.delete_doc("Attendance", attendance_name, force=1)
|
||||
continue
|
||||
|
||||
self.create_or_update_attendance(attendance_name, date)
|
||||
|
||||
def create_or_update_attendance(self, attendance_name, date):
|
||||
status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
|
||||
|
||||
if attendance_name:
|
||||
# update existing attendance, change absent to on leave
|
||||
doc = frappe.get_doc('Attendance', attendance_name)
|
||||
if doc.status != status:
|
||||
doc.db_set({
|
||||
'status': status,
|
||||
'leave_type': self.leave_type,
|
||||
'leave_application': self.name
|
||||
})
|
||||
else:
|
||||
# make new attendance and submit it
|
||||
doc = frappe.new_doc("Attendance")
|
||||
doc.employee = self.employee
|
||||
doc.employee_name = self.employee_name
|
||||
doc.attendance_date = date
|
||||
doc.company = self.company
|
||||
doc.leave_type = self.leave_type
|
||||
doc.leave_application = self.name
|
||||
doc.status = status
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.submit()
|
||||
|
||||
def cancel_attendance(self):
|
||||
if self.docstatus == 2:
|
||||
|
||||
@@ -5,7 +5,16 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.permissions import clear_user_permissions_for_doctype
|
||||
from frappe.utils import add_days, add_months, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_year_ending,
|
||||
get_year_start,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
@@ -19,6 +28,10 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
create_assignment_for_multiple_employees,
|
||||
)
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_holiday_list,
|
||||
make_leave_application,
|
||||
)
|
||||
|
||||
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
|
||||
|
||||
@@ -61,13 +74,15 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
|
||||
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
set_leave_approver()
|
||||
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.rollback()
|
||||
|
||||
def _clear_roles(self):
|
||||
frappe.db.sql("""delete from `tabHas Role` where parent in
|
||||
@@ -106,6 +121,72 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
|
||||
self.assertTrue(getdate(d) in dates)
|
||||
|
||||
def test_attendance_for_include_holidays(self):
|
||||
# Case 1: leave type with 'Include holidays within leaves as leaves' enabled
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
|
||||
leave_type = frappe.get_doc(dict(
|
||||
leave_type_name="Test Include Holidays",
|
||||
doctype="Leave Type",
|
||||
include_holiday=True
|
||||
)).insert()
|
||||
|
||||
date = getdate()
|
||||
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
|
||||
|
||||
holiday_list = make_holiday_list()
|
||||
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
|
||||
first_sunday = get_first_sunday(holiday_list)
|
||||
|
||||
leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
|
||||
leave_application.reload()
|
||||
self.assertEqual(leave_application.total_leave_days, 4)
|
||||
self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
|
||||
|
||||
leave_application.cancel()
|
||||
|
||||
def test_attendance_update_for_exclude_holidays(self):
|
||||
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
|
||||
leave_type = frappe.get_doc(dict(
|
||||
leave_type_name="Test Do Not Include Holidays",
|
||||
doctype="Leave Type",
|
||||
include_holiday=False
|
||||
)).insert()
|
||||
|
||||
date = getdate()
|
||||
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
|
||||
|
||||
holiday_list = make_holiday_list()
|
||||
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
|
||||
first_sunday = get_first_sunday(holiday_list)
|
||||
|
||||
# already marked attendance on a holiday should be deleted in this case
|
||||
config = {
|
||||
"doctype": "Attendance",
|
||||
"employee": "_T-Employee-00001",
|
||||
"status": "Present"
|
||||
}
|
||||
attendance_on_holiday = frappe.get_doc(config)
|
||||
attendance_on_holiday.attendance_date = first_sunday
|
||||
attendance_on_holiday.save()
|
||||
|
||||
# already marked attendance on a non-holiday should be updated
|
||||
attendance = frappe.get_doc(config)
|
||||
attendance.attendance_date = add_days(first_sunday, 3)
|
||||
attendance.save()
|
||||
|
||||
leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
|
||||
leave_application.reload()
|
||||
# holiday should be excluded while marking attendance
|
||||
self.assertEqual(leave_application.total_leave_days, 3)
|
||||
self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
|
||||
|
||||
# attendance on holiday deleted
|
||||
self.assertFalse(frappe.db.exists("Attendance", attendance_on_holiday.name))
|
||||
|
||||
# attendance on non-holiday updated
|
||||
self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
|
||||
|
||||
def test_block_list(self):
|
||||
self._clear_roles()
|
||||
|
||||
@@ -241,7 +322,13 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
leave_period = get_leave_period()
|
||||
today = nowdate()
|
||||
holiday_list = 'Test Holiday List for Optional Holiday'
|
||||
optional_leave_date = add_days(today, 7)
|
||||
employee = get_employee()
|
||||
|
||||
default_holiday_list = make_holiday_list()
|
||||
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list)
|
||||
first_sunday = get_first_sunday(default_holiday_list)
|
||||
|
||||
optional_leave_date = add_days(first_sunday, 1)
|
||||
|
||||
if not frappe.db.exists('Holiday List', holiday_list):
|
||||
frappe.get_doc(dict(
|
||||
@@ -253,7 +340,6 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
dict(holiday_date = optional_leave_date, description = 'Test')
|
||||
]
|
||||
)).insert()
|
||||
employee = get_employee()
|
||||
|
||||
frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list)
|
||||
leave_type = 'Test Optional Type'
|
||||
@@ -266,7 +352,7 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
|
||||
allocate_leaves(employee, leave_period, leave_type, 10)
|
||||
|
||||
date = add_days(today, 6)
|
||||
date = add_days(first_sunday, 2)
|
||||
|
||||
leave_application = frappe.get_doc(dict(
|
||||
doctype = 'Leave Application',
|
||||
@@ -636,13 +722,13 @@ def create_carry_forwarded_allocation(employee, leave_type):
|
||||
carry_forward=1)
|
||||
leave_allocation.submit()
|
||||
|
||||
def make_allocation_record(employee=None, leave_type=None):
|
||||
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
|
||||
allocation = frappe.get_doc({
|
||||
"doctype": "Leave Allocation",
|
||||
"employee": employee or "_T-Employee-00001",
|
||||
"leave_type": leave_type or "_Test Leave Type",
|
||||
"from_date": "2013-01-01",
|
||||
"to_date": "2019-12-31",
|
||||
"from_date": from_date or "2013-01-01",
|
||||
"to_date": to_date or "2019-12-31",
|
||||
"new_leaves_allocated": 30
|
||||
})
|
||||
|
||||
@@ -691,3 +777,16 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
|
||||
}).insert()
|
||||
|
||||
allocate_leave.submit()
|
||||
|
||||
|
||||
def get_first_sunday(holiday_list):
|
||||
month_start_date = get_first_day(nowdate())
|
||||
month_end_date = get_last_day(nowdate())
|
||||
first_sunday = frappe.db.sql("""
|
||||
select holiday_date from `tabHoliday`
|
||||
where parent = %s
|
||||
and holiday_date between %s and %s
|
||||
order by holiday_date
|
||||
""", (holiday_list, month_start_date, month_end_date))[0][0]
|
||||
|
||||
return first_sunday
|
||||
@@ -1,294 +1,108 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "HR-LPR-.YYYY.-.#####",
|
||||
"beta": 0,
|
||||
"creation": "2018-04-13 15:20:52.864288",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"from_date",
|
||||
"to_date",
|
||||
"is_active",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"optional_holiday_list"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "From Date",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "To Date",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "is_active",
|
||||
"fieldtype": "Check",
|
||||
"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": "Is Active",
|
||||
"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
|
||||
"label": "Is Active"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"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
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "optional_holiday_list",
|
||||
"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": "Holiday List for Optional Leave",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Holiday List",
|
||||
"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
|
||||
"options": "Holiday List"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-05-30 16:15:43.305502",
|
||||
"links": [],
|
||||
"modified": "2022-01-13 13:28:12.951025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Period",
|
||||
"name_case": "",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"search_fields": "from_date, to_date, company",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -113,10 +113,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-01 17:54:01.014509",
|
||||
"modified": "2022-01-13 13:37:11.218882",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Policy Assignment",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -164,5 +165,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "employee_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -57,9 +57,7 @@ class LeavePolicyAssignment(Document):
|
||||
leave_policy_detail.leave_type, leave_policy_detail.annual_allocation,
|
||||
leave_type_details, date_of_joining
|
||||
)
|
||||
|
||||
leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
|
||||
|
||||
leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
|
||||
self.db_set("leaves_allocated", 1)
|
||||
return leave_allocations
|
||||
|
||||
@@ -131,6 +129,8 @@ class LeavePolicyAssignment(Document):
|
||||
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
|
||||
leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
|
||||
new_leaves_allocated = monthly_earned_leave * months_passed
|
||||
else:
|
||||
new_leaves_allocated = 0
|
||||
|
||||
return new_leaves_allocated
|
||||
|
||||
|
||||
@@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
|
||||
if (cur_dialog.fields_dict.leave_period.value) {
|
||||
me.set_effective_date();
|
||||
}
|
||||
}
|
||||
},
|
||||
get_query() {
|
||||
let filters = {"is_active": 1};
|
||||
if (cur_dialog.fields_dict.company.value)
|
||||
filters["company"] = cur_dialog.fields_dict.company.value;
|
||||
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, get_first_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_employee,
|
||||
@@ -17,9 +18,8 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
test_dependencies = ["Employee"]
|
||||
|
||||
class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
|
||||
for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
|
||||
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
|
||||
|
||||
def test_grant_leaves(self):
|
||||
@@ -54,8 +54,8 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
|
||||
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
|
||||
self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
|
||||
self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
|
||||
self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
|
||||
self.assertEqual(getdate(leave_alloc_doc.from_date), getdate(leave_period.from_date))
|
||||
self.assertEqual(getdate(leave_alloc_doc.to_date), getdate(leave_period.to_date))
|
||||
self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
|
||||
self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
|
||||
|
||||
@@ -101,6 +101,55 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
# User are now allowed to grant leave
|
||||
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
|
||||
|
||||
def test_earned_leave_allocation(self):
|
||||
leave_period = create_leave_period("Test Earned Leave Period")
|
||||
employee = get_employee()
|
||||
leave_type = create_earned_leave_type("Test Earned Leave")
|
||||
|
||||
leave_policy = frappe.get_doc({
|
||||
"doctype": "Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
|
||||
}).insert()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name
|
||||
}
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
|
||||
leaves_allocated = frappe.db.get_value("Leave Allocation", {
|
||||
"leave_policy_assignment": leave_policy_assignments[0]
|
||||
}, "total_leaves_allocated")
|
||||
self.assertEqual(leaves_allocated, 0)
|
||||
|
||||
def tearDown(self):
|
||||
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
|
||||
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def create_earned_leave_type(leave_type):
|
||||
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
|
||||
|
||||
return frappe.get_doc(dict(
|
||||
leave_type_name=leave_type,
|
||||
doctype="Leave Type",
|
||||
is_earned_leave=1,
|
||||
earned_leave_frequency="Monthly",
|
||||
rounding=0.5,
|
||||
max_leaves_allowed=6
|
||||
)).insert()
|
||||
|
||||
|
||||
def create_leave_period(name):
|
||||
frappe.delete_doc_if_exists("Leave Period", name, force=1)
|
||||
start_date = get_first_day(getdate())
|
||||
|
||||
return frappe.get_doc(dict(
|
||||
name=name,
|
||||
doctype="Leave Period",
|
||||
from_date=start_date,
|
||||
to_date=add_months(start_date, 12),
|
||||
company="_Test Company",
|
||||
is_active=1
|
||||
)).insert()
|
||||
@@ -4,15 +4,32 @@
|
||||
frappe.ui.form.on('Shift Type', {
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(
|
||||
'Mark Attendance',
|
||||
() => frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'process_auto_attendance',
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
frappe.msgprint(__("Attendance has been marked as per employee check-ins"));
|
||||
__('Mark Attendance'),
|
||||
() => {
|
||||
if (!frm.doc.enable_auto_attendance) {
|
||||
frm.scroll_to_field('enable_auto_attendance');
|
||||
frappe.throw(__('Please Enable Auto Attendance and complete the setup first.'));
|
||||
}
|
||||
})
|
||||
|
||||
if (!frm.doc.process_attendance_after) {
|
||||
frm.scroll_to_field('process_attendance_after');
|
||||
frappe.throw(__('Please set {0}.', [__('Process Attendance After').bold()]));
|
||||
}
|
||||
|
||||
if (!frm.doc.last_sync_of_checkin) {
|
||||
frm.scroll_to_field('last_sync_of_checkin');
|
||||
frappe.throw(__('Please set {0}.', [__('Last Sync of Checkin').bold()]));
|
||||
}
|
||||
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'process_auto_attendance',
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
frappe.msgprint(__('Attendance has been marked as per employee check-ins'));
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ frappe.ui.form.on('Loan', {
|
||||
});
|
||||
});
|
||||
|
||||
$.each(["payment_account", "loan_account"], function (i, field) {
|
||||
$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
|
||||
frm.set_query(field, function () {
|
||||
return {
|
||||
"filters": {
|
||||
@@ -88,6 +88,10 @@ frappe.ui.form.on('Loan', {
|
||||
frm.add_custom_button(__('Loan Write Off'), function() {
|
||||
frm.trigger("make_loan_write_off_entry");
|
||||
},__('Create'));
|
||||
|
||||
frm.add_custom_button(__('Loan Refund'), function() {
|
||||
frm.trigger("make_loan_refund");
|
||||
},__('Create'));
|
||||
}
|
||||
}
|
||||
frm.trigger("toggle_fields");
|
||||
@@ -155,6 +159,21 @@ frappe.ui.form.on('Loan', {
|
||||
})
|
||||
},
|
||||
|
||||
make_loan_refund: function(frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
"loan": frm.doc.name
|
||||
},
|
||||
method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv",
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
let doc = frappe.model.sync(r.message)[0];
|
||||
frappe.set_route("Form", doc.doctype, doc.name);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
request_loan_closure: function(frm) {
|
||||
frappe.confirm(__("Do you really want to close this loan"),
|
||||
function() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user