Compare commits

..

164 Commits

Author SHA1 Message Date
Saqib
3a3a0a83b7 fix: link to error log list 2021-11-03 17:47:01 +05:30
Rohit Waghchaure
b8d75ff241 Merge branch 'version-13-pre-release' into version-13 2021-10-20 21:30:20 +05:30
Rohit Waghchaure
a3b7682935 bumped to version 13.13.0 2021-10-20 21:50:20 +05:50
rohitwaghchaure
200f6da8b2 Merge pull request #28040 from rohitwaghchaure/change-log-for-v13-13
chore: change log for v13.13.0
2021-10-20 21:23:39 +05:30
Rohit Waghchaure
7c9018f401 chore: change log for v13.13.0 2021-10-20 21:15:13 +05:30
Sagar Vora
ffadd671b7 fix: add mistakenly removed patches 2021-10-20 19:26:15 +05:30
mergify[bot]
47befa697d fix: incorrect status being set in Invoices (backport #28019) (#28030)
* fix: incorrect status being set in Invoices (#28019)

Co-authored-by: Pruthvi Patel <pruthvipatel145@gmail.com>
(cherry picked from commit 8d9d0987fe)

* fix: merge conflict

Co-authored-by: Sagar Vora <sagar@resilient.tech>
2021-10-20 19:24:19 +05:30
Jannat Patel
676c5280cc Merge pull request #28016 from frappe/mergify/bp/version-13-pre-release/pr-27728 2021-10-20 15:53:48 +05:30
Marica
e0decb0ae2 Merge pull request #28021 from frappe/mergify/bp/version-13-pre-release/pr-28005
fix: Fetch thumbnail from Item master instead of regenerating (backport #28005)
2021-10-20 15:14:30 +05:30
marination
b906cc20ae fix: Move thumbnail updation to different patch
- Thumbnail updation handled via different patch
- create_website_items will only have one purpose
- added progress bar to `create_website_items`
- code cleanup

(cherry picked from commit 348a961b53)
2021-10-20 09:12:33 +00:00
marination
c98421c69a fix: Check if thumbnail column exists in case of table trimming
(cherry picked from commit ac8014e24c)
2021-10-20 09:12:32 +00:00
marination
d7afb9ef65 fix: Get db values as dict when checking for thumbnail in existing web item
(cherry picked from commit 46a5a83789)
2021-10-20 09:12:32 +00:00
marination
a915b9cf72 fix: re-run patch
- Patch will just fetch thumbnails if website items are created, else it will create new website items

(cherry picked from commit 11c498d9e5)
2021-10-20 09:12:32 +00:00
marination
a022e01d3f fix: Fetch thumbnail from Item master instead of regenerating
(cherry picked from commit 94177c0764)
2021-10-20 09:12:31 +00:00
Goh Yan Chang
867cfa04b2 Update employee_leave_balance.py
fix: Employee Leave Balance report showing wrong figures
(cherry picked from commit 632f7848a3)
2021-10-20 06:58:12 +00:00
Jannat Patel
6e63dc1360 Merge pull request #28013 from frappe/mergify/bp/version-13-pre-release/pr-27904 2021-10-20 12:24:37 +05:30
Jannat Patel
698214bd59 fix: removed unused lines 2021-10-20 11:36:00 +05:30
Jannat Patel
873d166a4e fix: conflicts 2021-10-20 11:32:35 +05:30
Jannat Patel
9166d58717 fix: map missing fields in opportunity (#27904)
(cherry picked from commit d81f811349)

# Conflicts:
#	erpnext/crm/doctype/opportunity/opportunity.py
2021-10-20 05:56:59 +00:00
Deepesh Garg
7895d2a048 Merge pull request #28004 from frappe/mergify/bp/version-13-pre-release/pr-27867
fix: Totals row incorrect value in GL Entry (backport #27867)
2021-10-19 17:50:20 +05:30
Deepesh Garg
5a06ee9230 fix: Totals row incorrect value in GL Entry (#27867)
(cherry picked from commit ebe68c1a7a)
2021-10-19 09:39:02 +00:00
Noah Jacob
b6609d1649 Merge pull request #27998 from frappe/mergify/bp/version-13-pre-release/pr-27990
fix: changes in schedules gets overwritten on save (backport #27990)
2021-10-19 14:20:44 +05:30
Noah Jacob
9431bb9466 fix: changes in schedules gets overwritten on save
(cherry picked from commit af1b9e100e)
2021-10-19 08:07:42 +00:00
mergify[bot]
fdd9cc76be fix: flaky Org Chart Test (#27971) (#27989)
(cherry picked from commit 8eacaddde7)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-10-18 11:47:22 +05:30
Deepesh Garg
f8348ab681 Merge pull request #27983 from frappe/mergify/bp/version-13-pre-release/pr-27967
fix: Account number and name incorrectly imported using COA importer (backport #27967)
2021-10-18 11:04:39 +05:30
Deepesh Garg
eecfb25c90 Merge pull request #27981 from frappe/mergify/bp/version-13-pre-release/pr-27934
fix: TDS round off not working from second transaction (backport #27934)
2021-10-18 11:03:00 +05:30
Deepesh Garg
41a0e12954 Merge pull request #27979 from frappe/mergify/bp/version-13-pre-release/pr-27970
fix (India): Interstate internal transfer invoices not visible in GSTR-1 (backport #27970)
2021-10-18 11:01:06 +05:30
Ankush Menat
f0383289d8 Merge pull request #27986 from frappe/mergify/bp/version-13-pre-release/pr-27962
fix: Retain space inside Serial no string while cleaning serial nos (backport #27962)
2021-10-18 10:34:31 +05:30
Jannat Patel
46209023ce Merge pull request #27985 from frappe/mergify/bp/version-13-pre-release/pr-27850 2021-10-18 10:32:48 +05:30
marination
e69bd39cdd test: Include serial no with spaces in it in sanitation test
(cherry picked from commit a9341672cf)
2021-10-18 04:56:11 +00:00
Marica
0fcb3cd918 fix: Use strip instead of lstrip and rstrip
Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
(cherry picked from commit 8cf188d9c0)
2021-10-18 04:56:10 +00:00
marination
44ab131792 fix: Retain space inside Serial no string while cleaning serial nos
(cherry picked from commit 41035b0330)
2021-10-18 04:56:10 +00:00
pateljannat
b648d77316 fix: exclude inactive employees from auto attendance
(cherry picked from commit 921b4be348)
2021-10-18 04:35:18 +00:00
Deepesh Garg
0012f0b2da fix: Account number and name incorrectly import using COA importer
(cherry picked from commit 17a8649500)
2021-10-18 03:47:22 +00:00
Deepesh Garg
1796f09c0f fix: TDS round off not working from second transaction
(cherry picked from commit b7a08535b5)
2021-10-18 03:24:46 +00:00
Deepesh Garg
64b58b148f fix: TDS round off not working from second transaction
(cherry picked from commit ca0067212d)
2021-10-18 03:24:46 +00:00
Deepesh Garg
4415bf9968 fix: Interstate internal transfer invoices not visible in GSTR-1
(cherry picked from commit d9d42b13ab)
2021-10-18 03:23:55 +00:00
rohitwaghchaure
1b7d94d70d Merge pull request #27961 from rohitwaghchaure/merge-13-hotfix-to-pre-release-for-13-13
chore: Merge branch 'version-13-hotfix' into 'version-13-pre-release'
2021-10-14 19:17:22 +05:30
Rohit Waghchaure
952c60b3f5 Merge branch 'version-13-hotfix' into 'version-13-pre-release' 2021-10-14 18:31:09 +05:30
rohitwaghchaure
8c33103838 Merge pull request #27958 from frappe/mergify/bp/version-13-hotfix/pr-27954
fix: value_after_depreciation calculation (backport #27954)
2021-10-14 18:25:57 +05:30
mergify[bot]
a8c966eb25 fix: patch to enable scheduled job for reposting (backport #27957)
* fix: patch to enable scheduled job for reposting

(cherry picked from commit efc60ec2b5)

# Conflicts:
#	erpnext/patches.txt

* chore: formatting

(cherry picked from commit 3f97413814)

* Update enable_scheduler_job_for_item_reposting.py

(cherry picked from commit 230a5d4b39)

* fix: resolve conflict

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2021-10-14 17:31:51 +05:30
Saqib
88b1c1c87e fix: value_after_depreciation calculation (#27954)
(cherry picked from commit 1f70dd6e98)
2021-10-14 10:53:52 +00:00
Marica
771213c415 Merge pull request #27955 from frappe/mergify/bp/version-13-hotfix/pr-27947
fix: Improve error message for Serial No mismatch between SI and DN (backport #27947)
2021-10-14 14:24:54 +05:30
marination
d86f5ec1ba fix: Remove trailing space and line break in translatable string
(cherry picked from commit 60f35ad8a2)
2021-10-14 08:19:54 +00:00
marination
a568fc7924 fix: Improve error message for Serial No mismatch between SI and DN
(cherry picked from commit 646acb6b46)
2021-10-14 08:19:53 +00:00
mergify[bot]
c040256793 fix: cannot add deductions in internal transfer payment entry (backport #27545) (#27930)
* fix: cannot add deductions in internal transfer payment entry

(cherry picked from commit 1b7414e948)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.py

* fix: Update message string

(cherry picked from commit 3b9514d6e1)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.py

* fix: conflicts

Co-authored-by: Saqib Ansari <nextchamp.saqib@gmail.com>
Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
2021-10-14 12:34:03 +05:30
Chillar Anand
29996ee726 fix(hr): Update expense account after company is updated (#27843) (#27919)
(cherry picked from commit f0c4ea14a9)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-10-14 11:13:00 +05:30
Deepesh Garg
1120506e11 Merge pull request #27941 from frappe/mergify/bp/version-13-hotfix/pr-27783
fix(Subscription): reorder updation of end date (backport #27783)
2021-10-13 16:56:07 +05:30
mergify[bot]
e64751e3a2 fix: not authorized to update entries after freezing accounts (backport #27937)
* fix: not authorized to update entries after freezing accounts (#27937)

* fix: not authorized to update entries after freezing accounts

* fix: Add test case

* fix(patch): patched to requeue failed reposts(check_freezing_date)

* chore: misc fixes

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
(cherry picked from commit 2bb383b178)

# Conflicts:
#	erpnext/patches.txt

* fix: resolve conflict

Co-authored-by: Noah Jacob <noahjacobkurian@gmail.com>
Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-13 16:41:21 +05:30
Himanshu
bf47c6836e Update subscription.py
(cherry picked from commit 44306bd0e5)
2021-10-13 09:52:21 +00:00
hrwx
e80192e2da fix: create past invoices
(cherry picked from commit ae657c7e4e)
2021-10-13 09:52:21 +00:00
Sagar Vora
b2f1b02e34 test: use test_dependencies instead of duplication
(cherry picked from commit 656015d99d)
2021-10-13 09:52:20 +00:00
Sagar Vora
ed090f2e3e fix: remove newline
(cherry picked from commit fc375c5bde)
2021-10-13 09:52:20 +00:00
hrwx
d796172249 fix: reorder updation of end date
(cherry picked from commit 0f03b19109)
2021-10-13 09:52:20 +00:00
Deepesh Garg
ff9f6366ad Merge pull request #27935 from frappe/mergify/bp/version-13-hotfix/pr-27907
feat: HSN based tax breakup table check in GST Settings  (backport #27907)
2021-10-13 12:56:00 +05:30
Deepesh Garg
b8683d5532 Merge pull request #27925 from frappe/mergify/bp/version-13-hotfix/pr-27758
perf: Add indexes in stock queries and speed up bin updation (backport #27758)
2021-10-13 12:08:38 +05:30
Subin Tom
4a156cdc2e added new section in gst settings page
(cherry picked from commit fc4facc5dc)
2021-10-13 06:38:27 +00:00
Subin Tom
771b076448 feat: HSN wise tax breakup check in GST Settings
(cherry picked from commit 530de12b07)
2021-10-13 06:38:26 +00:00
mergify[bot]
e6346ac982 fix: minor ux fixes in Sales & Purchase Invoice (backport #27927) (#27932)
* fix: keeping sections consistent across sales & purchase invoice

(cherry picked from commit 2bc1ca993a)

* fix: set collapsible & print hide

(cherry picked from commit d181cc42a1)

Co-authored-by: Anuja Pawar <anuja.pawar20@gmail.com>
2021-10-13 11:01:21 +05:30
Ankush Menat
547e173fe0 ci: rule to fail PRs that add a new manual commit (#27928)
Manual commits are frequent source of bugs, confusions or undefined
behaviour.

All new manual commits should be explcitly ignored with explanation on
why it's added. This will only fail for new additions. Existing ones
need to be cleaned up manually.

(cherry picked from commit 06b426e9c3)
2021-10-12 23:05:09 +05:30
mergify[bot]
37bd0ecf87 fix: force reload custom field doctype (#27909) (#27910)
custom_field.json has the same modified key in both versions but not the same content. This can happen again if something is backported, safe solution is to force reload.

(cherry picked from commit ad444153cc)

Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-12 20:38:09 +05:30
Marica
8244980d79 Merge pull request #27924 from marination/item-configure-empty-popup
fix: Item Variant selection empty popup on website
2021-10-12 20:32:18 +05:30
Deepesh Garg
10b239ec50 perf: Add indexes in stock queries and speed up bin updation #27758
perf: Add indexes in stock queries and speed up bin updation
(cherry picked from commit 6f107da165)
2021-10-12 14:46:26 +00:00
marination
c9c4a9995b fix: Item Variant selection empty popup on website
- pass item_code instead of website item name to fetch attributes
2021-10-12 19:56:12 +05:30
Ankush Menat
b6dc71679e fix: remove transaction commit from tests
(cherry picked from commit 8d69ec72a6)
2021-10-12 16:41:48 +05:30
Ankush Menat
5bdb6041b9 refactor: rollback after full test
(cherry picked from commit acdb26a4bb)
2021-10-12 16:41:48 +05:30
Ankush Menat
89828defc5 test: add custom TestCase class and use in stock
(cherry picked from commit 06fa35a9c1)
2021-10-12 16:41:48 +05:30
Deepesh Garg
816236b587 Merge pull request #27918 from frappe/mergify/bp/version-13-hotfix/pr-27884
fix: patch fails if accounts are frozen (backport #27884)
2021-10-12 15:04:01 +05:30
Deepesh Garg
69f17721ef Merge pull request #27917 from frappe/mergify/bp/version-13-hotfix/pr-27896
fix: Status check for closed loans (backport #27896)
2021-10-12 15:03:31 +05:30
Afshan
125bb1f99a Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-27884 2021-10-12 14:32:36 +05:30
mergify[bot]
6f786b42a9 fix: add cost center in gl entry for advance payment entry (#27840) (#27915)
(cherry picked from commit 569dc5f6b1)

Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
2021-10-12 14:31:50 +05:30
Saqib Ansari
fed80177de fix: rollback on exception
(cherry picked from commit c103f72fad)
2021-10-12 08:15:17 +00:00
Saqib Ansari
2ce36d1edc feat: handle exceptions
(cherry picked from commit 353ad5f6ff)
2021-10-12 08:15:17 +00:00
Saqib Ansari
7e44c30404 fix: patch fails if accounts are frozen
(cherry picked from commit b0aa4a6e1c)
2021-10-12 08:15:16 +00:00
Deepesh Garg
47ced6810f fix: Linting issues
(cherry picked from commit af14ba43de)
2021-10-12 08:11:47 +00:00
Deepesh Garg
6c3f5687f2 fix: Incorrect maximum loan amount update
(cherry picked from commit 8355af6dcf)
2021-10-12 08:11:47 +00:00
Deepesh Garg
1b632b683f fix: Status check for closed loans
(cherry picked from commit 3337ae120c)
2021-10-12 08:11:46 +00:00
mergify[bot]
c5660e8511 Merge pull request #27906 from Anuja-pawar/accounts-settings (#27912)
fix(Accounts Settings): Update label

(cherry picked from commit e1967870a9)

Co-authored-by: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com>
2021-10-12 12:23:53 +05:30
mergify[bot]
c4338d184e fix(accounts): Fix issue with fetching loyalty point entries (#27892) (#27913)
(cherry picked from commit 401e22fb8d)

Co-authored-by: Chillar Anand <chillar@avilpage.com>
2021-10-12 12:23:32 +05:30
Noah Jacob
d598a61556 Merge pull request #27901 from frappe/mergify/bp/version-13-hotfix/pr-27800
refactor: updated buying onboarding tours. (backport #27800)
2021-10-11 16:33:59 +05:30
Noah Jacob
d262d0ac27 refactor: updated onboarding cards and tours
(cherry picked from commit f5e0cad6a1)
2021-10-11 10:04:40 +00:00
mergify[bot]
7dc2f95932 fix: v12 migrate error - unknown column ‘mandatory_depends_on’ (backport #27897) (#27900)
* fix: v12 migrate error - unknown column ‘mandatory_depends_on’ (#27897)

* fix: v12 doesn't have mandatory_depends_on field

* fix: move update_vehicle_no_reqd_condition to v13

* fix: move update_vehicle_no_reqd_condition to v13

* fix: file name missing .py

* refactor!: add back empty line

* fix: linters issue

(cherry picked from commit 7acdcc70ad)

# Conflicts:
#	erpnext/patches.txt

* fix: resolve conflicts

Co-authored-by: Dany Robert <rtdany10@gmail.com>
Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-11 10:02:31 +00:00
rohitwaghchaure
cf87c9138e Merge pull request #27898 from frappe/mergify/bp/version-13-hotfix/pr-27863
fix: consolidated report not consider company currency (backport #27863)
2021-10-11 14:07:51 +05:30
Rohit Waghchaure
1a42f82d14 fix: opening balance to calculate 'Unclosed Fiscal Years Profit / Loss (Credit)'
(cherry picked from commit 19d14da0d4)
2021-10-11 08:37:27 +00:00
Rohit Waghchaure
2c5a0bff47 fix: consolidated report not consider company currency
(cherry picked from commit dc4206428d)
2021-10-11 08:37:26 +00:00
mergify[bot]
a831e6b552 fix: bom item query (backport #27890) (#27894)
* fix: bom item query #27890

fix: bom item query
(cherry picked from commit 0a3dd3e954)

# Conflicts:
#	erpnext/manufacturing/doctype/bom/test_bom.py

* fix: resolve conflict

* chore: unused imports

[skip ci]

Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-11 12:45:24 +05:30
ChillarAnand
f844c36ab2 fix(CI): Use bugbear instead of flake8-mutable
(cherry picked from commit 4dc17a856e)
2021-10-11 11:23:22 +05:30
mergify[bot]
091c2f3023 fix(perf): index creation on voucher_detail_no (#27866) (#27875)
voucher_detail_no is supposed to have an index, it was added on
on_doctype_update function of table, however this function is only
called if DocType itself is updated and `on_update` is called on
DocType. Stock ledger Entry doctype hasn't changed since addition of
this index in function.

Before: Lack of this index was causing full table scan in
get_future_sle_to_fix function. (~50 seconds in a reposting job)

After: Single row is fetched (~0.5 second in full reposting job)

Learnings:
1. Add simple indexes via doctype only
2. For complex indexes always change doctype.json file for it to take
   effect.

(cherry picked from commit 6019f60d0a)

Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-08 17:19:41 +05:30
mergify[bot]
119c2f01e1 fix(perf): index creation on voucher_detail_no (#27866) (#27876)
voucher_detail_no is supposed to have an index, it was added on
on_doctype_update function of table, however this function is only
called if DocType itself is updated and `on_update` is called on
DocType. Stock ledger Entry doctype hasn't changed since addition of
this index in function.

Before: Lack of this index was causing full table scan in
get_future_sle_to_fix function. (~50 seconds in a reposting job)

After: Single row is fetched (~0.5 second in full reposting job)

Learnings:
1. Add simple indexes via doctype only
2. For complex indexes always change doctype.json file for it to take
   effect.

(cherry picked from commit 6019f60d0a)

Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-08 17:19:34 +05:30
gavin
40aac908d1 fix: Ignore mandatory fields if exist (#27871)
The goal of this fix is to not break the patch in case of customizations
In this particular case, it's regarding a customized Note DocType with
multiple custom mandatory fields
2021-10-08 15:10:03 +05:30
mergify[bot]
4e6d588ae1 fix: remove readonly from billing address (#27873)
(cherry picked from commit 41fefa356f)

Co-authored-by: 18alantom <2.alan.tom@gmail.com>
2021-10-08 15:08:45 +05:30
mergify[bot]
504f2f06d3 fix: Salary Slip Label fixes (backport #27865) (#27870)
* fix: Salary Slip Label fixes (#27865)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
(cherry picked from commit d3d4a3da62)

# Conflicts:
#	erpnext/payroll/doctype/salary_slip/salary_slip.json

* fix: conflicts

Co-authored-by: yadavyk <32797974+yadavyk@users.noreply.github.com>
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-10-08 14:16:12 +05:30
mergify[bot]
d96fd60878 fix: update dead links in help_links.js (#27860) (#27868)
(cherry picked from commit 90a249527d)

Co-authored-by: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com>
2021-10-08 13:31:07 +05:30
Deepesh Garg
d9a219850a fix: SO delivery Date not getting set via data import (#27862)
* fix: SO delivery Date not getting set via data import

* fix: logic to add delivery dates

* fix: linting issue

Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
Co-authored-by: Afshan <afshan13k@gmail.com>
2021-10-08 12:38:40 +05:30
mergify[bot]
cb6d884058 fix(Payment Reconciliation): minor ux fixes (#27779) (#27859)
* fix: minor fixes

* fix: Linters check

* fix: sider check

* fix: kept unallocated payment amount hidden in allocation

* fix: removed Add row button from the tables (redundant)

Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
Co-authored-by: Saqib <nextchamp.saqib@gmail.com>
(cherry picked from commit 5cc3ea0aa7)

Co-authored-by: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com>
2021-10-07 22:47:05 +05:30
mergify[bot]
4102f799dc fix: trim sales invoice custom field lengths (backport #27665) (#27750)
* fix: trim sales invoice custom field lengths

(cherry picked from commit a7df4227da)

* patch: trim sales invoice custom field lengths

(cherry picked from commit f1fcb385f5)

# Conflicts:
#	erpnext/patches.txt

* fix: do not set length for date field

(cherry picked from commit 83cc597594)

* fix: merge conflicts

Co-authored-by: Saqib Ansari <nextchamp.saqib@gmail.com>
Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
2021-10-07 20:59:07 +05:30
Subin Tom
3c53c5b660 fix: cancelled sales invoices are considered in billed quantity calculation (#27845)
Co-authored-by: Saqib Ansari <nextchamp.saqib@gmail.com>
Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
2021-10-07 20:36:00 +05:30
mergify[bot]
0a4b3d8129 fix: help links for purchase cycle and JV (#27856) (#27858)
(cherry picked from commit 07c680d7cc)

Co-authored-by: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com>
2021-10-07 20:29:28 +05:30
mergify[bot]
bad489426a feat: (Stock Reco) Ignore Empty Stock while fetching items from warehouse (#27848)
- Added checkbox to `Fetch Items from Warehouse` dialog to ignore empty stock
- fix: Items fetched twice due to Item Defaults
- Improved code readability

(cherry picked from commit 533ee9a401)

Co-authored-by: marination <maricadsouza221197@gmail.com>
2021-10-07 20:28:08 +05:30
mergify[bot]
729e29d268 fix: update help links for Sales Invoice page (#27853) (#27854)
(cherry picked from commit 646fd29f0e)

Co-authored-by: Kenneth Sequeira <33246109+kennethsequeira@users.noreply.github.com>
2021-10-07 18:42:06 +05:30
rohitwaghchaure
8108d4761b Merge pull request #27852 from frappe/mergify/bp/version-13-hotfix/pr-27851
feat: option to set the width for the multi-select dialog box (backport #27851)
2021-10-07 17:39:20 +05:30
Rohit Waghchaure
00cb04df84 feat: option to set the width for the multi-select dialog box
(cherry picked from commit 69ffddf747)
2021-10-07 12:08:58 +00:00
mergify[bot]
dd0cefbeb9 refactor: Clean up mutable defaults and add CI check (#27828) (#27841)
* refactor: Clean up mutable defaults and add CI check

(cherry picked from commit 772d4753e7)

Co-authored-by: Chillar Anand <chillar@avilpage.com>
2021-10-07 09:57:35 +00:00
Anoop
4da5cb36e7 Merge pull request #27812 from akurungadam/fix-appointment-slots
fix(healthcare): Availability slots display, disabled Practitioner Schedule
2021-10-07 08:45:56 +05:30
mergify[bot]
56b58cbeea fix: use ceil in case of whole uoms for reorder qty (#27834) (#27838)
* fix: use ceil in case of whole uoms for reorder qty

* fix: cache uom query

(cherry picked from commit d4b2471cea)

Co-authored-by: Alan <2.alan.tom@gmail.com>
2021-10-06 18:54:22 +05:30
Marica
71f676eedd Merge pull request #27764 from frappe/mergify/bp/version-13-hotfix/pr-27661
refactor: fetching of account balance in chart of accounts (backport #27661)
2021-10-06 12:56:10 +05:30
mergify[bot]
e57037b4cf ci: fail build if asset bundling fails (#27820) (#27823)
(cherry picked from commit 35e30bdcaf)

Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-05 23:59:47 +05:30
mergify[bot]
2cbd5a9fcf fix: removed redundant piece of code (#27817) (#27821)
(cherry picked from commit cec66d2d10)

Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
2021-10-05 19:26:58 +05:30
Noah Jacob
47a7eeca54 Merge pull request #27814 from frappe/mergify/bp/version-13-hotfix/pr-27813
fix: ignore random periodicity in validations (backport #27813)
2021-10-05 17:30:00 +05:30
Ankush Menat
62bbf0fe45 fix: ignore random periodicity in validations
(cherry picked from commit 3d3655ed73)
2021-10-05 11:28:34 +00:00
Marica
ae8c1ae311 Merge pull request #27803 from frappe/mergify/bp/version-13-hotfix/pr-27660
fix: tax rate being overridden in case of 0.00 (backport #27660)
2021-10-05 16:44:16 +05:30
Deepesh Garg
8f5ab94b70 Merge pull request #27792 from deepeshgarg007/pre-release-fixes
fix: COA Importer showing blank validations
2021-10-05 15:18:04 +05:30
Deepesh Garg
9507b2d752 fix: Use get_list instead of get_all to avoid perm issues 2021-10-05 14:51:35 +05:30
mergify[bot]
7e018f94ce fix: batch_no not mapped from PR to Stock Entry (#27804)
(cherry picked from commit 9613af6c4e)

Co-authored-by: Noah Jacob <noahjacobkurian@gmail.com>
2021-10-05 14:39:36 +05:30
Ankush Menat
2cfafede44 fix(ux): use toast instead of popup 2021-10-05 14:27:09 +05:30
Ankush Menat
df1f8fddf6 fix: using DN for transfer w/o internal customer (#27798)
This used to be work before though not "advertised", since a lot of
users have started using it as feature, it can't be broken now.
2021-10-05 14:21:27 +05:30
Dany Robert
a17fed9cd9 fix: return tax rate since fetch is removed
(cherry picked from commit 2b4959fb3b)
2021-10-05 08:45:24 +00:00
Dany Robert
d8479a41e5 fix: tax rate being overridden in case of 0.00
Tax rate could be different for different expenses.
Therefore, rate is kept as 0.00 and tax amount entered manually.
But fetching used to override the rate(upon saving) and mess up the amount.

(cherry picked from commit 5ce6a4c107)
2021-10-05 08:45:23 +00:00
Deepesh Garg
d2f5d31f98 Merge pull request #27559 from frappe-pr-bot/backport/version-13-hotfix/27524
fix: Tax breakup based on items, missing GST fields
2021-10-05 14:13:50 +05:30
Deepesh Garg
44ee44dec5 Merge pull request #27786 from frappe/mergify/bp/version-13-hotfix/pr-27785
fix: Delete linked Transaction Deletion Record docs on deleting company (backport #27785)
2021-10-05 14:09:16 +05:30
mergify[bot]
bebd77c27d fix: add (uom, brand) Item details in an Item Price (#27561) (#27795)
* fix: add (uom, brand) and update (uom) Item details in an Item Price

* fix: order of query interpolation args

Co-authored-by: Marica <maricadsouza221197@gmail.com>

* fix: named interpolation, remove item price

* fix: sql error

Co-authored-by: Marica <maricadsouza221197@gmail.com>
Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
(cherry picked from commit 7da777880b)

Co-authored-by: Alan <2.alan.tom@gmail.com>
2021-10-05 13:28:43 +05:30
Afshan
0c55a98190 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-27785 2021-10-05 13:26:47 +05:30
mergify[bot]
32d72fdecb fix: Only calculate first_respone_time if SLA is set (#27789) (#27793)
Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
(cherry picked from commit ad03eb25df)

Co-authored-by: Ganga Manoj <ganga.manoj98@gmail.com>
2021-10-05 12:49:06 +05:30
mergify[bot]
046ec928e0 fix: Display appropriate message if different Payment Terms are used in PE and its Payment References (#27763)
(cherry picked from commit 9f14695743)

Co-authored-by: GangaManoj <ganga.manoj98@gmail.com>
Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com>
2021-10-05 12:21:21 +05:30
Deepesh Garg
0660d6ed01 fix: COA Importer showing blank validations 2021-10-05 12:20:14 +05:30
Afshan
877820b902 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-27785 2021-10-05 11:50:25 +05:30
mergify[bot]
ab0e381cfc fix(asset): expected value after useful life validation (#27539) (#27790)
(cherry picked from commit 065a2ce983)

Co-authored-by: Saqib <nextchamp.saqib@gmail.com>
2021-10-05 11:47:56 +05:30
Saqib
5e34cdf00f Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-27661 2021-10-05 11:34:02 +05:30
GangaManoj
4ecb798585 fix: Delete linked Transaction Deletion Record docs on deleting company
(cherry picked from commit 38c7e42f0c)
2021-10-05 05:19:37 +00:00
mergify[bot]
91d269fe1a fix: set item uom as stock_uom if it isn't set (#27623) (#27780)
* fix: set item uom as stock_uom if it isn't set

(cherry picked from commit 5c372202d5)

Co-authored-by: Alan <2.alan.tom@gmail.com>
2021-10-04 22:41:12 +05:30
mergify[bot]
4535a9415f ci(Mergify): configuration update (#27777) (#27778)
Signed-off-by: Ankush Menat <me@ankush.dev>
(cherry picked from commit 4159361d52)

Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-04 18:10:52 +05:30
Sagar Vora
e17713c9d6 fix: multiple fixes to timesheets (#27775) 2021-10-04 17:08:50 +05:30
mergify[bot]
21a5498d5d fix: Merge "Accounting Ledger" and "Accounts Receivable" in "View" button (#27769) (#27771)
* fix: Added a new button "View" and merged "Accounting Ledger" and "Accounts Receivable" into it

* fix: sider issues

* chore: dead code

(cherry picked from commit b483f173a6)

Co-authored-by: Komal-Saraf0609 <81952590+Komal-Saraf0609@users.noreply.github.com>
2021-10-04 16:06:55 +05:30
Saqib
c0b17edbbf perf: fetching of account balance in chart of accounts (#27661)
(cherry picked from commit 9051735529)
2021-10-04 06:15:34 +00:00
Marica
e9ed379b57 Merge pull request #27743 from frappe/mergify/bp/version-13-hotfix/pr-27611
fix: Hero Slider Control & Alignment fixes (backport #27611)
2021-10-03 14:29:18 +05:30
Marica
4d0d642db7 Merge pull request #27720 from marination/e-comm-web-item-name-thumbnail-fix
fix: Website Items with same Item name unhandled, thumbnails missing
2021-10-03 14:26:20 +05:30
Deepesh Garg
f02438eb54 Merge pull request #27712 from deepeshgarg007/internal_transfer_check_fix
fix(India): Internal transfer check fix
2021-10-03 13:38:51 +05:30
Marica
62fa1f0305 Merge branch 'version-13-hotfix' into e-comm-web-item-name-thumbnail-fix 2021-10-03 13:30:50 +05:30
marination
77d4849ce8 fix: Pre-commit formatting 2021-10-03 13:30:02 +05:30
Deepesh Garg
fe4df3a14a Merge pull request #27748 from deepeshgarg007/yet_chart_of_accounts_importer_fixes
fix: Chart Of Accounts import button not visible
2021-10-02 21:39:07 +05:30
Deepesh Garg
ff570f48a0 fix: Linting issues 2021-10-02 20:46:20 +05:30
Deepesh Garg
e4b89d2fcd fix: Remove unwanted comments 2021-10-02 20:37:15 +05:30
Deepesh Garg
3529622a0d fix: Chart Of Accounts import button not visible 2021-10-02 20:35:11 +05:30
Shariq Ansari
8f98238114 chore: linter fix
(cherry picked from commit 18918e1b4f)
2021-10-02 11:38:31 +00:00
Shariq Ansari
86e3adf344 fix: Fixed alignment of Title, Subtitle, Action Button
(cherry picked from commit 0de735f20b)
2021-10-02 11:38:31 +00:00
Shariq Ansari
d6152df3b4 fix: Creating unique hash for slider id instead of slider name
(cherry picked from commit 3e8e6ac4e2)
2021-10-02 11:38:30 +00:00
Anurag Mishra
4837238f3d feat(HR): Some Enhancements and Onboarding (#25741)
* feat: Hr settings restructure

* feat: remove validation and make As warning

* feat: made leave policy Assignment feild read only

* feat: send leave Notification via 'Notification'

* patch: for field name change

* feat: removed defaults value for removed field

* feat: removed leave Notification fields

* feat: better label and description

* feat: Hr Module onboarding and Onboarding slides

* fix: sider, test, translations

* chore: remove unnecessary code formatting changes

* refactor: HR Onboarding

* refactor: HR Settings

* revert: Notification changes

* chore: remove unnecessary descriptions from leave type

* fix: linter issues

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-10-02 12:34:21 +05:30
mergify[bot]
e4f12f0458 fix: update variant qty in BOM, Create Work Order dialog (#27686) (#27732)
(cherry picked from commit ece446ffe5)

Co-authored-by: Alan <2.alan.tom@gmail.com>
2021-10-01 23:21:08 +05:30
mergify[bot]
23431cf261 fix: option to limit reposting in certain timeslot (#27725) (#27726)
(cherry picked from commit a04f9c904e)

Co-authored-by: Ankush Menat <ankush@iwebnotes.com>
2021-10-01 13:56:00 +05:30
Anurag Mishra
57e66f958c feat: Tracking Multi-round interview (#25482)
* feat: Tracking Multi-round interview

* fix: releted to scheduler event and formating

* fix: job applicant UI/UX and conflicts

* test: Interview Round

* fix(test): Employee referral, Employee Onboarding, Job Offer

* fix: sider

* feat: set default value in Hr settings

* feat: added validation for designation

* test: Interview

* test: Added validatiolns for skill

* test: Interview feedback

* fix: sider

* fix: remove unnecessary validations and form label cleanups

* chore: clean-up Interview Round and Interview Type doctype

* fix: remove redundant Rating Value, only keep Rating

* fix: update interview details on feedback submission

- make interview feedback submission dialog minimizable

* fix: show submit feedback button only if feedback doesn't exist

* refactor: Interview and Feedback statuses and workflow

* fix(HR Settings): clean up interview settings

* refactor: Interview

* refactor: Interview Feedback, remove unnecessary validations

* chore: update notification messages

* chore: remove unnecessary formatting changes in attendance list and leave application

* refactor: Job Applicant to Interview mapping

* chore: sorted imports

* chore: sorted imports

* fix: sider issues

* fix: linter issues

* fix: sider issues

* fix: tests

* fix: sorted imports

* fix: tests, sider

* fix: therapy plan test

* fix: sider issues

* feat: Include From Time and To Time fields in Interview for cleaner data

* feat: Interview Calendar

* fix: allow renaming masters

* fix: add more fields to list view and standard filter

* fix: validate overlapping interviews

* fix: update tests

* fix: linter issues

* refactor: replace reminder messages with Email Templates

* fix: sider issues

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-10-01 00:10:47 +05:30
mergify[bot]
05374cb8b2 fix(Org Chart): use attribute selectors instead of ID selector for node IDs with special characters (#27717) (#27718)
* fix(Org Chart): use attribute selectors instead of ID selector for node IDs with special chars

* fix: UI tests

(cherry picked from commit 9e08229b7b)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-09-30 18:45:22 +05:30
marination
36b519c962 fix: Website Items with same Item name unhandled, thumbnails missing
- Use naming series for Website Item. There could be two items with same name and different item code
- Fix: Website Item Page view breaks if cart is disabled
- Fix: thumbnails not created for Website Items after patch
- Fix: ‘Request for Quote’ button & cart summary not visible if checkout is disabled
2021-09-30 18:34:26 +05:30
mergify[bot]
15c9c08261 Merge pull request #27715 from frappe/mergify/bp/version-13-hotfix/pr-27668
fix: Batch scans get overwritten on the same row (backport #27668)
2021-09-30 16:34:11 +05:30
Marica
5d1de91b68 Merge pull request #27713 from frappe-pr-bot/backport/version-13-hotfix/27554
fix: Maintenance Schedule child table status for legacy data
2021-09-30 15:57:27 +05:30
Marica
cf6e10ac7b Merge branch 'version-13-hotfix' into backport/version-13-hotfix/27554 2021-09-30 15:55:04 +05:30
Ankush Menat
6d99bb5ce6 fix: remove stale doctypes and add msg for ecommerce refactor (#27700) 2021-09-30 15:49:26 +05:30
marination
6b38778dcb fix: reload doc in patch 2021-09-30 14:55:57 +05:30
marination
7c47f36a4c fix: Add patch to patches.txt 2021-09-30 14:18:35 +05:30
Marica
6ce2111b6d fix: Maintenance Schedule child table status for legacy data (#27554)
* fix: Maintenance Schedule child table status for legacy data

* fix: Include legacy draft schedules in patch

* fix: Pre-commit formatting

(cherry picked from commit cc143bca0d)
2021-09-30 08:39:55 +00:00
Deepesh Garg
f0af24fc6d fix(India): Internal transfer check fix 2021-09-30 13:28:53 +05:30
Deepesh Garg
6eb9a114be Merge branch 'version-13-hotfix' into backport/version-13-hotfix/27524 2021-09-27 14:55:32 +05:30
Subin Tom
8c01ae952b conflict fixes 2021-09-17 20:18:39 +05:30
Subin Tom
e8cf32e1c8 fixing conflicts 2021-09-17 20:16:10 +05:30
Subin Tom
8dfdab9dc1 fix: Tax breakup based on items, missing GST fields (#27524)
* fix: Tax breakup based on items

* fix: added gst fields,warehouse validation to pos inv,patch

* fix: tax breakup test fix, eway bill hsn fix

Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com>
(cherry picked from commit d49346ac45)

# Conflicts:
#	erpnext/patches.txt
#	erpnext/regional/india/setup.py
2021-09-17 05:21:40 +00:00
219 changed files with 4537 additions and 1019 deletions

View File

@@ -1,6 +1,8 @@
[flake8]
ignore =
B007,
B009,
B010,
B950,
E101,
E111,

View File

@@ -131,3 +131,21 @@ rules:
key `$X` is uselessly assigned twice. This could be a potential bug.
languages: [python]
severity: ERROR
- id: frappe-manual-commit
patterns:
- pattern: frappe.db.commit()
- pattern-not-inside: |
try:
...
except ...:
...
message: |
Manually commiting a transaction is highly discouraged. Read about the transaction model implemented by Frappe Framework before adding manual commits: https://frappeframework.com/docs/user/en/api/database#database-transaction-model If you think manual commit is required then add a comment explaining why and `// nosemgrep` on the same line.
paths:
exclude:
- "**/patches/**"
- "**/demo/**"
languages: [python]
severity: ERROR

View File

@@ -99,6 +99,8 @@ jobs:
- name: Build Assets
run: cd ~/frappe-bench/ && bench build
env:
CI: Yes
- name: UI Tests
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless

58
.mergify.yml Normal file
View File

@@ -0,0 +1,58 @@
pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- and:
- and:
- author!=surajshetty3416
- author!=gavindsouza
- author!=rohitwaghchaure
- author!=nabinhait
- or:
- base=version-13
- base=version-12
actions:
close:
comment:
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
- name: backport to version-13-hotfix
conditions:
- label="backport version-13-hotfix"
actions:
backport:
branches:
- version-13-hotfix
assignees:
- "{{ author }}"
- name: backport to version-13-pre-release
conditions:
- label="backport version-13-pre-release"
actions:
backport:
branches:
- version-13-pre-release
assignees:
- "{{ author }}"
- name: backport to version-12-hotfix
conditions:
- label="backport version-12-hotfix"
actions:
backport:
branches:
- version-12-hotfix
assignees:
- "{{ author }}"
- name: backport to version-12-pre-release
conditions:
- label="backport version-12-pre-release"
actions:
backport:
branches:
- version-12-pre-release
assignees:
- "{{ author }}"

View File

@@ -20,6 +20,9 @@ repos:
rev: 3.9.2
hooks:
- id: flake8
additional_dependencies: [
'flake8-bugbear',
]
args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$"

View File

@@ -24,7 +24,7 @@ context('Organizational Chart', () => {
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
cy.get('@input')
.clear({ force: true })
.type('Test Org Chart{enter}', { force: true })
.type('Test Org Chart{downarrow}{enter}', { force: true })
.blur({ force: true });
});
});

View File

@@ -25,7 +25,7 @@ context('Organizational Chart Mobile', () => {
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
cy.get('@input')
.clear({ force: true })
.type('Test Org Chart{enter}', { force: true })
.type('Test Org Chart{downarrow}{enter}', { force: true })
.blur({ force: true });
});
});

View File

@@ -7,7 +7,7 @@ import frappe
from erpnext.hooks import regional_overrides
__version__ = '13.12.1'
__version__ = '13.13.0'
def get_default_company(user=None):
'''Get default company for user'''

View File

@@ -8,6 +8,8 @@ from frappe import _, throw
from frappe.utils import cint, cstr
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
import erpnext
class RootNotEditable(frappe.ValidationError): pass
class BalanceMismatchError(frappe.ValidationError): pass
@@ -196,7 +198,7 @@ class Account(NestedSet):
"company": company,
# parent account's currency should be passed down to child account's curreny
# if it is None, it picks it up from default company currency, which might be unintended
"account_currency": self.account_currency,
"account_currency": erpnext.get_company_currency(company),
"parent_account": parent_acc_name_map[company]
})
@@ -207,8 +209,7 @@ class Account(NestedSet):
# update the parent company's value in child companies
doc = frappe.get_doc("Account", child_account)
parent_value_changed = False
for field in ['account_type', 'account_currency',
'freeze_account', 'balance_must_be']:
for field in ['account_type', 'freeze_account', 'balance_must_be']:
if doc.get(field) != self.get(field):
parent_value_changed = True
doc.set(field, self.get(field))

View File

@@ -45,6 +45,49 @@ frappe.treeview_settings["Account"] = {
],
root_label: "Accounts",
get_tree_nodes: 'erpnext.accounts.utils.get_children',
on_get_node: function(nodes, deep=false) {
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
let accounts = [];
if (deep) {
// in case of `get_all_nodes`
accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
} else {
accounts = nodes;
}
const get_balances = frappe.call({
method: 'erpnext.accounts.utils.get_account_balances',
args: {
accounts: accounts,
company: cur_tree.args.company
},
});
get_balances.then(r => {
if (!r.message || r.message.length == 0) return;
for (let account of r.message) {
const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue;
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? "Dr": "Cr";
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) {
$('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ format(account.balance, account.company_currency)
+ " " + dr_or_cr
+ '</span>').insertBefore(node.$ul);
}
}
});
},
add_tree_node: 'erpnext.accounts.utils.add_ac',
menu_items:[
{
@@ -122,24 +165,6 @@ frappe.treeview_settings["Account"] = {
}
}, "add");
},
onrender: function(node) {
if (frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
// show Dr if positive since balance is calculated as debit - credit else show Cr
let balance = node.data.balance_in_account_currency || node.data.balance;
let dr_or_cr = balance > 0 ? "Dr": "Cr";
if (node.data && node.data.balance!==undefined) {
$('<span class="balance-area pull-right">'
+ (node.data.balance_in_account_currency ?
(format_currency(Math.abs(node.data.balance_in_account_currency),
node.data.account_currency) + " / ") : "")
+ format_currency(Math.abs(node.data.balance), node.data.company_currency)
+ " " + dr_or_cr
+ '</span>').insertBefore(node.$ul);
}
}
},
toolbar: [
{
label:__("Add Child"),

View File

@@ -12,7 +12,7 @@ from six import iteritems
from unidecode import unidecode
def create_charts(company, chart_template=None, existing_company=None, custom_chart=None):
def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None):
chart = custom_chart or get_chart(chart_template, existing_company)
if chart:
accounts = []
@@ -22,7 +22,7 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
if root_account:
root_type = child.get("root_type")
if account_name not in ["account_number", "account_type",
if account_name not in ["account_name", "account_number", "account_type",
"root_type", "is_group", "tax_rate"]:
account_number = cstr(child.get("account_number")).strip()
@@ -35,7 +35,7 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
account = frappe.get_doc({
"doctype": "Account",
"account_name": account_name,
"account_name": child.get('account_name') if from_coa_importer else account_name,
"company": company,
"parent_account": parent,
"is_group": is_group,
@@ -213,7 +213,7 @@ def validate_bank_account(coa, bank_account):
return (bank_account in accounts)
@frappe.whitelist()
def build_tree_from_json(chart_template, chart_data=None):
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
''' get chart template from its folder and parse the json to be rendered as tree '''
chart = chart_data or get_chart(chart_template)
@@ -226,9 +226,12 @@ def build_tree_from_json(chart_template, chart_data=None):
''' recursively called to form a parent-child based list of dict from chart template '''
for account_name, child in iteritems(children):
account = {}
if account_name in ["account_number", "account_type",\
if account_name in ["account_name", "account_number", "account_type",\
"root_type", "is_group", "tax_rate"]: continue
if from_coa_importer:
account_name = child['account_name']
account['parent_account'] = parent
account['expandable'] = True if identify_is_group(child) else False
account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \

View File

@@ -175,7 +175,7 @@
"default": "0",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms"
"label": "Automatically Fetch Payment Terms from Order"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -283,7 +283,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-08-19 11:17:38.788054",
"modified": "2021-10-11 17:42:36.427699",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -69,7 +69,7 @@ def import_coa(file_name, company):
frappe.local.flags.ignore_root_company_validation = True
forest = build_forest(data)
create_charts(company, custom_chart=forest)
create_charts(company, custom_chart=forest, from_coa_importer=True)
# trigger on_update for company to reset default accounts
set_default_accounts(company)
@@ -148,7 +148,7 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
if not for_validate:
forest = build_forest(data)
accounts = build_tree_from_json("", chart_data=forest) # returns a list of dict in a tree render-able form
accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
# filter out to show data for the selected node only
accounts = [d for d in accounts if d['parent_account']==parent]
@@ -212,11 +212,14 @@ def build_forest(data):
if not account_name:
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
name = account_name
if account_number:
account_number = cstr(account_number).strip()
account_name = "{} - {}".format(account_number, account_name)
charts_map[account_name] = {}
charts_map[account_name]['account_name'] = name
if account_number: charts_map[account_name]["account_number"] = account_number
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
if account_type: charts_map[account_name]["account_type"] = account_type
if root_type: charts_map[account_name]["root_type"] = root_type

View File

@@ -16,7 +16,7 @@ class LoyaltyPointEntry(Document):
def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=None):
if not expiry_date:
date = today()
expiry_date = today()
return frappe.db.sql('''
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice

View File

@@ -390,6 +390,9 @@ class PaymentEntry(AccountsController):
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
for key, allocated_amount in iteritems(invoice_payment_amount_map):
if not invoice_paid_amount_map.get(key):
frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1]))
outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt'))
@@ -502,12 +505,13 @@ class PaymentEntry(AccountsController):
def validate_received_amount(self):
if self.paid_from_account_currency == self.paid_to_account_currency:
if self.paid_amount != self.received_amount:
frappe.throw(_("Received Amount should be same as Paid Amount"))
if self.paid_amount < self.received_amount:
frappe.throw(_("Received Amount cannot be greater than Paid Amount"))
def set_received_amount(self):
self.base_received_amount = self.base_paid_amount
if self.paid_from_account_currency == self.paid_to_account_currency:
if self.paid_from_account_currency == self.paid_to_account_currency \
and not self.payment_type == 'Internal Transfer':
self.received_amount = self.paid_amount
def set_amounts_after_tax(self):
@@ -709,10 +713,14 @@ class PaymentEntry(AccountsController):
dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit"
for d in self.get("references"):
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy()
gle.update({
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name
"against_voucher": d.reference_name,
"cost_center": cost_center
})
allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate),

View File

@@ -52,21 +52,35 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
refresh: function() {
this.frm.disable_save();
this.frm.set_df_property('invoices', 'cannot_delete_rows', true);
this.frm.set_df_property('payments', 'cannot_delete_rows', true);
this.frm.set_df_property('allocation', 'cannot_delete_rows', true);
this.frm.set_df_property('invoices', 'cannot_add_rows', true);
this.frm.set_df_property('payments', 'cannot_add_rows', true);
this.frm.set_df_property('allocation', 'cannot_add_rows', true);
if (this.frm.doc.receivable_payable_account) {
this.frm.add_custom_button(__('Get Unreconciled Entries'), () =>
this.frm.trigger("get_unreconciled_entries")
);
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
}
if (this.frm.doc.invoices.length && this.frm.doc.payments.length) {
this.frm.add_custom_button(__('Allocate'), () =>
this.frm.trigger("allocate")
);
this.frm.change_custom_button_type('Allocate', null, 'primary');
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
}
if (this.frm.doc.allocation.length) {
this.frm.add_custom_button(__('Reconcile'), () =>
this.frm.trigger("reconcile")
);
this.frm.change_custom_button_type('Reconcile', null, 'primary');
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
this.frm.change_custom_button_type('Allocate', null, 'default');
}
},

View File

@@ -12,15 +12,16 @@
"receivable_payable_account",
"col_break1",
"from_invoice_date",
"to_invoice_date",
"minimum_invoice_amount",
"maximum_invoice_amount",
"invoice_limit",
"column_break_13",
"from_payment_date",
"to_payment_date",
"minimum_invoice_amount",
"minimum_payment_amount",
"column_break_11",
"to_invoice_date",
"to_payment_date",
"maximum_invoice_amount",
"maximum_payment_amount",
"column_break_13",
"invoice_limit",
"payment_limit",
"bank_cash_account",
"sec_break1",
@@ -79,6 +80,7 @@
},
{
"depends_on": "eval:(doc.payments).length || (doc.invoices).length",
"description": "If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.",
"fieldname": "sec_break1",
"fieldtype": "Section Break",
"label": "Unreconciled Entries"
@@ -163,6 +165,7 @@
"label": "Maximum Payment Amount"
},
{
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "payment_limit",
"fieldtype": "Int",
"label": "Payment Limit"
@@ -171,13 +174,17 @@
"fieldname": "maximum_invoice_amount",
"fieldtype": "Currency",
"label": "Maximum Invoice Amount"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"links": [],
"modified": "2021-08-30 13:05:51.977861",
"modified": "2021-10-04 20:27:11.114194",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",

View File

@@ -14,8 +14,8 @@
"section_break_6",
"allocated_amount",
"unreconciled_amount",
"amount",
"column_break_8",
"amount",
"is_advance",
"section_break_5",
"difference_amount",
@@ -127,12 +127,13 @@
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Row"
"label": "Reference Row",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-09-20 17:23:09.455803",
"modified": "2021-10-06 11:48:59.616562",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",

View File

@@ -33,7 +33,9 @@ class TestPOSProfile(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def get_customers_list(pos_profile={}):
def get_customers_list(pos_profile=None):
if pos_profile is None:
pos_profile = {}
cond = "1=1"
customer_groups = []
if pos_profile.get('customer_groups'):

View File

@@ -398,7 +398,9 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules):
pricing_rules[0].apply_rule_on_other_items = items
return pricing_rules
def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]):
def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
if items is None:
items = []
sum_qty, sum_amt = [0, 0]
doctype = doc.get('parenttype') or doc.doctype

View File

@@ -69,7 +69,9 @@ class PromotionalScheme(Document):
{'promotional_scheme': self.name}):
frappe.delete_doc('Pricing Rule', rule.name)
def get_pricing_rules(doc, rules = {}):
def get_pricing_rules(doc, rules=None):
if rules is None:
rules = {}
new_doc = []
for child_doc, fields in {'price_discount_slabs': price_discount_fields,
'product_discount_slabs': product_discount_fields}.items():
@@ -78,7 +80,9 @@ def get_pricing_rules(doc, rules = {}):
return new_doc
def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}):
def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
if rules is None:
rules = {}
new_doc = []
args = get_args_for_pricing_rule(doc)
applicable_for = frappe.scrub(doc.get('applicable_for'))

View File

@@ -149,16 +149,18 @@
"cb_17",
"hold_comment",
"more_info",
"status",
"inter_company_invoice_reference",
"represents_company",
"column_break_147",
"is_internal_supplier",
"accounting_details_section",
"credit_to",
"party_account_currency",
"is_opening",
"against_expense_account",
"column_break_63",
"unrealized_profit_loss_account",
"status",
"inter_company_invoice_reference",
"is_internal_supplier",
"represents_company",
"remarks",
"subscription_section",
"from_date",
@@ -1171,6 +1173,15 @@
"options": "fa fa-file-text",
"print_hide": 1
},
{
"default": "0",
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"ignore_user_permissions": 1,
"label": "Is Internal Supplier",
"read_only": 1
},
{
"fieldname": "credit_to",
"fieldtype": "Link",
@@ -1196,7 +1207,7 @@
"default": "No",
"fieldname": "is_opening",
"fieldtype": "Select",
"label": "Is Opening",
"label": "Is Opening Entry",
"oldfieldname": "is_opening",
"oldfieldtype": "Select",
"options": "No\nYes",
@@ -1298,15 +1309,6 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"default": "0",
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"ignore_user_permissions": 1,
"label": "Is Internal Supplier",
"read_only": 1
},
{
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
@@ -1395,13 +1397,24 @@
"hidden": 1,
"label": "Ignore Default Payment Terms Template",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details",
"print_hide": 1
},
{
"fieldname": "column_break_147",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2021-09-28 13:10:28.351810",
"modified": "2021-10-12 20:55:16.145651",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry,
get_total_in_party_account_currency,
is_overdue,
unlink_inter_company_doc,
update_linked_doc,
@@ -1147,6 +1148,7 @@ class PurchaseInvoice(BuyingController):
return
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
total = get_total_in_party_account_currency(self)
if not status:
if self.docstatus == 2:
@@ -1154,9 +1156,9 @@ class PurchaseInvoice(BuyingController):
elif self.docstatus == 1:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif is_overdue(self):
elif is_overdue(self, total):
self.status = "Overdue"
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
elif 0 < outstanding_amount < total:
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"

View File

@@ -446,12 +446,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
},
currency() {
var me = this;
this._super();
$.each(cur_frm.doc.timesheets, function(i, d) {
let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
});
calculate_total_billing_amount(cur_frm)
if (this.frm.doc.timesheets) {
this.frm.doc.timesheets.forEach((d) => {
let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
});
frm.trigger("calculate_timesheet_totals");
}
}
});
@@ -999,7 +1002,7 @@ frappe.ui.form.on('Sales Invoice', {
frappe.ui.form.on("Sales Invoice Timesheet", {
timesheets_remove(frm, cdt, cdn) {
timesheets_remove(frm) {
frm.trigger("calculate_timesheet_totals");
}
});

View File

@@ -124,6 +124,13 @@
"total_advance",
"outstanding_amount",
"disable_rounded_total",
"column_break4",
"write_off_amount",
"base_write_off_amount",
"write_off_outstanding_amount_automatically",
"column_break_74",
"write_off_account",
"write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
"get_advances",
@@ -144,13 +151,6 @@
"column_break_90",
"change_amount",
"account_for_change_amount",
"column_break4",
"write_off_amount",
"base_write_off_amount",
"write_off_outstanding_amount_automatically",
"column_break_74",
"write_off_account",
"write_off_cost_center",
"terms_section_break",
"tc_name",
"terms",
@@ -161,14 +161,14 @@
"column_break_84",
"language",
"more_information",
"status",
"inter_company_invoice_reference",
"is_internal_customer",
"represents_company",
"customer_group",
"campaign",
"is_discounted",
"col_break23",
"status",
"is_internal_customer",
"is_discounted",
"source",
"more_info",
"debit_to",
@@ -1990,16 +1990,6 @@
"label": "Additional Discount Account",
"options": "Account"
},
{
"default": "0",
"fieldname": "ignore_default_payment_terms_template",
"fieldtype": "Check",
"hidden": 1,
"label": "Ignore Default Payment Terms Template",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address_name",
@@ -2015,6 +2005,14 @@
"label": "Dispatch Address",
"read_only": 1
},
{
"default": "0",
"fieldname": "ignore_default_payment_terms_template",
"fieldtype": "Check",
"hidden": 1,
"label": "Ignore Default Payment Terms Template",
"read_only": 1
},
{
"fieldname": "total_billing_hours",
"fieldtype": "Float",
@@ -2033,7 +2031,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-09-28 13:09:34.391799",
"modified": "2021-10-11 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2088,4 +2086,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -1296,12 +1296,20 @@ class SalesInvoice(SellingController):
serial_nos = item.serial_no or ""
si_serial_nos = set(get_serial_nos(serial_nos))
serial_no_diff = si_serial_nos - dn_serial_nos
if si_serial_nos - dn_serial_nos:
frappe.throw(_("Serial Numbers in row {0} does not match with Delivery Note").format(item.idx))
if serial_no_diff:
dn_link = frappe.utils.get_link_to_form("Delivery Note", item.delivery_note)
serial_no_msg = ", ".join(frappe.bold(d) for d in serial_no_diff)
msg = _("Row #{0}: The following Serial Nos are not present in Delivery Note {1}:").format(
item.idx, dn_link)
msg += " " + serial_no_msg
frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
if item.serial_no and cint(item.qty) != len(si_serial_nos):
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
frappe.throw(_("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
item.idx, item.qty, item.item_code, len(si_serial_nos)))
def update_project(self):
@@ -1470,6 +1478,7 @@ class SalesInvoice(SellingController):
return
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
total = get_total_in_party_account_currency(self)
if not status:
if self.docstatus == 2:
@@ -1477,9 +1486,9 @@ class SalesInvoice(SellingController):
elif self.docstatus == 1:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif is_overdue(self):
elif is_overdue(self, total):
self.status = "Overdue"
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
elif 0 < outstanding_amount < total:
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
@@ -1506,27 +1515,42 @@ class SalesInvoice(SellingController):
if update:
self.db_set('status', self.status, update_modified = update_modified)
def is_overdue(doc):
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
def get_total_in_party_account_currency(doc):
total_fieldname = (
"grand_total"
if doc.disable_rounded_total
else "rounded_total"
)
if doc.party_account_currency != doc.currency:
total_fieldname = "base_" + total_fieldname
return flt(doc.get(total_fieldname), doc.precision(total_fieldname))
def is_overdue(doc, total):
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
if outstanding_amount <= 0:
return
grand_total = flt(doc.grand_total, doc.precision("grand_total"))
nowdate = getdate()
if doc.payment_schedule:
# calculate payable amount till date
payable_amount = sum(
payment.payment_amount
for payment in doc.payment_schedule
if getdate(payment.due_date) < nowdate
)
today = getdate()
if doc.get('is_pos') or not doc.get('payment_schedule'):
return getdate(doc.due_date) < today
if (grand_total - outstanding_amount) < payable_amount:
return True
# calculate payable amount till date
payment_amount_field = (
"base_payment_amount"
if doc.party_account_currency != doc.currency
else "payment_amount"
)
payable_amount = sum(
payment.get(payment_amount_field)
for payment in doc.payment_schedule
if getdate(payment.due_date) < today
)
return (total - outstanding_amount) < payable_amount
elif getdate(doc.due_date) < nowdate:
return True
def get_discounting_status(sales_invoice):
status = None

View File

@@ -1085,8 +1085,6 @@ class TestSalesInvoice(unittest.TestCase):
actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
frappe.db.commit()
self.assertEqual(actual_qty_0 - 5, actual_qty_1)
# outgoing_rate
@@ -2341,6 +2339,18 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.save()
self.assertRaises(frappe.ValidationError, si.submit)
si.posting_date = getdate()
si.submit()
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-.#####'

View File

@@ -16,9 +16,9 @@
"column_break_9",
"billing_amount",
"section_break_11",
"timesheet_detail",
"column_break_5",
"time_sheet",
"timesheet_detail",
"column_break_13",
"project_name"
],
"fields": [
@@ -91,7 +91,6 @@
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Totals"
@@ -110,11 +109,15 @@
"fieldtype": "Data",
"label": "Project Name",
"read_only": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
"modified": "2021-08-15 18:37:08.084930",
"modified": "2021-10-02 03:48:44.979777",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",

View File

@@ -33,7 +33,7 @@ class Subscription(Document):
# update start just before the subscription doc is created
self.update_subscription_period(self.start_date)
def update_subscription_period(self, date=None):
def update_subscription_period(self, date=None, return_date=False):
"""
Subscription period is the period to be billed. This method updates the
beginning of the billing period and end of the billing period.
@@ -41,28 +41,41 @@ class Subscription(Document):
The beginning of the billing period is represented in the doctype as
`current_invoice_start` and the end of the billing period is represented
as `current_invoice_end`.
"""
self.set_current_invoice_start(date)
self.set_current_invoice_end()
def set_current_invoice_start(self, date=None):
If return_date is True, it wont update the start and end dates.
This is implemented to get the dates to check if is_current_invoice_generated
"""
This sets the date of the beginning of the current billing period.
_current_invoice_start = self.get_current_invoice_start(date)
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
if return_date:
return _current_invoice_start, _current_invoice_end
self.current_invoice_start = _current_invoice_start
self.current_invoice_end = _current_invoice_end
def get_current_invoice_start(self, date=None):
"""
This returns the date of the beginning of the current billing period.
If the `date` parameter is not given , it will be automatically set as today's
date.
"""
if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
self.current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
self.current_invoice_start = self.trial_period_start
elif date:
self.current_invoice_start = date
else:
self.current_invoice_start = nowdate()
_current_invoice_start = None
def set_current_invoice_end(self):
if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
_current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
elif date:
_current_invoice_start = date
else:
_current_invoice_start = nowdate()
return _current_invoice_start
def get_current_invoice_end(self, date=None):
"""
This sets the date of the end of the current billing period.
This returns the date of the end of the current billing period.
If the subscription is in trial period, it will be set as the end of the
trial period.
@@ -71,44 +84,47 @@ class Subscription(Document):
current billing period where `x` is the billing interval from the
`Subscription Plan` in the `Subscription`.
"""
if self.is_trialling() and getdate(self.current_invoice_start) < getdate(self.trial_period_end):
self.current_invoice_end = self.trial_period_end
_current_invoice_end = None
if self.is_trialling() and getdate(date) < getdate(self.trial_period_end):
_current_invoice_end = self.trial_period_end
else:
billing_cycle_info = self.get_billing_cycle_data()
if billing_cycle_info:
if self.is_new_subscription() and getdate(self.start_date) < getdate(self.current_invoice_start):
self.current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
# For cases where trial period is for an entire billing interval
if getdate(self.current_invoice_end) < getdate(self.current_invoice_start):
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
if getdate(self.current_invoice_end) < getdate(date):
_current_invoice_end = add_to_date(date, **billing_cycle_info)
else:
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
_current_invoice_end = add_to_date(date, **billing_cycle_info)
else:
self.current_invoice_end = get_last_day(self.current_invoice_start)
_current_invoice_end = get_last_day(date)
if self.follow_calendar_months:
billing_info = self.get_billing_cycle_and_interval()
billing_interval_count = billing_info[0]['billing_interval_count']
calendar_months = get_calendar_months(billing_interval_count)
calendar_month = 0
current_invoice_end_month = getdate(self.current_invoice_end).month
current_invoice_end_year = getdate(self.current_invoice_end).year
current_invoice_end_month = getdate(_current_invoice_end).month
current_invoice_end_year = getdate(_current_invoice_end).year
for month in calendar_months:
if month <= current_invoice_end_month:
calendar_month = month
if cint(calendar_month - billing_interval_count) <= 0 and \
getdate(self.current_invoice_start).month != 1:
getdate(date).month != 1:
calendar_month = 12
current_invoice_end_year -= 1
self.current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' \
+ cstr(calendar_month) + '-01')
_current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' + cstr(calendar_month) + '-01')
if self.end_date and getdate(self.current_invoice_end) > getdate(self.end_date):
self.current_invoice_end = self.end_date
if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
_current_invoice_end = self.end_date
return _current_invoice_end
@staticmethod
def validate_plans_billing_cycle(billing_cycle_data):
@@ -484,8 +500,9 @@ class Subscription(Document):
def is_current_invoice_generated(self):
invoice = self.get_current_invoice()
_current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
return True
return False
@@ -538,15 +555,15 @@ class Subscription(Document):
else:
self.set_status_grace_period()
if getdate() > getdate(self.current_invoice_end):
self.update_subscription_period(add_days(self.current_invoice_end, 1))
# Generate invoices periodically even if current invoice are unpaid
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
or self.is_prepaid_to_invoice()):
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(prorate)
if getdate() > getdate(self.current_invoice_end):
self.update_subscription_period(add_days(self.current_invoice_end, 1))
@staticmethod
def is_paid(invoice):
"""

View File

@@ -18,6 +18,7 @@ from frappe.utils.data import (
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
test_dependencies = ("UOM", "Item Group", "Item")
def create_plan():
if not frappe.db.exists('Subscription Plan', '_Test Plan Name'):
@@ -68,7 +69,6 @@ def create_plan():
supplier.insert()
class TestSubscription(unittest.TestCase):
def setUp(self):
create_plan()

View File

@@ -203,6 +203,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
# then chargeable value is "prev invoices + advances" value which cross the threshold
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
return tax_amount, tax_deducted
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
@@ -322,9 +325,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
if cint(tax_details.round_off_tax_amount):
tds_amount = round(tds_amount)
return tds_amount
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):

View File

@@ -293,7 +293,7 @@ def check_freezing_date(posting_date, adv_adj=False):
if acc_frozen_upto:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
if getdate(posting_date) <= getdate(acc_frozen_upto) \
and not frozen_accounts_modifier in frappe.get_roles() or frappe.session.user == 'Administrator':
and (frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == 'Administrator'):
frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto)))
def set_as_cancel(voucher_type, voucher_no):

View File

@@ -139,9 +139,9 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
data["total"] = total
return data
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters={}):
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
cond = ""
filters = frappe._dict(filters)
filters = frappe._dict(filters or {})
if filters.include_default_book_entries:
company_fb = frappe.db.get_value("Company", company, 'default_finance_book')

View File

@@ -103,8 +103,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
column.is_tree = true;
}
value = default_formatter(value, row, column, data);
if (data && data.account && column.apply_currency_formatter) {
data.currency = erpnext.get_currency(column.company_name);
}
value = default_formatter(value, row, column, data);
if (!data.parent_account) {
value = $(`<span>${value}</span>`);

View File

@@ -3,12 +3,14 @@
from __future__ import unicode_literals
from collections import defaultdict
import frappe
from frappe import _
from frappe.utils import cint, flt, getdate
import erpnext
from erpnext.accounts.report.balance_sheet.balance_sheet import (
check_opening_balance,
get_chart_data,
get_provisional_profit_loss,
)
@@ -31,7 +33,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_report_summary as get_pl_summary,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
def execute(filters=None):
@@ -42,7 +44,7 @@ def execute(filters=None):
fiscal_year = get_fiscal_year_data(filters.get('from_fiscal_year'), filters.get('to_fiscal_year'))
companies_column, companies = get_companies(filters)
columns = get_columns(companies_column)
columns = get_columns(companies_column, filters)
if filters.get('report') == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data(fiscal_year, companies, columns, filters)
@@ -73,21 +75,24 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity,
companies, filters.get('company'), company_currency, True)
message, opening_balance = check_opening_balance(asset, liability, equity)
message, opening_balance = prepare_companywise_opening_balance(asset, liability, equity, companies)
if opening_balance and round(opening_balance,2) !=0:
unclosed ={
if opening_balance:
unclosed = {
"account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"warn_if_negative": True,
"currency": company_currency
}
for company in companies:
unclosed[company] = opening_balance
if provisional_profit_loss:
provisional_profit_loss[company] = provisional_profit_loss[company] - opening_balance
unclosed["total"]=opening_balance
for company in companies:
unclosed[company] = opening_balance.get(company)
if provisional_profit_loss and provisional_profit_loss.get(company):
provisional_profit_loss[company] = (
flt(provisional_profit_loss[company]) - flt(opening_balance.get(company))
)
unclosed["total"] = opening_balance.get(company)
data.append(unclosed)
if provisional_profit_loss:
@@ -102,6 +107,37 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
return data, message, chart, report_summary
def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, companies):
opening_balance = {}
for company in companies:
opening_value = 0
# opening_value = Aseet - liability - equity
for data in [asset_data, liability_data, equity_data]:
account_name = get_root_account_name(data[0].root_type, company)
opening_value += get_opening_balance(account_name, data, company)
opening_balance[company] = opening_value
if opening_balance:
return _("Previous Financial Year is not closed"), opening_balance
return '', {}
def get_opening_balance(account_name, data, company):
for row in data:
if row.get('account_name') == account_name:
return row.get('company_wise_opening_bal', {}).get(company, 0.0)
def get_root_account_name(root_type, company):
return frappe.get_all(
'Account',
fields=['account_name'],
filters = {'root_type': root_type, 'is_group': 1,
'company': company, 'parent_account': ('is', 'not set')},
as_list=1
)[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters):
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(filters)
@@ -193,30 +229,37 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data["total"] = total
return data
def get_columns(companies):
columns = [{
"fieldname": "account",
"label": _("Account"),
"fieldtype": "Link",
"options": "Account",
"width": 300
}]
columns.append({
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"hidden": 1
})
def get_columns(companies, filters):
columns = [
{
"fieldname": "account",
"label": _("Account"),
"fieldtype": "Link",
"options": "Account",
"width": 300
}, {
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"hidden": 1
}
]
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
columns.append({
"fieldname": company,
"label": company,
"label": f'{company} ({currency})',
"fieldtype": "Currency",
"options": "currency",
"width": 150
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company
})
return columns
@@ -236,6 +279,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None
end_date = filters.period_end_date
filters.end_date = end_date
gl_entries_by_account = {}
for root in frappe.db.sql("""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1):
@@ -244,9 +289,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
end_date, root.lft, root.rgt, filters,
gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
accumulate_values_into_parents(accounts, accounts_by_name, companies)
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency)
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency)
@@ -257,7 +303,10 @@ def get_company_currency(filters=None):
return (filters.get('presentation_currency')
or frappe.get_cached_value('Company', filters.company, "default_currency"))
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year):
start_date = (fiscal_year.year_start_date
if filters.filter_based_on == 'Fiscal Year' else filters.period_start_date)
for entries in gl_entries_by_account.values():
for entry in entries:
if entry.account_number:
@@ -266,15 +315,32 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_d
account_name = entry.account_name
d = accounts_by_name.get(account_name)
if d:
debit, credit = 0, 0
for company in companies:
# check if posting date is within the period
if (entry.company == company or (filters.get('accumulated_in_group_company'))
and entry.company in companies.get(company)):
d[company] = d.get(company, 0.0) + flt(entry.debit) - flt(entry.credit)
parent_company_currency = erpnext.get_company_currency(d.company)
child_company_currency = erpnext.get_company_currency(entry.company)
debit, credit = flt(entry.debit), flt(entry.credit)
if (not filters.get('presentation_currency')
and entry.company != company
and parent_company_currency != child_company_currency
and filters.get('accumulated_in_group_company')):
debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date)
credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date)
d[company] = d.get(company, 0.0) + flt(debit) - flt(credit)
if entry.posting_date < getdate(start_date):
d['company_wise_opening_bal'][company] += (flt(debit) - flt(credit))
if entry.posting_date < getdate(start_date):
d["opening_balance"] = d.get("opening_balance", 0.0) + flt(entry.debit) - flt(entry.credit)
d["opening_balance"] = d.get("opening_balance", 0.0) + flt(debit) - flt(credit)
def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts"""
@@ -282,17 +348,18 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
if d.parent_account:
account = d.parent_account_name
if not accounts_by_name.get(account):
continue
# if not accounts_by_name.get(account):
# continue
for company in companies:
accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
accounts_by_name[account]['company_wise_opening_bal'][company] += d.get('company_wise_opening_bal', {}).get(company, 0.0)
accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters)
@@ -353,7 +420,7 @@ def get_accounts(root_type, filters):
`tabAccount` where company = %s and root_type = %s
""" , (filters.get('company'), root_type), as_dict=1)
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency):
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = []
for d in accounts:
@@ -367,10 +434,13 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
"parent_account": _(d.parent_account),
"indent": flt(d.indent),
"year_start_date": start_date,
"root_type": d.root_type,
"year_end_date": end_date,
"currency": company_currency,
"currency": filters.presentation_currency,
"company_wise_opening_bal": d.company_wise_opening_bal,
"opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1)
})
for company in companies:
if d.get(company) and balance_must_be == "Credit":
# change sign based on Debit or Credit, since calculation is done using (debit - credit)
@@ -385,6 +455,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
row["has_value"] = has_value
row["total"] = total
data.append(row)
return data
@@ -447,6 +518,7 @@ def get_account_details(account):
'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
def validate_entries(key, entry, accounts_by_name, accounts):
# If an account present in the child company and not in the parent company
if key not in accounts_by_name:
args = get_account_details(entry.account)
@@ -456,12 +528,23 @@ def validate_entries(key, entry, accounts_by_name, accounts):
args.update({
'lft': parent_args.lft + 1,
'rgt': parent_args.rgt - 1,
'indent': 3,
'root_type': parent_args.root_type,
'report_type': parent_args.report_type
'report_type': parent_args.report_type,
'parent_account_name': parent_args.account_name,
'company_wise_opening_bal': defaultdict(float)
})
accounts_by_name.setdefault(key, args)
accounts.append(args)
idx = len(accounts)
# To identify parent account index
for index, row in enumerate(accounts):
if row.parent_account_name == args.parent_account_name:
idx = index
break
accounts.insert(idx+1, args)
def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = []
@@ -491,7 +574,6 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
for company in companies:
total_row.setdefault(company, 0.0)
total_row[company] += row.get(company, 0.0)
row[company] = 0.0
total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"])
@@ -511,6 +593,7 @@ def filter_accounts(accounts, depth=10):
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
d['company_wise_opening_bal'] = defaultdict(float)
accounts_by_name[account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)

View File

@@ -421,8 +421,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
update_value_in_dict(totals, 'closing', gle)
elif gle.posting_date <= to_date:
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
update_value_in_dict(totals, 'total', gle)
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
gle_map[gle.get(group_by)].entries.append(gle)
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
@@ -436,10 +434,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
else:
update_value_in_dict(consolidated_gle, key, gle)
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
update_value_in_dict(totals, 'closing', gle)
for key, value in consolidated_gle.items():
update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
update_value_in_dict(totals, 'total', value)
update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
update_value_in_dict(totals, 'closing', value)
entries.append(value)
return totals, entries

View File

@@ -4,11 +4,14 @@
from __future__ import unicode_literals
from json import loads
import frappe
import frappe.defaults
from frappe import _, throw
from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
from six import string_types
import erpnext
@@ -787,16 +790,28 @@ def get_children(doctype, parent, company, is_root=False):
if doctype == 'Account':
sort_accounts(acc, is_root, key="value")
company_currency = frappe.get_cached_value('Company', company, "default_currency")
for each in acc:
each["company_currency"] = company_currency
each["balance"] = flt(get_balance_on(each.get("value"), in_account_currency=False, company=company))
if each.account_currency != company_currency:
each["balance_in_account_currency"] = flt(get_balance_on(each.get("value"), company=company))
return acc
@frappe.whitelist()
def get_account_balances(accounts, company):
if isinstance(accounts, string_types):
accounts = loads(accounts)
if not accounts:
return []
company_currency = frappe.get_cached_value("Company", company, "default_currency")
for account in accounts:
account["company_currency"] = company_currency
account["balance"] = flt(get_balance_on(account["value"], in_account_currency=False, company=company))
if account["account_currency"] and account["account_currency"] != company_currency:
account["balance_in_account_currency"] = flt(get_balance_on(account["value"], company=company))
return accounts
def create_payment_gateway_account(gateway, payment_channel="Email"):
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account

View File

@@ -194,7 +194,7 @@ class Asset(AccountsController):
start = self.clear_depreciation_schedule()
# value_after_depreciation - current Asset value
if d.value_after_depreciation:
if self.docstatus == 1 and d.value_after_depreciation:
value_after_depreciation = (flt(d.value_after_depreciation) -
flt(self.opening_accumulated_depreciation))
else:

View File

@@ -682,6 +682,27 @@ class TestAsset(unittest.TestCase):
# reset indian company
frappe.flags.company = company_flag
def test_expected_value_change(self):
"""
tests if changing `expected_value_after_useful_life`
affects `value_after_depreciation`
"""
asset = create_asset(calculate_depreciation=1)
asset.opening_accumulated_depreciation = 2000
asset.number_of_depreciations_booked = 1
asset.finance_books[0].expected_value_after_useful_life = 100
asset.save()
asset.reload()
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
# changing expected_value_after_useful_life shouldn't affect value_after_depreciation
asset.finance_books[0].expected_value_after_useful_life = 200
asset.save()
asset.reload()
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category()

View File

@@ -22,7 +22,7 @@ class TestAssetRepair(unittest.TestCase):
frappe.db.sql("delete from `tabTax Rule`")
def test_update_status(self):
asset = create_asset()
asset = create_asset(submit=1)
initial_status = asset.status
asset_repair = create_asset_repair(asset = asset)
@@ -76,7 +76,7 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation = 1)
asset = create_asset(calculate_depreciation = 1, submit=1)
initial_asset_value = get_asset_value(asset)
asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
asset.reload()
@@ -85,7 +85,7 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
asset = create_asset(calculate_depreciation = 1)
asset = create_asset(calculate_depreciation = 1, submit=1)
initial_asset_value = get_asset_value(asset)
asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
asset.reload()
@@ -103,7 +103,7 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(asset_repair.name, gl_entry.voucher_no)
def test_increase_in_asset_life(self):
asset = create_asset(calculate_depreciation = 1)
asset = create_asset(calculate_depreciation = 1, submit=1)
initial_num_of_depreciations = num_of_depreciations(asset)
create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
asset.reload()
@@ -126,7 +126,7 @@ def create_asset_repair(**args):
if args.asset:
asset = args.asset
else:
asset = create_asset(is_existing_asset = 1)
asset = create_asset(is_existing_asset = 1, submit=1)
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({
"asset": asset.name,

View File

@@ -11,7 +11,7 @@ frappe.tour['Buying Settings'] = [
{
fieldname: "supp_master_name",
title: "Supplier Naming By",
description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a>" + __(" choose the 'Naming Series' option."),
description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a <a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a> choose the 'Naming Series' option."),
},
{
fieldname: "buying_price_list",

View File

@@ -0,0 +1,77 @@
{
"creation": "2021-07-28 11:51:42.319984",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-10-05 13:06:56.414584",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
"owner": "Administrator",
"reference_doctype": "Buying Settings",
"save_on_complete": 0,
"steps": [
{
"description": "When a Supplier is saved, system generates a unique identity or name for that Supplier which can be used to refer the Supplier in various Buying transactions.",
"field": "",
"fieldname": "supp_master_name",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Supplier Naming By",
"parent_field": "",
"position": "Bottom",
"title": "Supplier Naming By"
},
{
"description": "Configure what should be the default value of Supplier Group when creating a new Supplier.",
"field": "",
"fieldname": "supplier_group",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default Supplier Group",
"parent_field": "",
"position": "Right",
"title": "Default Supplier Group"
},
{
"description": "Item prices will be fetched from this Price List.",
"field": "",
"fieldname": "buying_price_list",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default Buying Price List",
"parent_field": "",
"position": "Bottom",
"title": "Default Buying Price List"
},
{
"description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice or a Purchase Receipt directly without creating a Purchase Order first.",
"field": "",
"fieldname": "po_required",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
"parent_field": "",
"position": "Bottom",
"title": "Purchase Order Required"
},
{
"description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first.",
"field": "",
"fieldname": "pr_required",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
"parent_field": "",
"position": "Bottom",
"title": "Purchase Receipt Required"
}
],
"title": "Buying Settings"
}

View File

@@ -0,0 +1,82 @@
{
"creation": "2021-07-29 14:11:58.271113",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-10-05 13:11:31.436135",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
"owner": "Administrator",
"reference_doctype": "Purchase Order",
"save_on_complete": 1,
"steps": [
{
"description": "Select a Supplier",
"field": "",
"fieldname": "supplier",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Supplier",
"parent_field": "",
"position": "Right",
"title": "Supplier"
},
{
"description": "Set the \"Required By\" date for the materials. This sets the \"Required By\" date for all the items.",
"field": "",
"fieldname": "schedule_date",
"fieldtype": "Date",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Required By",
"parent_field": "",
"position": "Left",
"title": "Required By"
},
{
"description": "Items to be purchased can be added here.",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Items",
"parent_field": "",
"position": "Bottom",
"title": "Items Table"
},
{
"child_doctype": "Purchase Order Item",
"description": "Enter the Item Code.",
"field": "",
"fieldname": "item_code",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 1,
"label": "Item Code",
"next_step_condition": "eval: doc.item_code",
"parent_field": "",
"parent_fieldname": "items",
"position": "Right",
"title": "Item Code"
},
{
"child_doctype": "Purchase Order Item",
"description": "Enter the required quantity for the material.",
"field": "",
"fieldname": "qty",
"fieldtype": "Float",
"has_next_condition": 0,
"is_table_field": 1,
"label": "Quantity",
"parent_field": "",
"parent_fieldname": "items",
"position": "Bottom",
"title": "Quantity"
}
],
"title": "Purchase Order"
}

View File

@@ -19,7 +19,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/buying",
"idx": 0,
"is_complete": 0,
"modified": "2020-07-08 14:05:28.273641",
"modified": "2021-08-24 18:13:42.463776",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",
@@ -28,23 +28,11 @@
{
"step": "Introduction to Buying"
},
{
"step": "Create a Supplier"
},
{
"step": "Setup your Warehouse"
},
{
"step": "Create a Product"
},
{
"step": "Create a Material Request"
},
{
"step": "Create your first Purchase Order"
},
{
"step": "Buying Settings"
}
],
"subtitle": "Products, Purchases, Analysis, and more.",

View File

@@ -1,19 +1,21 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"action_label": "Let\u2019s create your first Material Request",
"creation": "2020-05-15 14:39:09.818764",
"description": "# Track Material Request\n\n\nAlso known as Purchase Request or an Indent, is a document identifying a requirement of a set of items (products or services) for various purposes like procurement, transfer, issue, or manufacturing. Once the Material Request is validated, a purchase manager can take the next actions for purchasing items like requesting RFQ from a supplier or directly placing an order with an identified Supplier.\n\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-15 14:39:09.818764",
"modified": "2021-08-24 18:08:08.347501",
"modified_by": "Administrator",
"name": "Create a Material Request",
"owner": "Administrator",
"reference_document": "Material Request",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Create a Material Request",
"title": "Track Material Request",
"validate_action": 1
}

View File

@@ -1,19 +1,21 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"action_label": "Let\u2019s create your first Purchase Order",
"creation": "2020-05-12 18:17:49.976035",
"description": "# Create first Purchase Order\n\nPurchase Order is at the heart of your buying transactions. In ERPNext, Purchase Order can can be created against a Purchase Material Request (indent) and Supplier Quotation as well. Purchase Orders is also linked to Purchase Receipt and Purchase Invoices, allowing you to keep a birds-eye view on your purchase deals.\n\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-12 18:31:56.856112",
"modified": "2021-08-24 18:08:08.936484",
"modified_by": "Administrator",
"name": "Create your first Purchase Order",
"owner": "Administrator",
"reference_document": "Purchase Order",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create your first Purchase Order",
"title": "Create first Purchase Order",
"validate_action": 1
}

View File

@@ -1,19 +1,22 @@
{
"action": "Watch Video",
"action": "Show Form Tour",
"action_label": "Let\u2019s walk-through few Buying Settings",
"creation": "2020-05-06 15:37:09.477765",
"description": "# Buying Settings\n\n\nBuying module\u2019s features are highly configurable as per your business needs. Buying Settings is the place where you can set your preferences for:\n\n- Supplier naming and default values\n- Billing and shipping preference in buying transactions\n\n\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2020-05-12 18:25:08.509900",
"modified": "2021-08-24 18:08:08.345735",
"modified_by": "Administrator",
"name": "Introduction to Buying",
"owner": "Administrator",
"show_full_form": 0,
"title": "Introduction to Buying",
"reference_document": "Buying Settings",
"show_form_tour": 1,
"show_full_form": 1,
"title": "Buying Settings",
"validate_action": 1,
"video_url": "https://youtu.be/efFajTTQBa8"
}

View File

@@ -45,7 +45,6 @@ class TestProcurementTracker(unittest.TestCase):
pr = make_purchase_receipt(po.name)
pr.get("items")[0].cost_center = "Main - _TPC"
pr.submit()
frappe.db.commit()
date_obj = datetime.date(datetime.now())
po.load_from_db()

View File

@@ -0,0 +1,29 @@
# Version 13.13.0 Release Notes
### Features & Enhancements
- HR Module onboarding ([#25741](https://github.com/frappe/erpnext/pull/25741))
- Tracking multiple rounds for the interview ([#25482](https://github.com/frappe/erpnext/pull/25482))
- HSN based tax breakup table check in GST Settings (India Localization) ([#27907](https://github.com/frappe/erpnext/pull/27907))
### Fixes
- To improve stock transactions added indexes in stock queries and speed up bin updation ([#27758](https://github.com/frappe/erpnext/pull/27758))
- Interstate internal transfer invoices not visible in GSTR-1 ([#27970](https://github.com/frappe/erpnext/pull/27970))
- Account number and name incorrectly imported using COA importer ([#27967](https://github.com/frappe/erpnext/pull/27967))
- Multiple fixes to timesheets ([#27775](https://github.com/frappe/erpnext/pull/27742))
- Totals row incorrect value in GL Entry ([#27867](https://github.com/frappe/erpnext/pull/27867))
- Sales Order delivery Date not getting set via data import ([#27862](https://github.com/frappe/erpnext/pull/27862))
- Add cost center in gl entry for advance payment entry ([#27840](https://github.com/frappe/erpnext/pull/27840))
- Item Variant selection empty popup on website ([#27924](https://github.com/frappe/erpnext/pull/27924))
- Improve performance of fetching account balance in chart of accounts ([#27661](https://github.com/frappe/erpnext/pull/27661))
- Chart Of Accounts import button not visible ([#27748](https://github.com/frappe/erpnext/pull/27748))
- Website Items with same Item name unhandled, thumbnails missing ([#27720](https://github.com/frappe/erpnext/pull/27720))
- Delete linked Transaction Deletion Record docs on deleting company ([#27785](https://github.com/frappe/erpnext/pull/27785))
- Display appropriate message for Payment Term discrepancies in Payment Entry ([#27749](https://github.com/frappe/erpnext/pull/27749))
- Updated buying onboarding tours. ([#27800](https://github.com/frappe/erpnext/pull/27800))
- Fixed variant qty in BOM while making work order ([#27686](https://github.com/frappe/erpnext/pull/27686))
- Availability slots display, disabled Practitioner Schedule ([#27812](https://github.com/frappe/erpnext/pull/27812))
- Consolidated report not consider company currency ([#27863](https://github.com/frappe/erpnext/pull/27863))
- Batch Number not copied from Purchase Receipt to Stock Entry ([#27794](https://github.com/frappe/erpnext/pull/27794))
- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation ([#27728](https://github.com/frappe/erpnext/pull/27728))

View File

@@ -1691,17 +1691,58 @@ 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()
for doctype in ("Sales Invoice", "Purchase Invoice"):
frappe.db.sql("""
update `tab{}` as dt set dt.status = 'Overdue'
where dt.docstatus = 1
and dt.status != 'Overdue'
and dt.outstanding_amount > 0
and (dt.grand_total - dt.outstanding_amount) <
(select sum(payment_amount) from `tabPayment Schedule` as ps
where ps.parent = dt.name and ps.due_date < %s)
""".format(doctype), getdate())
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}
)
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):

View File

@@ -79,8 +79,15 @@ class StockController(AccountsController):
def clean_serial_nos(self):
for row in self.get("items"):
if hasattr(row, "serial_no") and row.serial_no:
# replace commas by linefeed and remove all spaces in string
row.serial_no = row.serial_no.replace(",", "\n").replace(" ", "")
# replace commas by linefeed
row.serial_no = row.serial_no.replace(",", "\n")
# 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)
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None):
@@ -591,7 +598,7 @@ def future_sle_exists(args, sl_entries=None):
data = frappe.db.sql("""
select item_code, warehouse, count(name) as total_row
from `tabStock Ledger Entry`
from `tabStock Ledger Entry` force index (item_warehouse)
where
({})
and timestamp(posting_date, posting_time)

View File

@@ -34,6 +34,7 @@ class Opportunity(TransactionBase):
self.validate_item_details()
self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name()
self.map_fields()
if not self.title:
self.title = self.customer_name
@@ -41,6 +42,15 @@ class Opportunity(TransactionBase):
if not self.with_items:
self.items = []
def map_fields(self):
for field in self.meta.fields:
if not self.get(field.fieldname):
try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname)
frappe.db.set(self, field.fieldname, value)
except Exception:
continue
def make_new_lead_if_required(self):
"""Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email:

View File

@@ -41,7 +41,6 @@ class TestECommerceSettings(unittest.TestCase):
def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
frappe.db.commit()
cart_settings = self.get_cart_settings()
cart_settings.enabled = 1

View File

@@ -147,7 +147,7 @@ class WebsiteItem(WebsiteGenerator):
def make_thumbnail(self):
"""Make a thumbnail of `website_image`"""
if frappe.flags.in_import:
if frappe.flags.in_import or frappe.flags.in_migrate:
return
import requests.exceptions

View File

@@ -138,7 +138,9 @@ class Student(Document):
enrollment.submit()
return enrollment
def enroll_in_course(self, course_name, program_enrollment, enrollment_date=frappe.utils.datetime.datetime.now()):
def enroll_in_course(self, course_name, program_enrollment, enrollment_date=None):
if enrollment_date is None:
enrollment_date = frappe.utils.datetime.datetime.now()
try:
enrollment = frappe.get_doc({
"doctype": "Course Enrollment",

View File

@@ -62,7 +62,9 @@ class InpatientRecord(Document):
admit_patient(self, service_unit, check_in, expected_discharge)
@frappe.whitelist()
def discharge(self, check_out=now_datetime()):
def discharge(self, check_out=None):
if not check_out:
check_out = now_datetime()
if (getdate(check_out) < getdate(self.admitted_datetime)):
frappe.throw(_('Discharge date cannot be less than Admission date'))
discharge_patient(self, check_out)

View File

@@ -433,11 +433,12 @@ let check_and_set_availability = function(frm) {
slot_html += `<br><span> <b> ${__('Maximum Capacity:')} </b> ${slot_info.service_unit_capacity} </span>`;
}
slot_html += '</div><br><br>';
slot_html += '</div><br>';
slot_html += slot_info.avail_slot.map(slot => {
appointment_count = 0;
disabled = false;
count_class = tool_tip = '';
start_str = slot.from_time;
slot_start_time = moment(slot.from_time, 'HH:mm:ss');
slot_end_time = moment(slot.to_time, 'HH:mm:ss');
@@ -486,10 +487,11 @@ let check_and_set_availability = function(frm) {
data-duration=${interval}
data-service-unit="${slot_info.service_unit || ''}"
style="margin: 0 10px 10px 0; width: auto;" ${disabled ? 'disabled="disabled"' : ""}
data-toggle="tooltip" title="${tool_tip}">
${start_str.substring(0, start_str.length - 3)}<br>
<span class='badge ${count_class}'> ${count} </span>
data-toggle="tooltip" title="${tool_tip || ''}">
${start_str.substring(0, start_str.length - 3)}
${slot_info.service_unit_capacity ? `<br><span class='badge ${count_class}'> ${count} </span>` : ''}
</button>`;
}).join("");
if (slot_info.service_unit_capacity) {

View File

@@ -354,7 +354,7 @@ def get_available_slots(practitioner_doc, date):
validate_practitioner_schedules(schedule_entry, practitioner)
practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule)
if practitioner_schedule:
if practitioner_schedule and not practitioner_schedule.disabled:
available_slots = []
for time_slot in practitioner_schedule.time_slots:
if weekday == time_slot.day:

View File

@@ -21,6 +21,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
def setUp(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
frappe.db.sql('delete from `tabPatient Appointment`')
make_pos_profile()
def test_medical_record(self):

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import flt, getdate, nowdate
from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
create_appointment,
@@ -33,10 +33,12 @@ class TestTherapyPlan(unittest.TestCase):
self.assertEqual(plan.status, 'Not Started')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
session.start_date = getdate()
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
session.start_date = add_days(getdate(), 1)
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
@@ -44,6 +46,7 @@ class TestTherapyPlan(unittest.TestCase):
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
session.start_date = add_days(getdate(), 2)
session = frappe.get_doc(session)
session.submit()
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import flt, today
from frappe.utils import flt
class TherapyPlan(Document):
@@ -63,8 +63,6 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company, appointme
therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
if frappe.flags.in_test:
therapy_session.start_date = today()
return therapy_session.as_dict()

View File

@@ -611,7 +611,7 @@ def render_docs_as_html(docs):
@frappe.whitelist()
def render_doc_as_html(doctype, docname, exclude_fields = []):
def render_doc_as_html(doctype, docname, exclude_fields = None):
"""
Render document as HTML
"""
@@ -622,6 +622,9 @@ def render_doc_as_html(doctype, docname, exclude_fields = []):
sec_on = has_data = False
col_on = 0
if exclude_fields is None:
exclude_fields = []
for df in meta.fields:
# on section break append previous section and html to doc html
if df.fieldtype == "Section Break":

View File

@@ -338,6 +338,7 @@ scheduler_events = {
"all": [
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
"erpnext.hr.doctype.interview.interview.send_interview_reminder",
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
],
"hourly": [
@@ -383,6 +384,7 @@ scheduler_events = {
"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.hr.doctype.interview.interview.send_daily_feedback_reminder"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",

View File

@@ -9,83 +9,86 @@ frappe.listview_settings['Attendance'] = {
return [__(doc.status), "orange", "status,=," + doc.status];
}
},
onload: function(list_view) {
let me = this;
const months = moment.months()
list_view.page.add_inner_button( __("Mark Attendance"), function() {
const months = moment.months();
list_view.page.add_inner_button(__("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
fields: [
{
fieldname: 'employee',
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"}
},
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("month", "value", '');
fields: [{
fieldname: 'employee',
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"};
},
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
}
},
{
label: __("For Month"),
fieldtype: "Select",
fieldname: "month",
options: months,
reqd: 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).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: __("For Month"),
fieldtype: "Select",
fieldname: "month",
options: months,
reqd: 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).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: __("Status"),
fieldtype: "Select",
fieldname: "status",
options: ["Present", "Absent", "Half Day", "Work From Home"],
hidden:1,
reqd: 1,
}
},
{
label: __("Status"),
fieldtype: "Select",
fieldname: "status",
options: ["Present", "Absent", "Half Day", "Work From Home"],
hidden: 1,
reqd: 1,
},
{
label: __("Unmarked Attendance for days"),
fieldname: "unmarked_days",
fieldtype: "MultiCheck",
options: [],
columns: 2,
hidden: 1
},
],
primary_action(data) {
},
{
label: __("Unmarked Attendance for days"),
fieldname: "unmarked_days",
fieldtype: "MultiCheck",
options: [],
columns: 2,
hidden: 1
}],
primary_action(data) {
if (cur_dialog.no_unmarked_days_left) {
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
} else {
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
args: {
data: data
},
callback: function(r) {
callback: function (r) {
if (r.message === 1) {
frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
frappe.show_alert({
message: __("Attendance Marked"),
indicator: 'blue'
});
cur_dialog.hide();
}
}
@@ -101,21 +104,26 @@ frappe.listview_settings['Attendance'] = {
dialog.show();
});
},
get_multi_select_options: function(employee, month){
get_multi_select_options: function(employee, month) {
return new Promise(resolve => {
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
async: false,
args:{
args: {
employee: employee,
month: month,
}
}).then(r => {
var options = [];
for(var d in r.message){
for (var d in r.message) {
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
var date = momentObj.format('DD-MM-YYYY');
options.push({ "label":date, "value": r.message[d] , "checked": 1});
options.push({
"label": date,
"value": r.message[d],
"checked": 1
});
}
resolve(options);
});

View File

@@ -74,7 +74,6 @@ class TestDailyWorkSummary(unittest.TestCase):
from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \
where q.name = r.parent""", as_dict=1)
frappe.db.commit()
def setup_groups(self, hour=None):
# setup email to trigger at this hour

View File

@@ -4,40 +4,46 @@
frappe.provide("erpnext.hr");
erpnext.hr.EmployeeController = frappe.ui.form.Controller.extend({
setup: function() {
this.frm.fields_dict.user_id.get_query = function(doc, cdt, cdn) {
this.frm.fields_dict.user_id.get_query = function() {
return {
query: "frappe.core.doctype.user.user.user_query",
filters: {ignore_user_type: 1}
}
}
this.frm.fields_dict.reports_to.get_query = function(doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.employee_query"} }
filters: {
ignore_user_type: 1
}
};
};
this.frm.fields_dict.reports_to.get_query = function() {
return {
query: "erpnext.controllers.queries.employee_query"
};
};
},
refresh: function() {
var me = this;
erpnext.toggle_naming_series();
},
date_of_birth: function() {
return cur_frm.call({
method: "get_retirement_date",
args: {date_of_birth: this.frm.doc.date_of_birth}
args: {
date_of_birth: this.frm.doc.date_of_birth
}
});
},
salutation: function() {
if(this.frm.doc.salutation) {
if (this.frm.doc.salutation) {
this.frm.set_value("gender", {
"Mr": "Male",
"Ms": "Female"
}[this.frm.doc.salutation]);
} [this.frm.doc.salutation]);
}
},
});
frappe.ui.form.on('Employee',{
setup: function(frm) {
frappe.ui.form.on('Employee', {
setup: function (frm) {
frm.set_query("leave_policy", function() {
return {
"filters": {
@@ -46,7 +52,7 @@ frappe.ui.form.on('Employee',{
};
});
},
onload:function(frm) {
onload: function (frm) {
frm.set_query("department", function() {
return {
"filters": {
@@ -55,23 +61,28 @@ frappe.ui.form.on('Employee',{
};
});
},
prefered_contact_email:function(frm){
frm.events.update_contact(frm)
prefered_contact_email: function(frm) {
frm.events.update_contact(frm);
},
personal_email:function(frm){
frm.events.update_contact(frm)
personal_email: function(frm) {
frm.events.update_contact(frm);
},
company_email:function(frm){
frm.events.update_contact(frm)
company_email: function(frm) {
frm.events.update_contact(frm);
},
user_id:function(frm){
frm.events.update_contact(frm)
user_id: function(frm) {
frm.events.update_contact(frm);
},
update_contact:function(frm){
update_contact: function(frm) {
var prefered_email_fieldname = frappe.model.scrub(frm.doc.prefered_contact_email) || 'user_id';
frm.set_value("prefered_email",
frm.fields_dict[prefered_email_fieldname].value)
frm.fields_dict[prefered_email_fieldname].value);
},
status: function(frm) {
return frm.call({
method: "deactivate_sales_person",
@@ -81,19 +92,63 @@ frappe.ui.form.on('Employee',{
}
});
},
create_user: function(frm) {
if (!frm.doc.prefered_email)
{
frappe.throw(__("Please enter Preferred Contact Email"))
if (!frm.doc.prefered_email) {
frappe.throw(__("Please enter Preferred Contact Email"));
}
frappe.call({
method: "erpnext.hr.doctype.employee.employee.create_user",
args: { employee: frm.doc.name, email: frm.doc.prefered_email },
callback: function(r)
{
frm.set_value("user_id", r.message)
args: {
employee: frm.doc.name,
email: frm.doc.prefered_email
},
callback: function (r) {
frm.set_value("user_id", r.message);
}
});
}
});
cur_frm.cscript = new erpnext.hr.EmployeeController({frm: cur_frm});
cur_frm.cscript = new erpnext.hr.EmployeeController({
frm: cur_frm
});
frappe.tour['Employee'] = [
{
fieldname: "first_name",
title: "First Name",
description: __("Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched.")
},
{
fieldname: "company",
title: "Company",
description: __("Select a Company this Employee belongs to. Other HR features like Payroll. Expense Claims and Leaves for this Employee will be created for a given company only.")
},
{
fieldname: "date_of_birth",
title: "Date of Birth",
description: __("Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.")
},
{
fieldname: "date_of_joining",
title: "Date of Joining",
description: __("Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.")
},
{
fieldname: "holiday_list",
title: "Holiday List",
description: __("Select a default Holiday List for this Employee. The days listed in Holiday List will not be counted in Leave Application.")
},
{
fieldname: "reports_to",
title: "Reports To",
description: __("Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated.")
},
{
fieldname: "leave_approver",
title: "Leave Approver",
description: __("Select Leave Approver for an employee. The user one who will look after his/her Leave application")
},
];

View File

@@ -72,6 +72,7 @@ def get_job_applicant():
applicant = frappe.new_doc('Job Applicant')
applicant.applicant_name = 'Test Researcher'
applicant.email_id = 'test@researcher.com'
applicant.designation = 'Researcher'
applicant.status = 'Open'
applicant.cover_letter = 'I am a great Researcher.'
applicant.insert()

View File

@@ -38,8 +38,10 @@ def create_job_applicant(source_name, target_doc=None):
status = "Open"
job_applicant = frappe.new_doc("Job Applicant")
job_applicant.source = "Employee Referral"
job_applicant.employee_referral = emp_ref.name
job_applicant.status = status
job_applicant.designation = emp_ref.for_designation
job_applicant.applicant_name = emp_ref.full_name
job_applicant.email_id = emp_ref.email
job_applicant.phone_number = emp_ref.contact_no

View File

@@ -17,6 +17,11 @@ from erpnext.hr.doctype.employee_referral.employee_referral import (
class TestEmployeeReferral(unittest.TestCase):
def setUp(self):
frappe.db.sql("DELETE FROM `tabJob Applicant`")
frappe.db.sql("DELETE FROM `tabEmployee Referral`")
def test_workflow_and_status_sync(self):
emp_ref = create_employee_referral()
@@ -50,6 +55,10 @@ class TestEmployeeReferral(unittest.TestCase):
add_sal = create_additional_salary(emp_ref)
self.assertTrue(add_sal.ref_docname, emp_ref.name)
def tearDown(self):
frappe.db.sql("DELETE FROM `tabJob Applicant`")
frappe.db.sql("DELETE FROM `tabEmployee Referral`")
def create_employee_referral():
emp_ref = frappe.new_doc("Employee Referral")

View File

@@ -0,0 +1,40 @@
{
"actions": [],
"creation": "2021-04-12 13:05:06.741330",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"skill",
"description"
],
"fields": [
{
"fieldname": "skill",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Skill",
"options": "Skill",
"reqd": 1
},
{
"fetch_from": "skill.description",
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-12 14:26:33.062549",
"modified_by": "Administrator",
"module": "HR",
"name": "Expected Skill Set",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -10,6 +10,26 @@ frappe.ui.form.on('Expense Claim', {
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
var expenses = frm.doc.expenses;
for (var i = 0; i < expenses.length; i++) {
var expense = expenses[i];
if (!expense.expense_type) {
continue;
}
frappe.call({
method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center",
args: {
"expense_claim_type": expense.expense_type,
"company": frm.doc.company
},
callback: function(r) {
if (r.message) {
expense.default_account = r.message.account;
expense.cost_center = r.message.cost_center;
}
}
});
}
},
});

View File

@@ -176,7 +176,7 @@ def generate_taxes():
account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
return {'taxes':[{
"account_head": account,
"rate": 0,
"rate": 9,
"description": "CGST",
"tax_amount": 10,
"total": 210

View File

@@ -56,8 +56,6 @@
},
{
"columns": 2,
"fetch_from": "account_head.tax_rate",
"fetch_if_empty": 1,
"fieldname": "rate",
"fieldtype": "Float",
"in_list_view": 1,
@@ -111,4 +109,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View File

@@ -1,10 +1,10 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Holiday List', {
frappe.ui.form.on("Holiday List", {
refresh: function(frm) {
if (frm.doc.holidays) {
frm.set_value('total_holidays', frm.doc.holidays.length);
frm.set_value("total_holidays", frm.doc.holidays.length);
}
},
from_date: function(frm) {
@@ -14,3 +14,36 @@ frappe.ui.form.on('Holiday List', {
}
}
});
frappe.tour["Holiday List"] = [
{
fieldname: "holiday_list_name",
title: "Holiday List Name",
description: __("Enter a name for this Holiday List."),
},
{
fieldname: "from_date",
title: "From Date",
description: __("Based on your HR Policy, select your leave allocation period's start date"),
},
{
fieldname: "to_date",
title: "To Date",
description: __("Based on your HR Policy, select your leave allocation period's end date"),
},
{
fieldname: "weekly_off",
title: "Weekly Off",
description: __("Select your weekly off day"),
},
{
fieldname: "get_weekly_off_dates",
title: "Add Holidays",
description: __("Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays"),
},
{
fieldname: "holidays",
title: "Holidays",
description: __("Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually.")
},
];

View File

@@ -1,4 +1,3 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
@@ -94,9 +93,11 @@ def get_events(start, end, filters=None):
update={"allDay": 1})
def is_holiday(holiday_list, date=today()):
def is_holiday(holiday_list, date=None):
"""Returns true if the given date is a holiday in the given holiday list
"""
if date is None:
date = today()
if holiday_list:
return bool(frappe.get_all('Holiday List',
dict(name=holiday_list, holiday_date=date)))

View File

@@ -2,7 +2,22 @@
// For license information, please see license.txt
frappe.ui.form.on('HR Settings', {
restrict_backdated_leave_application: function(frm) {
frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application);
}
});
frappe.tour['HR Settings'] = [
{
fieldname: 'emp_created_by',
title: 'Employee Naming By',
description: __('Employee can be named by Employee ID if you assign one, or via Naming Series. Select your preference here.'),
},
{
fieldname: 'standard_working_hours',
title: 'Standard Working Hours',
description: __('Enter the Standard Working Hours for a normal work day. These hours will be used in calculations of reports such as Employee Hours Utilization and Project Profitability analysis.'),
},
{
fieldname: 'leave_and_expense_claim_settings',
title: 'Leave and Expense Clain Settings',
description: __('Review various other settings related to Employee Leaves and Expense Claim')
}
];

View File

@@ -7,30 +7,36 @@
"engine": "InnoDB",
"field_order": [
"employee_settings",
"retirement_age",
"emp_created_by",
"column_break_4",
"standard_working_hours",
"expense_approver_mandatory_in_expense_claim",
"column_break_9",
"retirement_age",
"reminders_section",
"send_birthday_reminders",
"column_break_9",
"send_work_anniversary_reminders",
"column_break_11",
"send_work_anniversary_reminders",
"column_break_18",
"send_holiday_reminders",
"frequency",
"leave_settings",
"leave_and_expense_claim_settings",
"send_leave_notification",
"leave_approval_notification_template",
"leave_status_notification_template",
"role_allowed_to_create_backdated_leave_application",
"column_break_18",
"leave_approver_mandatory_in_leave_application",
"restrict_backdated_leave_application",
"role_allowed_to_create_backdated_leave_application",
"column_break_29",
"expense_approver_mandatory_in_expense_claim",
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"restrict_backdated_leave_application",
"hiring_settings",
"check_vacancies"
"hiring_settings_section",
"check_vacancies",
"send_interview_reminder",
"interview_reminder_template",
"remind_before",
"column_break_4",
"send_interview_feedback_reminder",
"feedback_reminder_notification_template"
],
"fields": [
{
@@ -39,17 +45,16 @@
"label": "Employee Settings"
},
{
"description": "Enter retirement age in years",
"fieldname": "retirement_age",
"fieldtype": "Data",
"label": "Retirement Age"
"label": "Retirement Age (In Years)"
},
{
"default": "Naming Series",
"description": "Employee records are created using the selected field",
"description": "Employee records are created using the selected option",
"fieldname": "emp_created_by",
"fieldtype": "Select",
"label": "Employee Records to be created by",
"label": "Employee Naming By",
"options": "Naming Series\nEmployee Number\nFull Name"
},
{
@@ -62,28 +67,6 @@
"fieldtype": "Check",
"label": "Expense Approver Mandatory In Expense Claim"
},
{
"collapsible": 1,
"fieldname": "leave_settings",
"fieldtype": "Section Break",
"label": "Leave Settings"
},
{
"depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
"depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
@@ -100,35 +83,18 @@
"fieldtype": "Check",
"label": "Show Leaves Of All Department Members In Calendar"
},
{
"collapsible": 1,
"fieldname": "hiring_settings",
"fieldtype": "Section Break",
"label": "Hiring Settings"
},
{
"default": "0",
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
},
{
"default": "0",
"fieldname": "auto_leave_encashment",
"fieldtype": "Check",
"label": "Auto Leave Encashment"
},
{
"default": "0",
"fieldname": "restrict_backdated_leave_application",
"fieldtype": "Check",
"label": "Restrict Backdated Leave Application"
},
{
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
"fieldname": "role_allowed_to_create_backdated_leave_application",
"fieldtype": "Link",
"label": "Role Allowed to Create Backdated Leave Application",
"mandatory_depends_on": "eval:doc.restrict_backdated_leave_application == 1",
"options": "Role"
},
{
@@ -137,11 +103,40 @@
"fieldtype": "Check",
"label": "Send Leave Notification"
},
{
"depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
"depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
"fieldname": "standard_working_hours",
"fieldtype": "Int",
"label": "Standard Working Hours"
},
{
"collapsible": 1,
"fieldname": "leave_and_expense_claim_settings",
"fieldtype": "Section Break",
"label": "Leave and Expense Claim Settings"
},
{
"default": "00:15:00",
"depends_on": "send_interview_reminder",
"fieldname": "remind_before",
"fieldtype": "Time",
"label": "Remind Before"
},
{
"collapsible": 1,
"fieldname": "reminders_section",
@@ -166,6 +161,7 @@
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Set the frequency for holiday reminders",
"mandatory_depends_on": "send_holiday_reminders",
"options": "Weekly\nMonthly"
},
{
@@ -181,13 +177,62 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "send_interview_reminder",
"fieldtype": "Check",
"label": "Send Interview Reminder"
},
{
"default": "0",
"fieldname": "send_interview_feedback_reminder",
"fieldtype": "Check",
"label": "Send Interview Feedback Reminder"
},
{
"fieldname": "column_break_29",
"fieldtype": "Column Break"
},
{
"depends_on": "send_interview_feedback_reminder",
"fieldname": "feedback_reminder_notification_template",
"fieldtype": "Link",
"label": "Feedback Reminder Notification Template",
"mandatory_depends_on": "send_interview_feedback_reminder",
"options": "Email Template"
},
{
"depends_on": "send_interview_reminder",
"fieldname": "interview_reminder_template",
"fieldtype": "Link",
"label": "Interview Reminder Notification Template",
"mandatory_depends_on": "send_interview_reminder",
"options": "Email Template"
},
{
"default": "0",
"fieldname": "restrict_backdated_leave_application",
"fieldtype": "Check",
"label": "Restrict Backdated Leave Application"
},
{
"fieldname": "hiring_settings_section",
"fieldtype": "Section Break",
"label": "Hiring Settings"
},
{
"default": "0",
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-08-24 14:54:12.834162",
"modified": "2021-10-01 23:46:11.098236",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

View File

View File

@@ -0,0 +1,237 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Interview', {
onload: function (frm) {
frm.events.set_job_applicant_query(frm);
frm.set_query('interviewer', 'interview_details', function () {
return {
query: 'erpnext.hr.doctype.interview.interview.get_interviewer_list'
};
});
},
refresh: function (frm) {
if (frm.doc.docstatus != 2 && !frm.doc.__islocal) {
if (frm.doc.status === 'Pending') {
frm.add_custom_button(__('Reschedule Interview'), function() {
frm.events.show_reschedule_dialog(frm);
frm.refresh();
});
}
let allowed_interviewers = [];
frm.doc.interview_details.forEach(values => {
allowed_interviewers.push(values.interviewer);
});
if ((allowed_interviewers.includes(frappe.session.user))) {
frappe.db.get_value('Interview Feedback', {'interviewer': frappe.session.user, 'interview': frm.doc.name, 'docstatus': 1}, 'name', (r) => {
if (Object.keys(r).length === 0) {
frm.add_custom_button(__('Submit Feedback'), function () {
frappe.call({
method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
args: {
interview_round: frm.doc.interview_round
},
callback: function (r) {
frm.events.show_feedback_dialog(frm, r.message);
frm.refresh();
}
});
}).addClass('btn-primary');
}
});
}
}
},
show_reschedule_dialog: function (frm) {
let d = new frappe.ui.Dialog({
title: 'Reschedule Interview',
fields: [
{
label: 'Schedule On',
fieldname: 'scheduled_on',
fieldtype: 'Date',
reqd: 1
},
{
label: 'From Time',
fieldname: 'from_time',
fieldtype: 'Time',
reqd: 1
},
{
label: 'To Time',
fieldname: 'to_time',
fieldtype: 'Time',
reqd: 1
}
],
primary_action_label: 'Reschedule',
primary_action(values) {
frm.call({
method: 'reschedule_interview',
doc: frm.doc,
args: {
scheduled_on: values.scheduled_on,
from_time: values.from_time,
to_time: values.to_time
}
}).then(() => {
frm.refresh();
d.hide();
});
}
});
d.show();
},
show_feedback_dialog: function (frm, data) {
let fields = frm.events.get_fields_for_feedback();
let d = new frappe.ui.Dialog({
title: __('Submit Feedback'),
fields: [
{
fieldname: 'skill_set',
fieldtype: 'Table',
label: __('Skill Assessment'),
cannot_add_rows: false,
in_editable_grid: true,
reqd: 1,
fields: fields,
data: data
},
{
fieldname: 'result',
fieldtype: 'Select',
options: ['', 'Cleared', 'Rejected'],
label: __('Result')
},
{
fieldname: 'feedback',
fieldtype: 'Small Text',
label: __('Feedback')
}
],
size: 'large',
minimizable: true,
primary_action: function(values) {
frappe.call({
method: 'erpnext.hr.doctype.interview.interview.create_interview_feedback',
args: {
data: values,
interview_name: frm.doc.name,
interviewer: frappe.session.user,
job_applicant: frm.doc.job_applicant
}
}).then(() => {
frm.refresh();
});
d.hide();
}
});
d.show();
},
get_fields_for_feedback: function () {
return [{
fieldtype: 'Link',
fieldname: 'skill',
options: 'Skill',
in_list_view: 1,
label: __('Skill')
}, {
fieldtype: 'Rating',
fieldname: 'rating',
label: __('Rating'),
in_list_view: 1,
reqd: 1,
}];
},
set_job_applicant_query: function (frm) {
frm.set_query('job_applicant', function () {
let job_applicant_filters = {
status: ['!=', 'Rejected']
};
if (frm.doc.designation) {
job_applicant_filters.designation = frm.doc.designation;
}
return {
filters: job_applicant_filters
};
});
},
interview_round: async function (frm) {
frm.events.reset_values(frm);
frm.set_value('job_applicant', '');
let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
frm.set_value('designation', round_data.designation);
frm.events.set_job_applicant_query(frm);
if (frm.doc.interview_round) {
frm.events.set_interview_details(frm);
} else {
frm.set_value('interview_details', []);
}
},
set_interview_details: function (frm) {
frappe.call({
method: 'erpnext.hr.doctype.interview.interview.get_interviewers',
args: {
interview_round: frm.doc.interview_round
},
callback: function (data) {
let interview_details = data.message;
frm.set_value('interview_details', []);
if (data.message.length) {
frm.set_value('interview_details', interview_details);
}
}
});
},
job_applicant: function (frm) {
if (!frm.doc.interview_round) {
frm.doc.job_applicant = '';
frm.refresh();
frappe.throw(__('Select Interview Round First'));
}
if (frm.doc.job_applicant) {
frm.events.set_designation_and_job_opening(frm);
} else {
frm.events.reset_values(frm);
}
},
set_designation_and_job_opening: async function (frm) {
let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
frm.set_value('designation', round_data.designation);
frm.events.set_job_applicant_query(frm);
let job_applicant_data = (await frappe.db.get_value(
'Job Applicant', frm.doc.job_applicant, ['designation', 'job_title', 'resume_link'],
)).message;
if (!round_data.designation) {
frm.set_value('designation', job_applicant_data.designation);
}
frm.set_value('job_opening', job_applicant_data.job_title);
frm.set_value('resume_link', job_applicant_data.resume_link);
},
reset_values: function (frm) {
frm.set_value('designation', '');
frm.set_value('job_opening', '');
frm.set_value('resume_link', '');
}
});

View File

@@ -0,0 +1,254 @@
{
"actions": [],
"autoname": "HR-INT-.YYYY.-.####",
"creation": "2021-04-12 15:03:11.524090",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"interview_details_section",
"interview_round",
"job_applicant",
"job_opening",
"designation",
"resume_link",
"column_break_4",
"status",
"scheduled_on",
"from_time",
"to_time",
"interview_feedback_section",
"interview_details",
"ratings_section",
"expected_average_rating",
"column_break_12",
"average_rating",
"section_break_13",
"interview_summary",
"reminded",
"amended_from"
],
"fields": [
{
"fieldname": "job_applicant",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Job Applicant",
"options": "Job Applicant",
"reqd": 1
},
{
"fieldname": "job_opening",
"fieldtype": "Link",
"label": "Job Opening",
"options": "Job Opening",
"read_only": 1
},
{
"fieldname": "interview_round",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Interview Round",
"options": "Interview Round",
"reqd": 1
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Pending\nUnder Review\nCleared\nRejected",
"reqd": 1
},
{
"fieldname": "ratings_section",
"fieldtype": "Section Break",
"label": "Ratings"
},
{
"allow_on_submit": 1,
"fieldname": "average_rating",
"fieldtype": "Rating",
"in_list_view": 1,
"label": "Obtained Average Rating",
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "interview_summary",
"fieldtype": "Text"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "resume_link",
"fieldtype": "Data",
"label": "Resume link"
},
{
"fieldname": "interview_details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"fetch_from": "interview_round.expected_average_rating",
"fieldname": "expected_average_rating",
"fieldtype": "Rating",
"label": "Expected Average Rating",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_13",
"fieldtype": "Section Break",
"label": "Interview Summary"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fetch_from": "interview_round.designation",
"fieldname": "designation",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Interview",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "scheduled_on",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Scheduled On",
"reqd": 1,
"set_only_once": 1
},
{
"default": "0",
"fieldname": "reminded",
"fieldtype": "Check",
"hidden": 1,
"label": "Reminded"
},
{
"allow_on_submit": 1,
"fieldname": "interview_details",
"fieldtype": "Table",
"options": "Interview Detail"
},
{
"fieldname": "interview_feedback_section",
"fieldtype": "Section Break",
"label": "Feedback"
},
{
"fieldname": "from_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "From Time",
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "to_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "To Time",
"reqd": 1,
"set_only_once": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [
{
"link_doctype": "Interview Feedback",
"link_fieldname": "interview"
}
],
"modified": "2021-09-30 13:30:05.421035",
"modified_by": "Administrator",
"module": "HR",
"name": "Interview",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Interviewer",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "job_applicant",
"track_changes": 1
}

View File

@@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import datetime
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, get_link_to_form
class DuplicateInterviewRoundError(frappe.ValidationError):
pass
class Interview(Document):
def validate(self):
self.validate_duplicate_interview()
self.validate_designation()
self.validate_overlap()
def on_submit(self):
if self.status not in ['Cleared', 'Rejected']:
frappe.throw(_('Only Interviews with Cleared or Rejected status can be submitted.'), title=_('Not Allowed'))
def validate_duplicate_interview(self):
duplicate_interview = frappe.db.exists('Interview', {
'job_applicant': self.job_applicant,
'interview_round': self.interview_round,
'docstatus': 1
}
)
if duplicate_interview:
frappe.throw(_('Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}').format(
frappe.bold(get_link_to_form('Interview', duplicate_interview)),
frappe.bold(self.job_applicant)
))
def validate_designation(self):
applicant_designation = frappe.db.get_value('Job Applicant', self.job_applicant, 'designation')
if self.designation :
if self.designation != applicant_designation:
frappe.throw(_('Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}').format(
self.interview_round, frappe.bold(self.designation), applicant_designation),
exc=DuplicateInterviewRoundError)
else:
self.designation = applicant_designation
def validate_overlap(self):
interviewers = [entry.interviewer for entry in self.interview_details] or ['']
overlaps = frappe.db.sql("""
SELECT interview.name
FROM `tabInterview` as interview
INNER JOIN `tabInterview Detail` as detail
WHERE
interview.scheduled_on = %s and interview.name != %s and interview.docstatus != 2
and (interview.job_applicant = %s or detail.interviewer IN %s) and
((from_time < %s and to_time > %s) or
(from_time > %s and to_time < %s) or
(from_time = %s))
""", (self.scheduled_on, self.name, self.job_applicant, interviewers,
self.from_time, self.to_time, self.from_time, self.to_time, self.from_time))
if overlaps:
overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
frappe.throw(overlapping_details, title=_('Overlap'))
@frappe.whitelist()
def reschedule_interview(self, scheduled_on, from_time, to_time):
original_date = self.scheduled_on
from_time = self.from_time
to_time = self.to_time
self.db_set({
'scheduled_on': scheduled_on,
'from_time': from_time,
'to_time': to_time
})
self.notify_update()
recipients = get_recipients(self.name)
try:
frappe.sendmail(
recipients= recipients,
subject=_('Interview: {0} Rescheduled').format(self.name),
message=_('Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}').format(
original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time),
reference_doctype=self.doctype,
reference_name=self.name
)
except Exception:
frappe.msgprint(_('Failed to send the Interview Reschedule notification. Please configure your email account.'))
frappe.msgprint(_('Interview Rescheduled successfully'), indicator='green')
def get_recipients(name, for_feedback=0):
interview = frappe.get_doc('Interview', name)
if for_feedback:
recipients = [d.interviewer for d in interview.interview_details if not d.interview_feedback]
else:
recipients = [d.interviewer for d in interview.interview_details]
recipients.append(frappe.db.get_value('Job Applicant', interview.job_applicant, 'email_id'))
return recipients
@frappe.whitelist()
def get_interviewers(interview_round):
return frappe.get_all('Interviewer', filters={'parent': interview_round}, fields=['user as interviewer'])
def send_interview_reminder():
reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
['send_interview_reminder', 'interview_reminder_template'], as_dict=True)
if not reminder_settings.send_interview_reminder:
return
remind_before = cstr(frappe.db.get_single_value('HR Settings', 'remind_before')) or '01:00:00'
remind_before = datetime.datetime.strptime(remind_before, '%H:%M:%S')
reminder_date_time = datetime.datetime.now() + datetime.timedelta(
hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second)
interviews = frappe.get_all('Interview', filters={
'scheduled_on': ['between', (datetime.datetime.now(), reminder_date_time)],
'status': 'Pending',
'reminded': 0,
'docstatus': ['!=', 2]
})
interview_template = frappe.get_doc('Email Template', reminder_settings.interview_reminder_template)
for d in interviews:
doc = frappe.get_doc('Interview', d.name)
context = doc.as_dict()
message = frappe.render_template(interview_template.response, context)
recipients = get_recipients(doc.name)
frappe.sendmail(
recipients= recipients,
subject=interview_template.subject,
message=message,
reference_doctype=doc.doctype,
reference_name=doc.name
)
doc.db_set('reminded', 1)
def send_daily_feedback_reminder():
reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
['send_interview_feedback_reminder', 'feedback_reminder_notification_template'], as_dict=True)
if not reminder_settings.send_interview_feedback_reminder:
return
interview_feedback_template = frappe.get_doc('Email Template', reminder_settings.feedback_reminder_notification_template)
interviews = frappe.get_all('Interview', filters={'status': ['in', ['Under Review', 'Pending']], 'docstatus': ['!=', 2]})
for entry in interviews:
recipients = get_recipients(entry.name, for_feedback=1)
doc = frappe.get_doc('Interview', entry.name)
context = doc.as_dict()
message = frappe.render_template(interview_feedback_template.response, context)
if len(recipients):
frappe.sendmail(
recipients= recipients,
subject=interview_feedback_template.subject,
message=message,
reference_doctype='Interview',
reference_name=entry.name
)
@frappe.whitelist()
def get_expected_skill_set(interview_round):
return frappe.get_all('Expected Skill Set', filters ={'parent': interview_round}, fields=['skill'])
@frappe.whitelist()
def create_interview_feedback(data, interview_name, interviewer, job_applicant):
import json
from six import string_types
if isinstance(data, string_types):
data = frappe._dict(json.loads(data))
if frappe.session.user != interviewer:
frappe.throw(_('Only Interviewer Are allowed to submit Interview Feedback'))
interview_feedback = frappe.new_doc('Interview Feedback')
interview_feedback.interview = interview_name
interview_feedback.interviewer = interviewer
interview_feedback.job_applicant = job_applicant
for d in data.skill_set:
d = frappe._dict(d)
interview_feedback.append('skill_assessment', {'skill': d.skill, 'rating': d.rating})
interview_feedback.feedback = data.feedback
interview_feedback.result = data.result
interview_feedback.save()
interview_feedback.submit()
frappe.msgprint(_('Interview Feedback {0} submitted successfully').format(
get_link_to_form('Interview Feedback', interview_feedback.name)))
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_interviewer_list(doctype, txt, searchfield, start, page_len, filters):
filters = [
['Has Role', 'parent', 'like', '%{}%'.format(txt)],
['Has Role', 'role', '=', 'interviewer'],
['Has Role', 'parenttype', '=', 'User']
]
if filters and isinstance(filters, list):
filters.extend(filters)
return frappe.get_all('Has Role', limit_start=start, limit_page_length=page_len,
filters=filters, fields = ['parent'], as_list=1)
@frappe.whitelist()
def get_events(start, end, filters=None):
"""Returns events for Gantt / Calendar view rendering.
:param start: Start date-time.
:param end: End date-time.
:param filters: Filters (JSON).
"""
from frappe.desk.calendar import get_event_conditions
events = []
event_color = {
"Pending": "#fff4f0",
"Under Review": "#d3e8fc",
"Cleared": "#eaf5ed",
"Rejected": "#fce7e7"
}
conditions = get_event_conditions('Interview', filters)
interviews = frappe.db.sql("""
SELECT DISTINCT
`tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round,
`tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time,
`tabInterview`.to_time as to_time
from
`tabInterview`
where
(`tabInterview`.scheduled_on between %(start)s and %(end)s)
and docstatus != 2
{conditions}
""".format(conditions=conditions), {
"start": start,
"end": end
}, as_dict=True, update={"allDay": 0})
for d in interviews:
subject_data = []
for field in ["name", "job_applicant", "interview_round"]:
if not d.get(field):
continue
subject_data.append(d.get(field))
color = event_color.get(d.status)
interview_data = {
'from': get_datetime('%s %s' % (d.scheduled_on, d.from_time or '00:00:00')),
'to': get_datetime('%s %s' % (d.scheduled_on, d.to_time or '00:00:00')),
'name': d.name,
'subject': '\n'.join(subject_data),
'color': color if color else "#89bcde"
}
events.append(interview_data)
return events

View File

@@ -0,0 +1,14 @@
frappe.views.calendar['Interview'] = {
field_map: {
'start': 'from',
'end': 'to',
'id': 'name',
'title': 'subject',
'allDay': 'allDay',
'color': 'color'
},
order_by: 'scheduled_on',
gantt: true,
get_events_method: 'erpnext.hr.doctype.interview.interview.get_events'
};

View File

@@ -0,0 +1,5 @@
<h1>Interview Feedback Reminder</h1>
<p>
Interview Feedback for Interview {{ name }} is not submitted yet. Please submit your feedback. Thank you, good day!
</p>

View File

@@ -0,0 +1,12 @@
frappe.listview_settings['Interview'] = {
has_indicator_for_draft: 1,
get_indicator: function(doc) {
let status_color = {
'Pending': 'orange',
'Under Review': 'blue',
'Cleared': 'green',
'Rejected': 'red',
};
return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
}
};

View File

@@ -0,0 +1,5 @@
<h1>Interview Reminder</h1>
<p>
Interview: {{name}} is scheduled on {{scheduled_on}} from {{from_time}} to {{to_time}}
</p>

View File

@@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import datetime
import os
import unittest
import frappe
from frappe import _
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.utils import add_days, getdate, nowtime
from erpnext.hr.doctype.designation.test_designation import create_designation
from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
class TestInterview(unittest.TestCase):
def test_validations_for_designation(self):
job_applicant = create_job_applicant()
interview = create_interview_and_dependencies(job_applicant.name, designation='_Test_Sales_manager', save=0)
self.assertRaises(DuplicateInterviewRoundError, interview.save)
def test_notification_on_rescheduling(self):
job_applicant = create_job_applicant()
interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -4))
previous_scheduled_date = interview.scheduled_on
frappe.db.sql("DELETE FROM `tabEmail Queue`")
interview.reschedule_interview(add_days(getdate(previous_scheduled_date), 2),
from_time=nowtime(), to_time=nowtime())
interview.reload()
self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2))
notification = frappe.get_all("Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")})
self.assertIsNotNone(notification)
def test_notification_for_scheduling(self):
from erpnext.hr.doctype.interview.interview import send_interview_reminder
setup_reminder_settings()
job_applicant = create_job_applicant()
scheduled_on = datetime.datetime.now() + datetime.timedelta(minutes=10)
interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
frappe.db.sql("DELETE FROM `tabEmail Queue`")
send_interview_reminder()
interview.reload()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Interview Reminder" in email_queue[0].message)
def test_notification_for_feedback_submission(self):
from erpnext.hr.doctype.interview.interview import send_daily_feedback_reminder
setup_reminder_settings()
job_applicant = create_job_applicant()
scheduled_on = add_days(getdate(), -4)
create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
frappe.db.sql("DELETE FROM `tabEmail Queue`")
send_daily_feedback_reminder()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message)
def tearDown(self):
frappe.db.rollback()
def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1):
if designation:
designation=create_designation(designation_name = "_Test_Sales_manager").name
interviewer_1 = create_user("test_interviewer1@example.com", "Interviewer")
interviewer_2 = create_user("test_interviewer2@example.com", "Interviewer")
interview_round = create_interview_round(
"Technical Round", ["Python", "JS"],
designation=designation, save=True
)
interview = frappe.new_doc("Interview")
interview.interview_round = interview_round.name
interview.job_applicant = job_applicant
interview.scheduled_on = scheduled_on or getdate()
interview.from_time = from_time or nowtime()
interview.to_time = to_time or nowtime()
interview.append("interview_details", {"interviewer": interviewer_1.name})
interview.append("interview_details", {"interviewer": interviewer_2.name})
if save:
interview.save()
return interview
def create_interview_round(name, skill_set, interviewers=[], designation=None, save=True):
create_skill_set(skill_set)
interview_round = frappe.new_doc("Interview Round")
interview_round.round_name = name
interview_round.interview_type = create_interview_type()
interview_round.expected_average_rating = 4
if designation:
interview_round.designation = designation
for skill in skill_set:
interview_round.append("expected_skill_set", {"skill": skill})
for interviewer in interviewers:
interview_round.append("interviewer", {
"user": interviewer
})
if save:
interview_round.save()
return interview_round
def create_skill_set(skill_set):
for skill in skill_set:
if not frappe.db.exists("Skill", skill):
doc = frappe.new_doc("Skill")
doc.skill_name = skill
doc.save()
def create_interview_type(name="test_interview_type"):
if frappe.db.exists("Interview Type", name):
return frappe.get_doc("Interview Type", name).name
else:
doc = frappe.new_doc("Interview Type")
doc.name = name
doc.description = "_Test_Description"
doc.save()
return doc.name
def setup_reminder_settings():
if not frappe.db.exists('Email Template', _('Interview Reminder')):
base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
frappe.get_doc({
'doctype': 'Email Template',
'name': _('Interview Reminder'),
'response': response,
'subject': _('Interview Reminder'),
'owner': frappe.session.user,
}).insert(ignore_permissions=True)
if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
frappe.get_doc({
'doctype': 'Email Template',
'name': _('Interview Feedback Reminder'),
'response': response,
'subject': _('Interview Feedback Reminder'),
'owner': frappe.session.user,
}).insert(ignore_permissions=True)
hr_settings = frappe.get_doc('HR Settings')
hr_settings.interview_reminder_template = _('Interview Reminder')
hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
hr_settings.save()

View File

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

View File

@@ -0,0 +1,74 @@
{
"actions": [],
"creation": "2021-04-12 16:24:10.382863",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"interviewer",
"interview_feedback",
"average_rating",
"result",
"column_break_4",
"comments"
],
"fields": [
{
"fieldname": "interviewer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Interviewer",
"options": "User"
},
{
"allow_on_submit": 1,
"fieldname": "interview_feedback",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Interview Feedback",
"options": "Interview Feedback",
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "average_rating",
"fieldtype": "Rating",
"in_list_view": 1,
"label": "Average Rating",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fetch_from": "interview_feedback.feedback",
"fieldname": "comments",
"fieldtype": "Text",
"label": "Comments",
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "result",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Result",
"options": "\nCleared\nRejected",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-29 13:13:25.865063",
"modified_by": "Administrator",
"module": "HR",
"name": "Interview Detail",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestInterviewDetail(unittest.TestCase):
pass

View File

@@ -0,0 +1,54 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Interview Feedback', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Interview'];
frm.set_query('interview', function() {
return {
filters: {
docstatus: ['!=', 2]
}
};
});
},
interview_round: function(frm) {
frappe.call({
method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
args: {
interview_round: frm.doc.interview_round
},
callback: function(r) {
frm.set_value('skill_assessment', r.message);
}
});
},
interview: function(frm) {
frappe.call({
method: 'erpnext.hr.doctype.interview_feedback.interview_feedback.get_applicable_interviewers',
args: {
interview: frm.doc.interview || ''
},
callback: function(r) {
frm.set_query('interviewer', function() {
return {
filters: {
name: ['in', r.message]
}
};
});
}
});
},
interviewer: function(frm) {
if (!frm.doc.interview) {
frappe.throw(__('Select Interview first'));
frm.set_value('interviewer', '');
}
}
});

View File

@@ -0,0 +1,171 @@
{
"actions": [],
"autoname": "HR-INT-FEED-.####",
"creation": "2021-04-12 17:03:13.833285",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"details_section",
"interview",
"interview_round",
"job_applicant",
"column_break_3",
"interviewer",
"result",
"section_break_4",
"skill_assessment",
"average_rating",
"section_break_7",
"feedback",
"amended_from"
],
"fields": [
{
"allow_in_quick_entry": 1,
"fieldname": "interview",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Interview",
"options": "Interview",
"reqd": 1
},
{
"allow_in_quick_entry": 1,
"fetch_from": "interview.interview_round",
"fieldname": "interview_round",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Interview Round",
"options": "Interview Round",
"read_only": 1,
"reqd": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "interviewer",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Interviewer",
"options": "User",
"reqd": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"label": "Skill Assessment"
},
{
"allow_in_quick_entry": 1,
"fieldname": "skill_assessment",
"fieldtype": "Table",
"options": "Skill Assessment",
"reqd": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "average_rating",
"fieldtype": "Rating",
"in_list_view": 1,
"label": "Average Rating",
"read_only": 1
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Feedback"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Interview Feedback",
"print_hide": 1,
"read_only": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "feedback",
"fieldtype": "Text"
},
{
"fieldname": "result",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Result",
"options": "\nCleared\nRejected",
"reqd": 1
},
{
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"fetch_from": "interview.job_applicant",
"fieldname": "job_applicant",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Job Applicant",
"options": "Job Applicant",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-09-30 13:30:49.955352",
"modified_by": "Administrator",
"module": "HR",
"name": "Interview Feedback",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Interviewer",
"share": 1,
"submit": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "interviewer",
"track_changes": 1
}

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, get_link_to_form, getdate
class InterviewFeedback(Document):
def validate(self):
self.validate_interviewer()
self.validate_interview_date()
self.validate_duplicate()
self.calculate_average_rating()
def on_submit(self):
self.update_interview_details()
def on_cancel(self):
self.update_interview_details()
def validate_interviewer(self):
applicable_interviewers = get_applicable_interviewers(self.interview)
if self.interviewer not in applicable_interviewers:
frappe.throw(_('{0} is not allowed to submit Interview Feedback for the Interview: {1}').format(
frappe.bold(self.interviewer), frappe.bold(self.interview)))
def validate_interview_date(self):
scheduled_date = frappe.db.get_value('Interview', self.interview, 'scheduled_on')
if getdate() < getdate(scheduled_date) and self.docstatus == 1:
frappe.throw(_('{0} submission before {1} is not allowed').format(
frappe.bold('Interview Feedback'),
frappe.bold('Interview Scheduled Date')
))
def validate_duplicate(self):
duplicate_feedback = frappe.db.exists('Interview Feedback', {
'interviewer': self.interviewer,
'interview': self.interview,
'docstatus': 1
})
if duplicate_feedback:
frappe.throw(_('Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue.').format(
self.interview, get_link_to_form('Interview Feedback', duplicate_feedback)))
def calculate_average_rating(self):
total_rating = 0
for d in self.skill_assessment:
if d.rating:
total_rating += d.rating
self.average_rating = flt(total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0)
def update_interview_details(self):
doc = frappe.get_doc('Interview', self.interview)
total_rating = 0
if self.docstatus == 2:
for entry in doc.interview_details:
if entry.interview_feedback == self.name:
entry.average_rating = entry.interview_feedback = entry.comments = entry.result = None
break
else:
for entry in doc.interview_details:
if entry.interviewer == self.interviewer:
entry.average_rating = self.average_rating
entry.interview_feedback = self.name
entry.comments = self.feedback
entry.result = self.result
if entry.average_rating:
total_rating += entry.average_rating
doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
doc.save()
doc.notify_update()
@frappe.whitelist()
def get_applicable_interviewers(interview):
data = frappe.get_all('Interview Detail', filters={'parent': interview}, fields=['interviewer'])
return [d.interviewer for d in data]

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import add_days, flt, getdate
from erpnext.hr.doctype.interview.test_interview import (
create_interview_and_dependencies,
create_skill_set,
)
from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
class TestInterviewFeedback(unittest.TestCase):
def test_validation_for_skill_set(self):
frappe.set_user("Administrator")
job_applicant = create_job_applicant()
interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
skill_ratings = get_skills_rating(interview.interview_round)
interviewer = interview.interview_details[0].interviewer
create_skill_set(['Leadership'])
interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4})
frappe.set_user(interviewer)
self.assertRaises(frappe.ValidationError, interview_feedback.save)
frappe.set_user("Administrator")
def test_average_ratings_on_feedback_submission_and_cancellation(self):
job_applicant = create_job_applicant()
interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
skill_ratings = get_skills_rating(interview.interview_round)
# For First Interviewer Feedback
interviewer = interview.interview_details[0].interviewer
frappe.set_user(interviewer)
# calculating Average
feedback_1 = create_interview_feedback(interview.name, interviewer, skill_ratings)
total_rating = 0
for d in feedback_1.skill_assessment:
if d.rating:
total_rating += d.rating
avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating)
avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
'parent': feedback_1.interview,
'interviewer': feedback_1.interviewer,
'interview_feedback': feedback_1.name
}, 'average_rating')
# 1. average should be reflected in Interview Detail.
self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating))
'''For Second Interviewer Feedback'''
interviewer = interview.interview_details[1].interviewer
frappe.set_user(interviewer)
feedback_2 = create_interview_feedback(interview.name, interviewer, skill_ratings)
interview.reload()
feedback_2.cancel()
interview.reload()
frappe.set_user("Administrator")
def tearDown(self):
frappe.db.rollback()
def create_interview_feedback(interview, interviewer, skills_ratings):
interview_feedback = frappe.new_doc("Interview Feedback")
interview_feedback.interview = interview
interview_feedback.interviewer = interviewer
interview_feedback.result = "Cleared"
for rating in skills_ratings:
interview_feedback.append("skill_assessment", rating)
interview_feedback.save()
interview_feedback.submit()
return interview_feedback
def get_skills_rating(interview_round):
import random
skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
for d in skills:
d["rating"] = random.randint(1, 5)
return skills

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Interview Round", {
refresh: function(frm) {
if (!frm.doc.__islocal) {
frm.add_custom_button(__("Create Interview"), function() {
frm.events.create_interview(frm);
});
}
},
create_interview: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.interview_round.interview_round.create_interview",
args: {
doc: frm.doc
},
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
}
});

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