Compare commits

..

436 Commits

Author SHA1 Message Date
Rohit Waghchaure
9dec4a2454 Merge branch 'version-13-pre-release' into version-13 2022-02-09 14:44:10 +05:30
Rohit Waghchaure
7e910f21b8 bumped to version 13.20.0 2022-02-09 15:04:10 +05:50
rohitwaghchaure
b27c4ac6e2 Merge pull request #29727 from rohitwaghchaure/change-log-for-v13-20
chore: change log for v13.20.0
2022-02-09 14:42:15 +05:30
Rohit Waghchaure
a451c49a42 chore: change log for v13.20.0 2022-02-09 14:40:07 +05:30
Deepesh Garg
427d8c865a Merge pull request #29678 from frappe/mergify/bp/version-13-pre-release/pr-29460
feat: Refund entry against loans (backport #29460)
2022-02-07 19:17:07 +05:30
Deepesh Garg
5ed44bdb88 Merge pull request #29686 from frappe/mergify/bp/version-13-pre-release/pr-29660
fix: Add missing key in Loan (backport #29660)
2022-02-07 19:05:06 +05:30
Deepesh Garg
eb36ebee8f fix: Resolve conflicts 2022-02-07 18:49:57 +05:30
Deepesh Garg
159e492053 test: Add test case for repayment against partially disbursed loans
(cherry picked from commit ef69d1fd38)
2022-02-07 13:16:19 +00:00
Abhinav Raut
6dbb3cdf75 fix: missing key in loan
(cherry picked from commit d0043bdbac)
2022-02-07 13:16:19 +00:00
Deepesh Garg
25012399fa fix: Add disbursement accounts to tests
(cherry picked from commit 8ece2845f2)
2022-02-07 09:09:20 +00:00
Deepesh Garg
4ab6994b3d feat: Refund entry against loans
(cherry picked from commit c68c70f8bc)

# Conflicts:
#	erpnext/loan_management/doctype/loan/loan.json
#	erpnext/patches.txt
2022-02-07 09:09:19 +00:00
Ankush Menat
e618571fb3 fix: dont show cancelled PO items in plan report
(cherry picked from commit 6459ceaea1)
2022-02-06 20:07:22 +05:30
Marica
c99a14a7f3 Merge pull request #29646 from frappe/mergify/bp/version-13-pre-release/pr-29642
fix: Regenerate packed items on newly mapped doc (backport #29642)
2022-02-04 23:25:17 +05:30
marination
db24fca8c1 fix: Regenerate packed items on newly mapped doc
- Cannot determine action on newly mapped DN that hasnt been inserted
- Rows could have been deleted, updated, added, etc. before first save
- In this case , reset packing table

(cherry picked from commit bd41a99c8a)
2022-02-04 17:33:17 +00:00
Marica
e095e27cb9 Merge pull request #29629 from frappe/mergify/bp/version-13-pre-release/pr-29456
fix: Incorrect packing list for recurring items & code cleanup (backport #29456)
2022-02-04 10:17:46 +05:30
marination
bdfb478b3b fix: (Linter) import sequence
(cherry picked from commit f18ec2d947)
2022-02-03 16:41:20 +00:00
marination
2428812dcc test: Packed Items test file
(cherry picked from commit 4e7b4dc9a8)
2022-02-03 16:41:20 +00:00
marination
677a3975ba refactor: Price fetching and updation logic
- fetch price from price list, use item master valuation rate as fallback fo0r packed item
- use a item code, item row name map to maintain cumulative price
- reset table if item in a row is replaced
- loop over items table only to set price, lesser iterations than packed items table

(cherry picked from commit 2f4d266ee1)
2022-02-03 16:41:19 +00:00
marination
5d37c9c38d chore: Break updation logic into smaller functions
- Smaller functions for updation
- All calls visible from parent function to avoid context switching due to nested calls

(cherry picked from commit 2c14ab0439)
2022-02-03 16:41:19 +00:00
marination
b836d4fe25 fix: Linter and minor code refactor
- Create an indexed map of stale packed items table to avoid loops to check if packed item row exists
- Reset packed items if row deletion takes place
- Renamed functions to self-explain them
- Split long function
- Reduce function calls inside function (makes it harder to follow through)

(cherry picked from commit 4c677eafe9)
2022-02-03 16:41:18 +00:00
marination
153fd355bc chore: SQL to QB & accomodate Update Items
- `doc_before_save` does not exist via Update Items (updates stuff in the backend so doc isn't considered unsaved/dirty)
- converted more raw sql to qb and ORM

(cherry picked from commit f8a5786542)
2022-02-03 16:41:18 +00:00
marination
350cac9207 fix: Incorrect packing list for recurring items & code cleanup
- Fix Incorrect packing list for recurring items in the Items table
- Re-organised functions based on external use and order of use
- Deleted `clean_packing_list` function and reduced no.of loops
- Raw SQL to QB
- Minor formatting changes

(cherry picked from commit 3f48fc1898)
2022-02-03 16:41:17 +00:00
mergify[bot]
c054d1f7ee fix: ignore empty customer/supplier in item query (#29610) (#29614)
* fix: dont try to filter by customer/supplier if None

* test: item query with emtpy supplier

(cherry picked from commit 41a95e5624)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-02-03 14:44:33 +05:30
Deepesh Garg
68b337316a Merge pull request #29573 from frappe/mergify/bp/version-13-pre-release/pr-29561
feat: Provisional accounting for expenses (backport #29451) (backport #29561)
2022-02-01 20:39:09 +05:30
Deepesh Garg
1ab3714fd0 chore: Resolve conflicts 2022-02-01 19:41:00 +05:30
Deepesh Garg
dbb3ea3a6e chore: fix tests
(cherry picked from commit 640b2d57de)
2022-02-01 12:42:09 +00:00
Deepesh Garg
abfccde664 fix: Linting Issues
(cherry picked from commit c71dbda166)
2022-02-01 12:42:09 +00:00
Deepesh Garg
293ec00023 chore: Resolve conflicts
(cherry picked from commit aa16a4bb5f)

# Conflicts:
#	erpnext/patches.txt
2022-02-01 12:42:09 +00:00
Deepesh Garg
061a569f4d feat: Provisional accounting for expenses (#29451)
* feat: Provisonal accounting for expenses

* fix: Method for provisional accounting entry

* chore: Add test case

* fix: Remove test case

* fix: Use company doctype

* fix: Add provisional expense account field in Purchase Receipt Item

* fix: Test case

* fix: Move provisional expense account to parent

* fix: Patch

(cherry picked from commit 528c71382f)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
#	erpnext/patches.txt
#	erpnext/setup/doctype/company/company.json
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
(cherry picked from commit a54c84728f)

# Conflicts:
#	erpnext/patches.txt
2022-02-01 12:42:08 +00:00
rohitwaghchaure
8c0613a600 Merge pull request #29542 from rohitwaghchaure/merge-branch-hotfix-to-pre-release-for-13-20
chore: Merge branch 'version-13-hotfix' into 'version-13-pre-release'
2022-01-31 20:16:11 +05:30
Rohit Waghchaure
862961d92e chore: Merge branch 'version-13-hotfix' into 'version-13-pre-release' 2022-01-31 19:38:44 +05:30
mergify[bot]
5ac11a4ba5 refactor!: dynamically compute bom_level (backport #29522) (#29540)
* refactor!: dynamically compute bom_level

(cherry picked from commit 157b4b33fe)

# Conflicts:
#	erpnext/manufacturing/doctype/bom/bom.py
#	erpnext/patches.txt

* fix(ux): sort multi-production item plans correctly

(cherry picked from commit d38fd8635c)

* fix: conflicts

* fix: removed set_bom_level

* fix: conflict

Co-authored-by: Ankush Menat <ankush@frappe.io>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2022-01-31 17:11:43 +05:30
Ankush Menat
2bc42759d2 Merge pull request #29534 from frappe/mergify/bp/version-13-hotfix/pr-29519
fix: bin uniqueness (backport #29519)
2022-01-31 15:56:12 +05:30
mergify[bot]
9425ca07f5 chore: revert manual handling of stock level section (#29537) (#29538)
(cherry picked from commit a254201696)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-31 15:43:08 +05:30
Ankush Menat
2b868e7f56 fix: update reserved qty for production/ s/c
(cherry picked from commit 0a15334464)
2022-01-31 09:00:20 +00:00
Ankush Menat
f99f537b57 refactor: patch for fixing broken bins
fix(patch): delete fully broken bins

if bin doesn't have item_code or warehouse then it's not recoverable.

(cherry picked from commit c2ecc7a2d1)
2022-01-31 09:00:19 +00:00
Sagar Sharma
a7b30d9197 fix(patch): patch duplicate bins
(cherry picked from commit 7ff3ca25e5)
2022-01-31 09:00:18 +00:00
Ankush Menat
b3f36efa8f fix: ignore None item/wh whie updating reservation
(cherry picked from commit a1e7771cdd)
2022-01-31 09:00:17 +00:00
Ankush Menat
8aa6423093 fix: add unique constraint on bin at db level
(cherry picked from commit 08810dbcce)
2022-01-31 09:00:16 +00:00
Saqib Ansari
144c8ec904 Merge pull request #29528 from frappe/mergify/bp/version-13-hotfix/pr-29454
fix(sales order): incorrect no. of items fetched while creating dn (backport #29454)
2022-01-31 12:16:30 +05:30
Saqib Ansari
bed7201efe fix: merge conflicts 2022-01-31 11:40:59 +05:30
Saqib Ansari
e6f2343fb4 chore: add comment
(cherry picked from commit 0b9a850a17)
2022-01-31 06:09:26 +00:00
Saqib Ansari
9da63f2e79 fix(sales order): incorrect no. of items fetched while creating dn
(cherry picked from commit a73ad6759d)

# Conflicts:
#	erpnext/selling/doctype/sales_order/sales_order.js
2022-01-31 06:09:26 +00:00
Saqib Ansari
f31e52123a fix: qty filter not working if apply_multiple_pricing_rules is enabled 2022-01-31 11:37:33 +05:30
Deepesh Garg
2c83f18864 Merge pull request #29518 from frappe/mergify/bp/version-13-hotfix/pr-29499
fix(India): Tax calculation for overseas suppliers (backport #29499)
2022-01-31 11:06:51 +05:30
mergify[bot]
0fb4e81a30 fix: cost of poor quality report time filters not working (#28958) (#29521)
* fix: cost of poor quality report time filters not working

* chore:update cost of poor quality report to use query builder

* fix: linter warnings

* chore: updated report query

* chore: added test filters

* fix : cleared linter warnings

* chore: formatting

* refactor: query generation

- optionally apply date filters
- join instead of expensive sub-query
- return as dictionary

* test: simplify test

Co-authored-by: Ankush Menat <ankush@frappe.io>
(cherry picked from commit 0f7c2a19de)

Co-authored-by: aaronmenezes <ron2805@gmail.com>
2022-01-30 20:49:41 +05:30
Deepesh Garg
dc82becbd2 fix(India) Tax calculation for overseas suppliers
(cherry picked from commit e45c38337c)
2022-01-30 07:44:43 +00:00
Ankush Menat
70dc77305b fix: work order serial no allows storing whitespace
(cherry picked from commit 43bd88e741)
2022-01-29 14:27:09 +05:30
Ankush Menat
db3eef9a8b fix: extend sr_no cleanup to packed items too
(cherry picked from commit e177c5277f)
2022-01-29 14:27:09 +05:30
Ankush Menat
cc08125f39 refactor: reusable clean_serial_nos
(cherry picked from commit b20df3745e)
2022-01-29 14:27:09 +05:30
Ankush Menat
997ddbea78 fix: ignore alternate item while checking pending qty
(cherry picked from commit 14e3e163ae)
2022-01-29 14:02:10 +05:30
Deepesh Garg
ebe9599dda Merge pull request #29428 from deepeshgarg007/shipping_rule_apply
fix: Add shipping charges to taxes only if applicable
2022-01-29 12:56:49 +05:30
Saqib Ansari
8535ad5294 chore: undo unnecessary changes
(cherry picked from commit 3da2cac772)
2022-01-29 07:14:23 +00:00
Saqib Ansari
eeb2402764 chore: rename redefinition of multiple pricing rule test
(cherry picked from commit 5a7a9a598b)
2022-01-29 07:14:22 +00:00
Saqib Ansari
e7e0359e3e test: add test_multiple_pricing_rules
(cherry picked from commit c0d1f4869f)
2022-01-29 07:14:22 +00:00
Saqib Ansari
c24724ac7a fix: qty filter not working if apply_multiple_pricing_rules is enabled
(cherry picked from commit 1e17d6a607)
2022-01-29 07:14:22 +00:00
Deepesh Garg
5f9fdba49e Merge branch 'version-13-hotfix' into shipping_rule_apply 2022-01-29 12:41:14 +05:30
Ankush Menat
5dd443eeb0 test: delete data after running test
[skip ci]
2022-01-28 13:52:20 +05:30
Ankush Menat
de01f0f364 fix(patch): update BOMs to have correct transfer_material_against
(cherry picked from commit e4c220053f)
2022-01-28 13:52:20 +05:30
Ankush Menat
4e14d28564 fix: ensure correct-ish transfer against
(cherry picked from commit 86efa0dbfd)
2022-01-28 13:52:20 +05:30
Ankush Menat
03bd68654b chore: disable annoying flake8 formatting for QB (#29488)
[skip ci]

(cherry picked from commit 251576a6bb)
2022-01-28 13:50:02 +05:30
mergify[bot]
67689e7374 fix: stock ledger rendering glitch (#29482)
undefined Issue

(cherry picked from commit f380042195)

Co-authored-by: smehata <53169014+smehata@users.noreply.github.com>
(cherry picked from commit 0dafa8a12a)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2022-01-27 20:24:51 +05:30
Suraj Shetty
4e4855fed7 Merge pull request #29473 from frappe/mergify/bp/version-13-hotfix/pr-29471 2022-01-27 11:27:29 +05:30
Suraj Shetty
352c585ef5 fix: Further sort sales_order_analysis to get consistent response
Since the data was sorted by `transaction_date`, report sorting order used to be inconsistent everytime if there were lots of records with same transaction date. This used to create problems while exporting report as "Excel" with filters.

(cherry picked from commit 29c7947db0)
2022-01-27 05:46:44 +00:00
Akash Krishna
d2b61bbe8a fix(healthcare): populate current practitioner in appointment availability popup (#29405)
* fix: Remove two-way patient fetch from inpatient_record (Lab Test, Vital Signs, Sample Collection)

* fix: Patient Appointment - practitioner field gettng cleared on Check Availability dialog, added Practitioner name in the dialog

* fix: Patient Appointment - set practitioner in check availability dialog load

Co-authored-by: Chillar Anand <chillar@avilpage.com>
2022-01-26 14:13:40 +05:30
mergify[bot]
58fd7044b5 fix: remove all stock UOM conversion when changing stock UOM (#29463)
(cherry picked from commit eb8d08b411)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-26 12:28:26 +05:30
Rucha Mahabal
177a209f50 fix(Employee): set user image and validate user id only if user data is found (#29452) 2022-01-25 16:49:53 +05:30
Saurabh
034773a670 fix: broken patches (#29067)
* chore: patch fixes

* fix: remove desktop icons while deleting sales reports

* refactor: dont ignore dangerous exceptions in patches

* fix: make patch kinda idempotent

with previous query rerunning would've caused all values to become 0.

* fix: check type before patching

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-25 11:12:25 +05:30
mergify[bot]
63ccddfcc9 fix: fetch "transfer material against" from BOM (#29435) (#29436)
(cherry picked from commit b8fbfed096)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-24 22:10:28 +05:30
Deepesh Garg
ecba064161 Merge pull request #29404 from deepeshgarg007/doc_currency_override
fix: Previous document currency getting overridden
2022-01-24 20:49:09 +05:30
mergify[bot]
895dd68691 fix: honour 'include holidays' setting while marking attendance for leave application (backport #29425) (#29432)
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-01-24 19:21:58 +05:30
mergify[bot]
d361e23a59 test: clear singles cache for changed settings (#29430) (#29431)
(cherry picked from commit 3d3f0139fd)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-24 18:46:20 +05:30
Deepesh Garg
e8bc4f12a7 fix:Add shipping charges to taxes only if applicable 2022-01-24 16:16:27 +05:30
rohitwaghchaure
aba8788ea2 Merge pull request #29422 from frappe/mergify/bp/version-13-hotfix/pr-29419
fix: incorrect raw materials quantity in manufacture stock entry (backport #29419)
2022-01-24 15:32:38 +05:30
rohitwaghchaure
3dfe7faf70 fix: issue of CI 2022-01-24 13:11:13 +05:30
Deepesh Garg
f7593f599e Merge pull request #29424 from frappe/mergify/bp/version-13-hotfix/pr-29360
test: unit test for deferred item with zero months (backport #29360)
2022-01-24 13:01:49 +05:30
Deepesh Garg
51518cf978 Merge pull request #29423 from frappe/mergify/bp/version-13-hotfix/pr-29407
fix: opening invoice creation tool can fetch multiple accounting dimension (backport #29407)
2022-01-24 13:01:11 +05:30
Deepesh Garg
f6ee0c70e2 Merge branch 'version-13-hotfix' into doc_currency_override 2022-01-24 12:48:53 +05:30
ruthra
abd8291012 test: unit test for deferred items with zero months
(cherry picked from commit 1e0450ed17)
2022-01-24 07:12:37 +00:00
ruthra
37bbdc6743 refactor: a more generalized way to clear old entries for test cases
(cherry picked from commit ed0f9cc063)
2022-01-24 07:12:36 +00:00
ruthra
24011bc6a4 refactor: fix failing test cases
(cherry picked from commit f122a47d0d)
2022-01-24 07:07:04 +00:00
ruthra
1273221477 fix: linter issue
(cherry picked from commit 80ea163953)
2022-01-24 07:07:04 +00:00
ruthra
b96fa03228 test: validating custom accouting dimension in opening invoice tool
- create new dimension 'department'
 - assign and validate department in opening invoices

(cherry picked from commit 88463dc494)
2022-01-24 07:07:03 +00:00
ruthra
bbd777a9cb fix: opening invoice creation tool not taking in accouting dimension
(cherry picked from commit e2267cdba8)
2022-01-24 07:07:03 +00:00
rohitwaghchaure
384c6234d5 fix: conflicts 2022-01-24 12:16:19 +05:30
Rohit Waghchaure
540690252f fix: incorrect raw materials quantity in manufacture stock entry
(cherry picked from commit f8d232ac26)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
2022-01-24 06:44:46 +00:00
rohitwaghchaure
43a21f2836 Merge pull request #29337 from anupamvs/crm-contact-duplication-v13
fix: contact duplication on converting lead to customer
2022-01-24 10:16:23 +05:30
mergify[bot]
2fbaba40b5 fix: bypass_credit_limit_check in credit balance report (#29401) 2022-01-23 18:17:30 +05:30
Britlog
096e7b9ec5 fix: Don't delete barcode while saving Item (#29411) 2022-01-23 16:56:09 +05:30
Rohit Waghchaure
5c8fc3890f Merge branch 'version-13-pre-release' into version-13 2022-01-23 14:57:36 +05:30
Rohit Waghchaure
f44d90e097 bumped to version 13.19.0 2022-01-23 15:17:36 +05:50
rohitwaghchaure
4305d58273 Merge pull request #29415 from rohitwaghchaure/change-log-for-v13-19
chore: change log for v13.19.0
2022-01-23 14:55:32 +05:30
Rohit Waghchaure
194b7fbf2f chore: change log for v13.19.0 2022-01-23 14:50:49 +05:30
Marica
f9de807b25 Merge pull request #29400 from marination/minor-quotation-doc-wrong-var-pre-release
fix: Wrong variable name breaks Cart Items rendering
2022-01-21 19:45:49 +05:30
marination
5ed4b43b73 debug: CI 2022-01-21 19:21:38 +05:30
Marica
315d542c2a Merge pull request #29398 from marination/minor-quotation-doc-wrong-var
fix: Wrong variable name breaks Cart Items rendering
2022-01-21 19:20:29 +05:30
marination
bfbcde7595 debug: CI 2022-01-21 19:02:47 +05:30
Deepesh Garg
ef782fd35b fix: Previous document currency getting overriden 2022-01-21 18:00:35 +05:30
Ankush Menat
1538076ae4 fix(patch): skip patch if column doesn't exist 2022-01-21 16:11:32 +05:30
Ankush Menat
f60ea20145 test: create and cancel sr no with stock reco
(cherry picked from commit 4fa93e246e)
2022-01-21 16:11:32 +05:30
Ankush Menat
91887f215d fix(patch): update stock reconciliation current serial nos
When current serial nos are non-existing there shouldn't be any value in
current_serial_no field.

(cherry picked from commit 203f0086f8)
2022-01-21 16:11:32 +05:30
Ankush Menat
0e413ef54e fix: bad default for non-existing serial nos
serial_no field should have empty string if it doesn't have any serial
nos

(cherry picked from commit 33c4a0b5a9)
2022-01-21 16:11:32 +05:30
Ankush Menat
f336b5d586 perf: single query to update serial nos
(cherry picked from commit ea5efd0791)
2022-01-21 16:11:32 +05:30
marination
b27fe43920 fix: Wrong variable name breaks Cart Items rendering 2022-01-21 16:04:48 +05:30
marination
2be15bd8e0 fix: Wrong variable name breaks Cart Items rendering 2022-01-21 15:58:29 +05:30
Marica
398fa6eb48 Merge pull request #29396 from frappe/mergify/bp/version-13-pre-release/pr-29383
fix: Cart & Popup Logic of Item variant without Website Item (backport #29383)
2022-01-21 15:35:16 +05:30
mergify[bot]
d9f783f38e fix: update status on visit cancel (#29389) (#29394)
* fix: update status on cancelling visit

* test: updated test for cancelling visits

(cherry picked from commit 11c9a5a79f)

Co-authored-by: Noah Jacob <noahjacobkurian@gmail.com>
2022-01-21 15:22:56 +05:30
mergify[bot]
0da75d745e fix: update status on visit cancel (#29389) (#29395)
* fix: update status on cancelling visit

* test: updated test for cancelling visits

(cherry picked from commit 11c9a5a79f)

Co-authored-by: Noah Jacob <noahjacobkurian@gmail.com>
2022-01-21 15:18:15 +05:30
marination
522031fd73 fix: (test) set user for price test
(cherry picked from commit 2185016f71)
2022-01-21 09:46:15 +00:00
marination
927efffcb7 fix: Sider and Linter
(cherry picked from commit 33b5e1d7e6)
2022-01-21 09:46:15 +00:00
marination
454ae7e0ed tests: Variant without web item price fetch and add to cart
- Also, optionally get image from template web item for cart if variant has no image

(cherry picked from commit c1dd3d7d9e)
2022-01-21 09:46:14 +00:00
marination
c81cf33dce fix: Cart Logic of Item variant without Website Item
(cherry picked from commit 7d130af12b)
2022-01-21 09:46:14 +00:00
Saqib Ansari
6aaa187a49 Merge pull request #29392 from frappe/mergify/bp/version-13-hotfix/pr-29385
fix: first depreciation amount in asset (backport #29385)
2022-01-21 15:15:55 +05:30
Marica
f2d8d7ab96 Merge pull request #29383 from marination/item-variant-web-logic-fix
fix: Cart & Popup Logic of Item variant without Website Item
2022-01-21 15:15:33 +05:30
mergify[bot]
fde6f13b4b fix: cannot cancel jv after migrating from v12 (#29391) 2022-01-21 15:15:11 +05:30
marination
2185016f71 fix: (test) set user for price test 2022-01-21 14:55:43 +05:30
marination
33b5e1d7e6 fix: Sider and Linter 2022-01-21 14:49:10 +05:30
Saqib Ansari
4fec250cbf fix: merge conflicts 2022-01-21 14:33:59 +05:30
marination
c1dd3d7d9e tests: Variant without web item price fetch and add to cart
- Also, optionally get image from template web item for cart if variant has no image
2022-01-21 14:33:30 +05:30
Saqib Ansari
8e2b96e814 fix: failing test cases
(cherry picked from commit ae580d116c)

# Conflicts:
#	erpnext/assets/doctype/asset/test_asset.py
2022-01-21 08:56:40 +00:00
Saqib Ansari
398fab4786 fix: depreciation amount on selling an asset
(cherry picked from commit 0bbde4819b)
2022-01-21 08:56:40 +00:00
Saqib Ansari
844b73efcd fix: first depreciation amount in asset
(cherry picked from commit 60bfead773)
2022-01-21 08:56:39 +00:00
mergify[bot]
9869b31b31 fix(Timesheets): calculate to time (backport #28589) (#29390)
Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
Co-authored-by: Himanshu <himanshuwarekar@yahoo.com>
2022-01-21 13:51:49 +05:30
Saqib Ansari
04e3fb85e5 Merge pull request #28851 from frappe/mergify/bp/version-13-hotfix/pr-28507
fix: Clear Depreciation Schedule before modification (backport #28507)
2022-01-21 12:34:05 +05:30
Marica
1f099bcdfb Merge pull request #29077 from dj12djdjs/fix-ecommerce-cart-badge
fix(e-commerce)(UX): shopping cart qty badge
2022-01-21 12:28:28 +05:30
mergify[bot]
ceba7f2fe4 fix: updated packed_items getting fetched on Sales Return / Credit Note (#28607) (#29387)
* fix: updated packed_items getting fetched on sales return

* fix: updated test cases for product bundle returns

* fix: moved calculate_mapped_packed_items_return to stock.utils

* fix: updated packed_items getting fetched on Return/Credit Note

* refactor: change var name to avoid `self`

* fix: check item code before assigning parent item

(cherry picked from commit 20216fa9f1)

Co-authored-by: Noah Jacob <noahjacobkurian@gmail.com>
Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-21 12:11:58 +05:30
Chillar Anand
696fc0a482 refactor: Added deprecation warning for hospitality domain (#29384) 2022-01-21 11:36:40 +05:30
marination
7d130af12b fix: Cart Logic of Item variant without Website Item 2022-01-20 19:34:36 +05:30
Anupam Kumar
29241070c1 Merge branch 'version-13-hotfix' into crm-contact-duplication-v13 2022-01-20 17:30:21 +05:30
Ankush Menat
d32f229483 ci: fail on merge conflict label 2022-01-20 15:39:32 +05:30
Ankush Menat
8a9a13cb96 ci: concurrency control on v13 branches 2022-01-20 15:39:32 +05:30
Ankush Menat
d190ea4ad3 ci: disable coverage on v13 branch 2022-01-20 15:39:32 +05:30
Marica
eb44b16e31 Merge pull request #29378 from frappe/mergify/bp/version-13-hotfix/pr-29375
fix: (enhance) BOM Operations Report (backport #29375)
2022-01-20 15:34:20 +05:30
mergify[bot]
b9a42807f8 fix: show stock UOM for material transfers (#29376) (#29377)
(cherry picked from commit 7e407bcc42)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-20 15:06:11 +05:30
marination
655d8980dc fix: Allow only BOM with ops in BOM ID filter & add total row to report
(cherry picked from commit af734298c8)
2022-01-20 09:25:05 +00:00
marination
e963d0e8e0 fix: Sider (unused imports) and Linter(add extra empty line)
(cherry picked from commit 3c7b772112)
2022-01-20 09:25:05 +00:00
marination
77cb7ebd40 fix: (enhance) BOM Operations Report
- Added filters in the Report for BOM ID, Item Code and Workstation
- Converted Raw SQL to frappe.qb and added method to get filtered data
- Changed fieldtype of 'Time in mins' from Int to Float
- Get BOM wise grouped data to  keep order and accurate grouping in report

(cherry picked from commit a9ff1fc52e)
2022-01-20 09:25:04 +00:00
Deepesh Garg
84803f8f21 Merge pull request #29370 from frappe/mergify/bp/version-13-pre-release/pr-29366
fix: Cleanup empty rows on bank statement import (backport #29366)
2022-01-20 12:14:51 +05:30
Deepesh Garg
6a53cfea00 fix: Cleanup empty rows on bank statement import
(cherry picked from commit 25398d017b)
2022-01-20 05:56:08 +00:00
Deepesh Garg
829ea71ae1 Merge pull request #29366 from deepeshgarg007/bank_statement_import_template
fix: Cleanup empty rows on bank statement import
2022-01-20 11:24:48 +05:30
Deepesh Garg
25398d017b fix: Cleanup empty rows on bank statement import 2022-01-20 10:51:59 +05:30
mergify[bot]
f75d36dcb4 fix: from time and to time not updated in drag and drop action #29114 (#29361)
fix: from time and to time not updated in drag and drop action
(cherry picked from commit 8b5827ed6d)

Co-authored-by: Mohammed Yusuf Shaikh <49878143+mohammedyusufshaikh@users.noreply.github.com>
2022-01-19 19:30:21 +05:30
Marica
56dd25355d Merge pull request #29357 from frappe/mergify/bp/version-13-pre-release/pr-29352
refactor: update_serial_no function for old Maintenance Visits (backport #28843) (backport #29352)
2022-01-19 16:40:44 +05:30
marination
fa7f5d745e fix: Patch (reload doc)
(cherry picked from commit 93d665b38a)
2022-01-19 09:19:06 +00:00
marination
dd81d622dd fix: Conflicts
(cherry picked from commit 3023757a60)
2022-01-19 09:19:06 +00:00
Noah Jacob
4086f4f8d1 fix: Serial No. filters for unscheduled visits
(cherry picked from commit dec7513773)
(cherry picked from commit add36363ab)
2022-01-19 09:19:05 +00:00
Noah Jacob
311b2dd1a9 refactor: moved purpose table mandatory check to server
(cherry picked from commit b98c61f2a7)

# Conflicts:
#	erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
(cherry picked from commit 93325c72f0)
2022-01-19 09:19:05 +00:00
Noah Jacob
0783b36ba0 fix(test): fixed test case not creating serials
(cherry picked from commit 90d32006f4)
(cherry picked from commit 0bb0a9d032)
2022-01-19 09:19:04 +00:00
Noah Jacob
f491cc92cd test: added test for serial no. filters
(cherry picked from commit 9109660a69)
(cherry picked from commit f541b5c780)
2022-01-19 09:19:04 +00:00
Noah Jacob
824eef9354 fix(patch): updates maintenance schedule field for old docs
(cherry picked from commit c872aa43f8)

# Conflicts:
#	erpnext/patches.txt
(cherry picked from commit dc0d974144)
2022-01-19 09:19:04 +00:00
Noah Jacob
216f45ce11 refactor: update_serial_no function and code cleanup
(cherry picked from commit 4f52b86d7e)

# Conflicts:
#	erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
(cherry picked from commit 8775154fa0)
2022-01-19 09:19:03 +00:00
Marica
6c45223606 Merge pull request #29352 from frappe/mergify/bp/version-13-hotfix/pr-28843
refactor: update_serial_no function for old Maintenance Visits (backport #28843)
2022-01-19 14:48:24 +05:30
marination
93d665b38a fix: Patch (reload doc) 2022-01-19 13:53:18 +05:30
Anupam
44d2098530 fix: changed function name 2022-01-19 13:10:23 +05:30
marination
3023757a60 fix: Conflicts 2022-01-19 13:06:52 +05:30
Noah Jacob
add36363ab fix: Serial No. filters for unscheduled visits
(cherry picked from commit dec7513773)
2022-01-19 07:19:51 +00:00
Noah Jacob
93325c72f0 refactor: moved purpose table mandatory check to server
(cherry picked from commit b98c61f2a7)

# Conflicts:
#	erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
2022-01-19 07:19:50 +00:00
Noah Jacob
0bb0a9d032 fix(test): fixed test case not creating serials
(cherry picked from commit 90d32006f4)
2022-01-19 07:19:49 +00:00
Noah Jacob
f541b5c780 test: added test for serial no. filters
(cherry picked from commit 9109660a69)
2022-01-19 07:19:49 +00:00
Noah Jacob
dc0d974144 fix(patch): updates maintenance schedule field for old docs
(cherry picked from commit c872aa43f8)

# Conflicts:
#	erpnext/patches.txt
2022-01-19 07:19:48 +00:00
Noah Jacob
8775154fa0 refactor: update_serial_no function and code cleanup
(cherry picked from commit 4f52b86d7e)

# Conflicts:
#	erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.js
2022-01-19 07:19:47 +00:00
Anupam Kumar
b917f33399 Merge branch 'version-13-hotfix' into crm-contact-duplication-v13 2022-01-18 16:38:04 +05:30
Anupam
79ce36c19f fix: sider issue 2022-01-18 16:36:40 +05:30
Anupam
c8eae9dcef fix: contact duplication on converting lead to customer 2022-01-18 16:14:45 +05:30
mergify[bot]
69fded250c fix: enable allow negative stock by default in reposts (#29331) (#29333)
Negative stock validation is done BEFORE reposting as such it's not
required to be performed again and most likely this fails on old
intermediate data where user has no clue why it failed.

(cherry picked from commit fabe0bce15)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-18 13:33:29 +05:30
mergify[bot]
3064d95889 refactor: convert CTE validation to python (#29324) (#29332)
Some old servers running on mariadb <10.2 can't work with CTEs hence
reverting to basic python code

Co-authored-by: Ankush Menat <ankush@frappe.io>
(cherry picked from commit d7f16c4924)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2022-01-18 11:48:01 +05:30
mergify[bot]
d7f16c4924 refactor: convert CTE validation to python (#29324)
Some old servers running on mariadb <10.2 can't work with CTEs hence
reverting to basic python code

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-18 11:17:32 +05:30
rohitwaghchaure
ac2d1a9acc Merge pull request #29329 from rohitwaghchaure/merge-hotfix-to-pre-release-for-13-19
chore: Merge branch 'version-13-hotfix' into 'version-13-pre-release'
2022-01-18 00:23:29 +05:30
Rohit Waghchaure
8d3f8619cf chore: Merge branch 'version-13-hotfix' into 'version-13-pre-release' 2022-01-18 00:01:14 +05:30
mergify[bot]
d5d7f67ac2 fix: cover case when all material needs to be bought (#29326) (#29327)
When material request is to be made for purchase qty should be converted
to purchase UOM

(cherry picked from commit 48f3d53556)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-17 22:16:41 +05:30
mergify[bot]
5e3f5d8bcc fix: purchase to Stock UOM conversion on Production Plan #28570 (#29325)
fix: purchase to Stock UOM conversion on Production Plan
(cherry picked from commit 9cd26fbb6a)

Co-authored-by: Maxwell Morais <MaxMorais@users.noreply.github.com>
2022-01-17 22:15:12 +05:30
Devin Slauenwhite
c121ab8ef4 Merge remote-tracking branch 'upstream/version-13-hotfix' into fix-ecommerce-cart-badge 2022-01-17 09:15:14 -05:00
Ankush Menat
0b4b60a6c3 Merge pull request #29322 from frappe/mergify/bp/version-13-hotfix/pr-28987
fix: incorrect serial no valuation (backport #28987)
2022-01-17 18:02:57 +05:30
Deepesh Garg
7542dd3f15 Merge pull request #29318 from frappe/mergify/bp/version-13-hotfix/pr-29182
fix: get project from PO into payment entry (backport #29182)
2022-01-17 17:28:24 +05:30
Ankush Menat
0411bcbd5c test: serial no valuation test case
(cherry picked from commit 5d27a7672e)
2022-01-17 11:26:05 +00:00
Ankush Menat
9a3a679628 fix: get incoming rate only if not return
(cherry picked from commit fcc885bc16)
2022-01-17 11:26:04 +00:00
Ankush Menat
a3a9920771 fix: incorrect incoming rate computation for sr no
(cherry picked from commit b9642a1036)
2022-01-17 11:26:03 +00:00
mergify[bot]
c2d58ba796 feat(UX): Option to exclude holidays while marking monthly attendance (backport #29185) (#29321)
(cherry picked from commit 6aac8de53e)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-01-17 16:48:04 +05:30
Deepesh Garg
5da7e28d20 fix: Linting issues
(cherry picked from commit 09172002e7)
2022-01-17 09:34:58 +00:00
ruthra
7f5415177e refactor: get project from basic transactions
- sales order, sales invoice, purchase order and purchase order
- if project not found in transaction, get from items

(cherry picked from commit 8026f86548)
2022-01-17 09:34:57 +00:00
ruthra
ea9756da8f fix: get project from PO into payment entry
(cherry picked from commit ca17c7226c)
2022-01-17 09:34:57 +00:00
Ankush Menat
a0c2c94cff test: make sure zero incoming rate is maintained while consuming
(cherry picked from commit 0272397e54)
2022-01-17 13:21:48 +05:30
Ankush Menat
3b0f80b865 fix: dont update sle values from get_gl_entries
(cherry picked from commit 8f5772463c)
2022-01-17 13:21:48 +05:30
mergify[bot]
83ca2422f6 fix: show work order progress bar even it is closed (#29312) (#29313)
(cherry picked from commit 4d48035828)

Co-authored-by: Anupam Kumar <anupamvns0099@gmail.com>
2022-01-17 12:08:21 +05:30
mergify[bot]
de9a0d6a7f fix: ignore cancelled SLEs (#29303) (#29307)
(cherry picked from commit 82ea958730)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-16 21:44:07 +05:30
mergify[bot]
5db8af8cee fix(patch): sle.serial_no = "\n" causes incorrect queue (#29306) (#29309)
This happens due to old data.

(cherry picked from commit 66bf21f143)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-16 20:59:19 +05:30
mergify[bot]
42d3b0c063 fix: only add stock queue if FIFO (#29302) (#29305)
(cherry picked from commit b0cf6195e9)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-16 14:05:09 +05:30
mergify[bot]
ecbd885aad fix: exclude existing serial numbers while auto creating new #29292 (#29300)
fix: exclude existing serial numbers while auto creating new
(cherry picked from commit 2b681f04fd)

Co-authored-by: Dany Robert <danyrt@wahni.com>
2022-01-16 12:33:55 +05:30
Deepesh Garg
cfb43d159d Merge pull request #29298 from frappe/mergify/bp/version-13-hotfix/pr-29208
fix(India): NIL Rated, Exempted and non gst invoices in GSTR-1 report (backport #29208)
2022-01-15 19:18:25 +05:30
Deepesh Garg
f3ddf53d39 Merge pull request #29299 from frappe/mergify/bp/version-13-hotfix/pr-29115
fix: Deferred revenue booking for multi currency invoices via JV (backport #29115)
2022-01-15 19:18:11 +05:30
Deepesh Garg
ef3e4ee61f fix: Remove unwanted test
(cherry picked from commit fd922f1617)
2022-01-15 13:26:21 +00:00
Deepesh Garg
045a505a38 fix: Remove comment
(cherry picked from commit 08f26de3df)
2022-01-15 13:26:21 +00:00
Deepesh Garg
eff48299e4 fix: Test case
(cherry picked from commit 24bb1e8987)
2022-01-15 13:26:21 +00:00
Deepesh Garg
8ed138e373 fix: Test case
(cherry picked from commit 18c4ddadf1)
2022-01-15 13:26:20 +00:00
Deepesh Garg
a4e897d30b fix: Handle frozen books while handling
(cherry picked from commit 30a647ff80)
2022-01-15 13:26:20 +00:00
Deepesh Garg
38a330b55e fix: Test case
(cherry picked from commit e311667a25)
2022-01-15 13:26:19 +00:00
Deepesh Garg
a08b10d5cf fix: Add test case for multicurrency invoice
(cherry picked from commit 094158f287)
2022-01-15 13:26:19 +00:00
Deepesh Garg
eb3d28fcc0 fix: Deferred revenue booking for multi currency invoices via JV
(cherry picked from commit 98f294a8ae)
2022-01-15 13:26:19 +00:00
Deepesh Garg
103bf19700 fix: JSON for nil/exempt and non gst
(cherry picked from commit 55c445cd37)
2022-01-15 13:24:14 +00:00
Deepesh Garg
835448c9e5 fix(India): NIL Rated, Exempted and non gst invoices in GSTR-1 report
(cherry picked from commit 7d85755595)
2022-01-15 13:24:14 +00:00
mergify[bot]
94a30674df fix: threshold fields shows incorrect currency (#29289) 2022-01-14 15:29:46 +05:30
Deepesh Garg
2524be6386 Merge pull request #29281 from frappe/mergify/bp/version-13-hotfix/pr-29280
fix: accounts are coming from different company in the dropdown (backport #29280)
2022-01-13 22:41:26 +05:30
Rohit Waghchaure
e28b046cf9 fix: accounts are coming from different company in the dropdown
(cherry picked from commit dabe5981bb)
2022-01-13 15:42:50 +00:00
Marica
f1c08f9b8c Merge pull request #29274 from frappe/mergify/bp/version-13-hotfix/pr-29271
fix: Don't validate FG in repack entry (backport #29271)
2022-01-13 16:50:02 +05:30
marination
80b037ed13 test: Check for FinishedGoodError if 0 FG in repack entry
(cherry picked from commit c49dff385a)
2022-01-13 09:57:50 +00:00
marination
61681e73ef test: Basic test one item repacked into two
- Also run fg validation and fg marking after checking purpose, avoid unnecessary calls

(cherry picked from commit 3922a39591)
2022-01-13 09:57:50 +00:00
marination
a868fd7df9 fix: Allow multiple fg in repack entry
(cherry picked from commit eac7b5d5aa)
2022-01-13 09:57:50 +00:00
mergify[bot]
21c194495c fix: filter for leave period in Bulk Leave Policy Assignment (backport #29272) (#29273)
* fix: filter for leave period in Bulk Leave Policy Assignment

* fix: set title for Leave Policy Assignment

(cherry picked from commit 3a18e62a9b)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-01-13 14:28:56 +05:30
Deepesh Garg
884edb45b3 Merge pull request #29240 from frappe/mergify/bp/version-13-hotfix/pr-29193
fix: deferred report division by zero exception (backport #29193)
2022-01-13 12:56:27 +05:30
Devin Slauenwhite
81f1b7dfeb fix(Payroll): Cannot submit salary slips from amended payroll entry. (#29228)
* fix: salary slip transaction state after payroll entry cancel

* fix: use db_set in on_cancel method

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-01-13 10:29:52 +05:30
mergify[bot]
dcea4f4c96 fix: autoname generated for Job Applicant is too long (backport #29260) (#29263)
* fix: autoname generated for Job Applicant is too long (#29260)

* fix: autoname generated for Job Applicant is too long

- autoname based on email and append number if exists instead of concatenating name, email, title

- add more search fields for context during selection

* test: Job applicant naming and fix related tests

(cherry picked from commit 5cda4ea39f)

# Conflicts:
#	erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py

* fix: conflicts

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-01-12 19:54:59 +05:30
Marica
a4210a7c85 Merge pull request #29257 from marination/fix-reset-wh-defaults (#29259)
fix: Avoid resetting Default wh fields for Manufacture Entry
(cherry picked from commit efcfb825d7)

Co-authored-by: Marica <maricadsouza221197@gmail.com>
2022-01-12 17:09:13 +05:30
Marica
d01e794558 Merge pull request #29257 from marination/fix-reset-wh-defaults
fix: Avoid resetting Default wh fields for Manufacture Entry
(cherry picked from commit efcfb825d7)
2022-01-12 11:15:35 +00:00
Ganga Manoj
852543cbcd fix: Merge conflicts 2022-01-12 14:05:09 +05:30
rohitwaghchaure
464f813677 Merge pull request #29255 from frappe/mergify/bp/version-13-hotfix/pr-29179
fix: incorrect scrap item qty (backport #29179)
2022-01-12 14:00:36 +05:30
mergify[bot]
366f663c5a fix(asset): check if fixed asset account is set against company (#29256) 2022-01-12 13:45:37 +05:30
rohitwaghchaure
b4fee6d065 fix: conflicts 2022-01-12 13:31:34 +05:30
mergify[bot]
f4a031d94b fix: description not fetched in sales order analytics report (#29254) 2022-01-12 13:31:07 +05:30
rohitwaghchaure
fa66d1d87d fix: conflicts 2022-01-12 13:30:05 +05:30
Rohit Waghchaure
d71d769c7a fix: incorrect scrap item qty
(cherry picked from commit 8f0b2fa90e)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
2022-01-12 07:44:38 +00:00
Marica
e05ad8ed10 Merge pull request #29219 from marination/item-variant-config-backend-cleanup
fix: Behaviour of Item Variants Cache generation
2022-01-12 02:12:03 +05:30
Marica
f8761dc5ca Merge branch 'version-13-hotfix' into item-variant-config-backend-cleanup 2022-01-12 01:52:55 +05:30
marination
a0bd82e99f fix: Server Side test - teardown correctly 2022-01-11 22:43:39 +05:30
mergify[bot]
00ad0dd24d feat: early payment discount on sales & purchase orders (#29238) 2022-01-11 21:11:42 +05:30
Marica
c773051a3e Merge branch 'version-13-hotfix' into item-variant-config-backend-cleanup 2022-01-11 20:16:41 +05:30
marination
746bb6277f test: Variant Selector - Fetch next valid Attribute values fetching after selection of one 2022-01-11 20:15:24 +05:30
Ankush Menat
d9f31770c6 fix(patch): enable all uoms on recently created sites
(cherry picked from commit f8119563ca)
2022-01-11 18:56:50 +05:30
Ankush Menat
c1fbd2308c fix: UOM autocomplete broken
All new recent sites seem to have all UOMs as disabled by default. The
desired behaviour is exact opposite of this.

(cherry picked from commit 33aad4b950)
2022-01-11 18:56:50 +05:30
marination
577de39ca6 test: Variant Selector - Attribute Value fetching 2022-01-11 18:19:29 +05:30
ruthra
de9429b585 fix: deferred report division by zero exception
(cherry picked from commit fe4d7f86ee)
2022-01-11 11:50:18 +00:00
mergify[bot]
819346f69f fix: "update cost" should ignore overridden routing times #29154 (#29235)
fix: "update cost" should ignore overridden routing times
(cherry picked from commit 754596dfc1)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-11 09:02:57 +00:00
mergify[bot]
8bb72dece4 fix(pos): cannot ignore pricing rule for one particular invoice (#29232) 2022-01-11 14:26:39 +05:30
mergify[bot]
4343eddd46 fix(gl-report): group by cost center only if include_dimensions is checked (#28883) (#29231) 2022-01-11 13:24:21 +05:30
Ankush Menat
f35ef60262 Merge pull request #29224 from frappe/mergify/bp/version-13-hotfix/pr-29131
fix(patch): serial no whitespace trimming (backport #29131)
2022-01-10 22:33:15 +05:30
Ankush Menat
d0c7d4fc98 refactor: convert query to ORM
(cherry picked from commit cbaa8fdade)
2022-01-10 14:22:46 +00:00
Ankush Menat
fbe0442e89 fix(patch): serial no whitespace trimming
old data can contain trailing/leading whitespace which doesn't work well
with code to find last SLE for serial no.

(cherry picked from commit 0faa116f97)
2022-01-10 14:22:45 +00:00
mergify[bot]
055d620ef3 fix: pos invoices consolidation case with permlevel (#29220) 2022-01-10 19:48:28 +05:30
Marica
495c5126f8 fix: Exclude unpublished items while fetching items from other item groups (#29211) 2022-01-10 19:47:50 +05:30
marination
23f4b51e3e fix: Behaviour if Item Variants Cache generation
- Revert to old behaviour where variant doesnt need to be published
- Delete unused functions
2022-01-10 18:04:03 +05:30
marination
0174aa238c fix: Exclude unpublished items while fetching items from other item groups 2022-01-10 13:38:54 +05:30
mergify[bot]
ac4e996928 fix: Task Depends on not removed from Gantt chart (backport #28309) (#29207)
Co-authored-by: conncampbell <conncampbell@conzu.ca>
(cherry picked from commit 31123436fb)

Co-authored-by: Conn Campbell <conn.campbell@conzu.ca>
2022-01-10 10:49:29 +05:30
Deepesh Garg
d85aef5b2a Merge pull request #29188 from frappe/mergify/bp/version-13-hotfix/pr-29176
fix: Inconsistency in calculating outstanding amount (backport #29176)
2022-01-07 20:21:02 +05:30
mergify[bot]
822a8ba47a chore: remove framework patch for custom fields (#29117) (#29195)
* chore: patches were breaking during migration

* fix: rewrote the query in query builder

* chore: removed patch from patches.txt

(cherry picked from commit 25c875e447)

Co-authored-by: Shadrak Gurupnor <30501401+shadrak98@users.noreply.github.com>
2022-01-07 19:04:19 +05:30
Deepesh Garg
14aca20b00 Merge pull request #29194 from frappe/mergify/bp/version-13-hotfix/pr-28994
fix: cannot create debit note with zero qty (backport #28994)
2022-01-07 19:03:56 +05:30
Saqib Ansari
ecf341d1b1 fix: cannot save debit note with zero quantity
(cherry picked from commit 2be5104848)
2022-01-07 13:06:01 +00:00
Saqib Ansari
4ef9d289e8 fix(sales-invoice): cannot create debit note with zero qty
(cherry picked from commit 04ea42ce52)
2022-01-07 13:06:00 +00:00
Deepesh Garg
9b6c2fdfb7 fix: Conflicts 2022-01-07 15:03:55 +05:30
Deepesh Garg
203943ef97 Merge pull request #29186 from frappe/mergify/bp/version-13-hotfix/pr-29169
fix: GL Entries for loan repayment via Salary (backport #29169)
2022-01-07 15:02:19 +05:30
Deepesh Garg
b561c18502 fix: Inconsistency in calculating outstanding amount
(cherry picked from commit 9d3a5c3184)

# Conflicts:
#	erpnext/public/js/controllers/taxes_and_totals.js
2022-01-07 07:59:28 +00:00
Deepesh Garg
a333635ab6 Merge pull request #29135 from deepeshgarg007/gl_balance_ar_ap
fix: Show GL balance in Accounts Receivable and Payable summary
2022-01-07 12:59:17 +05:30
Deepesh Garg
892e63575e fix: GL Entries for loan repayment via Salary
(cherry picked from commit f316aaf41e)
2022-01-07 07:25:10 +00:00
Syed Mujeer Hashmi
60ddf39e37 chore: Remove the extra 's' in patient age string (#29180) 2022-01-07 09:22:36 +05:30
Rohit Waghchaure
dfe2140f70 Merge branch 'version-13-pre-release' into version-13 2022-01-06 23:59:30 +05:30
Rohit Waghchaure
c4dab3eef3 bumped to version 13.18.0 2022-01-07 00:19:30 +05:50
rohitwaghchaure
e967aafc28 Merge pull request #29178 from rohitwaghchaure/change-log-for-v13-18
chore: change log for v13.18.0
2022-01-06 23:58:02 +05:30
Rohit Waghchaure
0e925c6ea2 chore: change log for v13.18.0 2022-01-06 23:56:24 +05:30
mergify[bot]
49c33a67e1 fix: incorrect serial no valuation report showing cancelled entries (#29172) (#29174)
(cherry picked from commit 24fc487dd8)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-06 17:07:05 +05:30
mergify[bot]
7abe7c752b fix: incorrect serial no valuation report showing cancelled entries (#29172) (#29173)
(cherry picked from commit 24fc487dd8)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-06 17:05:00 +05:30
Deepesh Garg
d243291e1c Merge pull request #29155 from deepeshgarg007/ksa_vat_currency
fix: Currency in KSA VAT report
2022-01-06 16:22:40 +05:30
Deepesh Garg
493af62038 Merge pull request #29170 from frappe/mergify/bp/version-13-hotfix/pr-29125
fix: cannot create reverse journal entry (backport #29125)
2022-01-06 14:11:46 +05:30
Saqib Ansari
b82d4b7577 fix: cannot create reverse journal entry
(cherry picked from commit 3aa1817f7b)
2022-01-06 08:01:51 +00:00
mergify[bot]
2f98aa121a fix: Earned Leave allocation from Leave Policy Assignment (backport #29163) (#29166)
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-01-06 11:46:37 +05:30
mergify[bot]
37d2f6fd15 fix: Earned Leave allocation from Leave Policy Assignment (backport #29163) (#29165)
* fix: Earned Leave allocation from Leave Policy Assignment

* test: Earned Leave Allocation from Leave Policy Assignment

(cherry picked from commit 26247be3b8)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-01-06 11:44:58 +05:30
mergify[bot]
6cc3c612ee feat: 'Invoice Number' field in Opening Invoice Creation Tool (#29156) 2022-01-05 16:00:00 +05:30
Deepesh Garg
1d87e9d8f6 fix: Currency in KSA VAT report 2022-01-05 15:08:29 +05:30
Chillar Anand
a64efb764b refactor: Add deprecation warning for agriculture domain (#29149) 2022-01-05 11:56:32 +05:30
Subin Tom
d3820f6ce3 feat: 'Invoice Number' field in Opening Invoice Creation Tool (#29147) 2022-01-05 11:21:15 +05:30
rohitwaghchaure
2f07d38caf Merge pull request #29150 from frappe/mergify/bp/version-13-pre-release/pr-29103
fix: incorrect posting time fetching incorrect qty (backport #29103)
2022-01-05 10:36:37 +05:30
rohitwaghchaure
48f98a68b8 fix: incorrect posting time fetching incorrect qty (#29103)
(cherry picked from commit f02e6b4631)
2022-01-05 04:23:01 +00:00
mergify[bot]
485fa4700f fix(UX): validate setup on clicking Mark Attendance button in Shift Type (backport #29146) (#29148)
(cherry picked from commit ac816f4fed)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-01-04 21:52:42 +05:30
Deepesh Garg
f90b49a1b5 Merge pull request #29145 from frappe/mergify/bp/version-13-pre-release/pr-29142
fix: map Accounting Dimensions for Bank Entry against Payroll Entry (backport #29142)
2022-01-04 21:02:16 +05:30
Deepesh Garg
e8e93bdf43 Merge pull request #29144 from frappe/mergify/bp/version-13-hotfix/pr-29142
fix: map Accounting Dimensions for Bank Entry against Payroll Entry (backport #29142)
2022-01-04 21:01:54 +05:30
Rucha Mahabal
1eccb83904 fix: map Accounting Dimensions for Bank Entry against Payroll Entry
(cherry picked from commit 5eeb94db31)
2022-01-04 14:58:45 +00:00
Rucha Mahabal
5900968bd9 fix: map Accounting Dimensions for Bank Entry against Payroll Entry
(cherry picked from commit 5eeb94db31)
2022-01-04 14:58:07 +00:00
mergify[bot]
fa735a0539 fix: Modifying Opening invoice creation tool timestamp (#29127) (#29143) 2022-01-04 20:04:47 +05:30
mergify[bot]
b808ef1625 feat: allow user to change the parent company (#29138) 2022-01-04 19:29:39 +05:30
mergify[bot]
06c0313a26 fix: POS items added to cart despite low qty (#29141) 2022-01-04 19:20:19 +05:30
mergify[bot]
f4670a86a0 fix: minor issues (#29140) 2022-01-04 19:16:52 +05:30
Subin Tom
0b07fbd5a2 fix: Modifying Opening invoice creation tool timestamp (#29139) 2022-01-04 19:15:59 +05:30
mergify[bot]
2838dc537e fix: update idx after updating items in so/po (#29134) (#29136)
(cherry picked from commit 733c9defdf)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-01-04 19:05:38 +05:30
Deepesh Garg
1d270dca05 fix: Show GL balance in Accounts Receivable and payable summary 2022-01-04 17:53:08 +05:30
mergify[bot]
3898b19086 fix: future recurring period calculation (backport #29083) (#29119)
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
2022-01-04 12:00:42 +05:30
mergify[bot]
fe1a66cf54 fix: future recurring period calculation (backport #29083) (#29118)
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
(cherry picked from commit 11cfa16158)

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
2022-01-04 11:58:41 +05:30
Saqib Ansari
e2b2be82e0 Merge pull request #29112 from frappe/mergify/bp/version-13-hotfix/pr-28907
fix: Validation in POS for item batch no stock quantity (backport #28907)
2022-01-03 19:35:43 +05:30
Subin Tom
fe3ebafea4 fix: linters fix 2022-01-03 19:10:03 +05:30
Subin Tom
860a0f4696 fix: linters fix 2022-01-03 19:06:40 +05:30
Subin Tom
a34ded79f3 fix: importing Sum() for qb for test fix 2022-01-03 19:01:57 +05:30
Saqib Ansari
d1739eb59a Merge pull request #29113 from frappe/mergify/bp/version-13-hotfix/pr-28160
refactor: `update_invoice_status` with query builder (backport #28160)
2022-01-03 18:32:53 +05:30
Pruthvi Patel
84f3f34821 Update erpnext/controllers/accounts_controller.py
Co-authored-by: Saqib <nextchamp.saqib@gmail.com>
(cherry picked from commit 16a90d3e60)
2022-01-03 12:30:23 +00:00
Pruthvi Patel
50628d416c test: update_invoice_status
(cherry picked from commit 4d48ca4228)
2022-01-03 12:30:23 +00:00
Pruthvi Patel
e648a2ccde fix: consider Discounted status
(cherry picked from commit 0799f378b4)
2022-01-03 12:30:23 +00:00
Pruthvi Patel
1efce21ba4 refactor: update_invoice_status with query builder
(cherry picked from commit 6c96ed4e11)
2022-01-03 12:30:22 +00:00
Subin Tom
110a1e5557 fix: using get_batch_qty method to get available_qty
(cherry picked from commit 7385497219)
2022-01-03 12:27:40 +00:00
Subin Tom
80f92ee547 fix: added Sum() in query
(cherry picked from commit 267da48889)
2022-01-03 12:27:39 +00:00
Saqib
0be144c193 chore: remove extra whitespace
(cherry picked from commit c3a9ca09d1)
2022-01-03 12:27:39 +00:00
Saqib
bf92681fff fix: typo
(cherry picked from commit 9c8f523537)
2022-01-03 12:27:38 +00:00
Subin Tom
3947d859c5 fix: replaced sql query with frappe.qb
(cherry picked from commit 68beee2a00)
2022-01-03 12:27:38 +00:00
Subin Tom
0222cd198d fix: Validation in POS for item batch no stock quantity
(cherry picked from commit 9f235526d4)
2022-01-03 12:27:37 +00:00
rohitwaghchaure
fca8f17b40 Merge pull request #29104 from frappe/mergify/bp/version-13-hotfix/pr-29103
fix: incorrect posting time fetching incorrect qty (backport #29103)
2022-01-03 14:53:39 +05:30
rohitwaghchaure
fcbcb08fcd fix: incorrect posting time fetching incorrect qty (#29103)
(cherry picked from commit f02e6b4631)
2022-01-03 08:58:56 +00:00
mergify[bot]
674a923669 feat: Add currency in import download statement (#29093) 2022-01-03 13:42:25 +05:30
mergify[bot]
8a8648e6ec fix: filter query in bank reconciliation tool (#29100) 2022-01-03 11:41:38 +05:30
mergify[bot]
4fdaf51599 fix: filter query in bank reconciliation tool (#29099) 2022-01-03 11:41:23 +05:30
Deepesh Garg
19154074c1 Merge pull request #29095 from frappe/mergify/bp/version-13-pre-release/pr-29092
fix(India): Tax and Charges template not getting fetched based on tax category assigned (backport #29092)
2022-01-02 19:30:46 +05:30
Deepesh Garg
9a32bb6b51 fix: Test Case
(cherry picked from commit 342658ea70)
2022-01-02 13:37:23 +00:00
Deepesh Garg
cf4cac05ab fix(India): Tax and Charges template not getting fetched based on tax category assigned
(cherry picked from commit 7a5937a98c)
2022-01-02 13:37:23 +00:00
Deepesh Garg
39f7a9de47 Merge pull request #29092 from deepeshgarg007/taxes_category_tax_fetch
fix(India): Tax and Charges template not getting fetched based on tax category assigned
2022-01-02 19:06:00 +05:30
Deepesh Garg
342658ea70 fix: Test Case 2022-01-02 18:39:59 +05:30
Deepesh Garg
7a5937a98c fix(India): Tax and Charges template not getting fetched based on tax category assigned 2022-01-02 17:53:15 +05:30
Devin Slauenwhite
d763b16e9f Merge remote-tracking branch 'upstream/version-13-hotfix' into fix-ecommerce-cart-badge 2021-12-31 10:41:37 -05:00
Devin Slauenwhite
2602fc633b fix(e-commerce): shopping cart total quantity (#29076)
* fix: shopping cart total quantity

* fix: linter error
2021-12-31 20:28:00 +05:30
mergify[bot]
79d126f5d0 fix: leave_allocation variable not being defined (backport #29086) (#29087)
(cherry picked from commit dbbc8d8aed)

Co-authored-by: Vaibhav Chopra <53619134+sudo-vaibhav@users.noreply.github.com>
2021-12-31 13:54:18 +05:30
Devin Slauenwhite
c9a9d0bab8 fix(ux): tripple digit cart qty 2021-12-29 21:47:04 -05:00
rohitwaghchaure
0703c1f996 Merge pull request #29073 from rohitwaghchaure/merge-hotfix-to-pre-release-for-13-18
chore: Merge branch 'version-13-hotfix' into 'version-13-pre-release'
2021-12-29 23:29:28 +05:30
Rohit Waghchaure
b378a225d0 chore: Merge branch 'version-13-hotfix' into 'version-13-pre-release' 2021-12-29 23:10:55 +05:30
rohitwaghchaure
12f5464a3b Merge pull request #29066 from frappe/mergify/bp/version-13-hotfix/pr-29064
fix: optimize patch for update bom in SO and MR (backport #29064)
2021-12-29 13:59:40 +05:30
Rohit Waghchaure
9d575e6e27 fix: optimize patch for update bom in SO and MR
(cherry picked from commit d9b9f4e8b7)
2021-12-29 06:39:14 +00:00
Deepesh Garg
098b3614e1 Merge pull request #29061 from frappe/mergify/bp/version-13-hotfix/pr-28034
fix: Updates in term loan processing (backport #28034)
2021-12-28 19:36:04 +05:30
Deepesh Garg
bbe4bc1fc0 fix: Resolve conflicts 2021-12-28 19:10:27 +05:30
Deepesh Garg
0f8a7d7a6c fix: Update modified timestamp 2021-12-28 18:53:10 +05:30
Deepesh Garg
a4396d7f31 fix: Add test for loan repayment cancellation
(cherry picked from commit 68d49817a1)
2021-12-28 13:10:38 +00:00
Deepesh Garg
4248bebe48 fix: Test cases
(cherry picked from commit f78bf4c6ef)
2021-12-28 13:10:37 +00:00
Deepesh Garg
b616443809 fix: Book unaccrued interest check
(cherry picked from commit c572a4cb88)
2021-12-28 13:10:36 +00:00
Deepesh Garg
3c803c46d3 fix: Repayment schedule revert on cancel
(cherry picked from commit 8f6600b27a)
2021-12-28 13:10:35 +00:00
Deepesh Garg
dc3a0337d1 fix: Unsecured loan status update
(cherry picked from commit dcae9ba86e)

# Conflicts:
#	erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
2021-12-28 13:10:34 +00:00
Deepesh Garg
430617cb68 fix: Loan repayment schedule date
(cherry picked from commit 1a5f0da6ca)
2021-12-28 13:10:32 +00:00
Deepesh Garg
2b1b831560 fix: Updates in term loan processing
(cherry picked from commit 8116b9b62f)

# Conflicts:
#	erpnext/loan_management/doctype/loan/loan.json
2021-12-28 13:10:31 +00:00
mergify[bot]
165114ba4c fix: tests (#29051) (#29060)
* fix(test): create salary slip as per timesheet dates for Project Profitability

* fix(test): reload salary slip before checking for payment days

(cherry picked from commit 88df5ce530)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-12-28 17:28:31 +05:30
Marica
964b19e1b1 Merge pull request #29034 from frappe/mergify/bp/version-13-hotfix/pr-29033
fix: avoid `"string" in None` condition (backport #29033)
2021-12-28 15:02:28 +05:30
Marica
b9e327515b Merge pull request #29058 from frappe/mergify/bp/version-13-hotfix/pr-28555
fix: Validate Finished Goods for Independent Manufacture entries as well (backport #28555)
2021-12-28 15:01:11 +05:30
Deepesh Garg
e0a88d99f4 Merge pull request #29052 from frappe/mergify/bp/version-13-hotfix/pr-29009
fix: Start date validation for deferred invoices (backport #29009)
2021-12-28 14:45:58 +05:30
marination
00b9f1fbef fix: Merge conflict 2021-12-28 14:41:03 +05:30
marination
59aaef3719 fix: Avoid Impact on Repack Entry
- Repack entries must be considered for fg/scrap checkbox auto set
- This must be avoided only for Manufacture entries, due to confusion between scrap and fg
- No scrap in repack, so its straight forward

(cherry picked from commit 22809a2838)
2021-12-28 09:07:08 +00:00
marination
fdf5811c27 fix: Sider
(cherry picked from commit 7d0340e660)
2021-12-28 09:07:07 +00:00
marination
66a5b3f34d fix: Linter (imports)
(cherry picked from commit dfff972a78)
2021-12-28 09:07:07 +00:00
marination
d8b781bb1e test: Independent Manufacture Entry
- Check validations
- Check if FG basic rate is calculated correctly

(cherry picked from commit affb29194b)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/test_stock_entry.py
2021-12-28 09:07:07 +00:00
marination
6c6785e284 fix: Dont auto set is finished item or is scrap itm checkbox for independent entry
- System cant differentiate between scrap and FG when WO is absent, so dont auto set
- User must set it manually
- `validate_finished_goods` checks this

(cherry picked from commit 8c9779d69d)
2021-12-28 09:07:06 +00:00
marination
fc57ed1ca8 fix: Validate Finished Goods for Independent Manufacture entries as well
- Check if FG exists even if WO is absent
- Check if multiple FGs are there, block.

(cherry picked from commit d6bc121999)
2021-12-28 09:07:05 +00:00
mergify[bot]
aeb7bbc629 fix: rename non existent doctype field to the right one (#29055) (#29056)
(cherry picked from commit 095a6e186f)

Co-authored-by: Development for People <47140294+developmentforpeople@users.noreply.github.com>
2021-12-28 14:13:09 +05:30
Deepesh Garg
bdb6c4170b fix: Test case 2021-12-28 14:13:04 +05:30
Deepesh Garg
701bca5845 Merge pull request #28996 from deepeshgarg007/bank_reco_tool_fixes
fix: Issues in Bank Reconciliation tool
2021-12-28 13:40:28 +05:30
Deepesh Garg
becb39da90 fix: Remove debug statement 2021-12-28 13:18:17 +05:30
Marica
aa58653b75 Merge pull request #29053 from frappe/mergify/bp/version-13-hotfix/pr-28966
fix: Stock Ageing Report - Negative Opening Stock (backport #28966)
2021-12-28 13:04:33 +05:30
marination
bae184fc2f fix: Merge Conflicts 2021-12-28 12:43:14 +05:30
marination
24727dbadb fix: Linter (extra line after import)
(cherry picked from commit 6ca978c18b)
2021-12-28 06:54:48 +00:00
marination
ba5f4843c0 test: Stock Ageing FIFO Slot generation
(cherry picked from commit 098f72e7ec)
2021-12-28 06:54:47 +00:00
marination
bf266d82e7 chore: Added .md file to explain stock ageing business logic
(cherry picked from commit c007f84ade)
2021-12-28 06:54:47 +00:00
marination
e30c52861d chore: Use frappe.qb for query
(cherry picked from commit 878fd377c2)
2021-12-28 06:54:46 +00:00
marination
57b7fe6627 fix: Sider and Server side test
- args passed to wrong function
- missing space around '='

(cherry picked from commit 24a35c69c0)
2021-12-28 06:54:46 +00:00
marination
c04b62a7dd fix: Linter (imports)
(cherry picked from commit 8951a5c267)
2021-12-28 06:54:46 +00:00
marination
19fd1fb4b1 fix: Stock Ageing Report - Negative Opening Stock
- Consider negative opening stock in logic and neutralise it with +ve stock
- minor code refactor: class for FIFOSlots to generate chronological FIFO queue

(cherry picked from commit 0f43792dbb)

# Conflicts:
#	erpnext/stock/report/stock_ageing/stock_ageing.py
2021-12-28 06:54:46 +00:00
Deepesh Garg
b00f6676f1 fix: Start date validation for deferred invoices
(cherry picked from commit 7ad149f9fe)
2021-12-28 06:36:26 +00:00
Deepesh Garg
8be6bd978e Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-29033 2021-12-28 11:03:34 +05:30
mergify[bot]
bffe329ade fix: filter out Claimed employee advances in Expense Claim (#29046) (#29047)
(cherry picked from commit 25f4de80b3)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-12-27 21:23:50 +05:30
Rucha Mahabal
b0bc3994c3 fix: flaky HR tests (#29045)
* fix(test): Leave Allocation validation against Leave Application after submit (#29005)

* fix(test): Leave Allocation validation against Leave Application after submit

* chore: clean-up Leave Allocation tests

* fix(test): set holiday list for leave allocation test

* fix: flaky HR tests (#29017)

* fix(test): use root company in Expense Claim tests

* fix(test): set Holiday List for Leave Allocation

* fix(test): set Holiday List for company
2021-12-27 17:26:16 +05:30
Rucha Mahabal
d0d82bc34a fix: ignore links while setting default notification templates in Settings (#29042) 2021-12-27 15:16:58 +05:30
Marica
1c2a8c679c Merge pull request #28951 from dj12djdjs/fix-ecommerce-selling-price-with-rule
fix(e-commerce): fetch selling price with pricing rule
2021-12-27 12:49:33 +05:30
Marica
39fd9ee40d Merge branch 'version-13-hotfix' into fix-ecommerce-selling-price-with-rule 2021-12-27 12:19:41 +05:30
Raffael Meyer
f92cdbbbf3 fix: avoid "string" in None condition
(cherry picked from commit 52397c9771)
2021-12-26 04:50:10 +00:00
Deepesh Garg
3f6d9336b5 Merge pull request #28953 from deepeshgarg007/price_fetch_optimize
fix(UX): Optimize rate updation on changing price list
2021-12-24 19:33:29 +05:30
mergify[bot]
b2e42aa16c chore: add running stock value difference in invariant report (#29012) (#29013)
[skip ci]

(cherry picked from commit 7987a46509)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-23 21:17:42 +05:30
Devin Slauenwhite
6783fc808b Merge remote-tracking branch 'upstream/version-13-hotfix' into fix-ecommerce-selling-price-with-rule 2021-12-22 20:58:34 -05:00
Devin Slauenwhite
bcc436e63e fix(test): e-commerce fetch logged-in user item price. 2021-12-22 20:20:57 -05:00
Devin Slauenwhite
7eafc9ffe8 test: e-commerce customer pricing rule 2021-12-22 13:11:02 -05:00
Deepesh Garg
ff183ee895 fix: Issues in Bank Reconciliation tool 2021-12-22 18:04:49 +05:30
Marica
808e40cc4d Merge pull request #28985 from marination/item-card-group-links
fix: Convert Item links to Website Item links in `Item Card Group` template data
2021-12-22 13:53:25 +05:30
Marica
2777b501b5 Merge branch 'version-13-hotfix' into item-card-group-links 2021-12-22 13:25:18 +05:30
marination
a6e7b43f0b fix: Faulty 'continue' not setting value in db 2021-12-22 13:23:00 +05:30
marination
ffdaa849fb fix: Code cleanup 2021-12-22 12:26:43 +05:30
marination
85abb95df6 fix: Sider 2021-12-22 12:24:09 +05:30
Deepesh Garg
b60fbf5ba9 fix: Add round floats for price list rate 2021-12-22 12:01:39 +05:30
Deepesh Garg
233f79bf96 fix: Recalculate taxes irrespective of price list rate changed or not 2021-12-22 11:26:19 +05:30
mergify[bot]
dd01c0e854 fix: Currency fix for cost field in subscription plan (#28821) (#28898)
(cherry picked from commit be103ab8e6)

Co-authored-by: Subin Tom <36098155+nemesis189@users.noreply.github.com>
2021-12-22 10:43:00 +05:30
Devin Slauenwhite
de697a7aee Merge remote-tracking branch 'upstream/version-13-hotfix' into version-13-hotfix 2021-12-21 11:24:53 -05:00
mergify[bot]
b59df1118e fix: missed colon (#28979) (#28984)
(cherry picked from commit 076cb408db)

Co-authored-by: Development for People <47140294+developmentforpeople@users.noreply.github.com>
2021-12-21 16:04:22 +05:30
marination
242cc7e23e Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into item-card-group-links 2021-12-21 15:51:50 +05:30
marination
dfe718ef79 fix: Convert Item links to Website Item links in Item Card Group template data
- Changed link option to Website Item in Item card group template
- patch to convert pre-existing data
2021-12-21 15:43:48 +05:30
mergify[bot]
84404bf6e3 fix: Reset value_after_depreciation on reversing journal entry during Asset return (#28980)
(cherry picked from commit 1ed30ee7c7)

Co-authored-by: Ganga Manoj <ganga.manoj98@gmail.com>
2021-12-21 15:13:11 +05:30
Deepesh Garg
56d72764b7 Merge pull request #28935 from deepeshgarg007/rcm_tax_template_fetch_issue
fix: Is Reverse Charge check in Tax Category
2021-12-21 13:41:22 +05:30
Deepesh Garg
84cebca254 Merge pull request #28974 from frappe/mergify/bp/version-13-hotfix/pr-28937
fix: Added filter for dispatch address (backport #28937)
2021-12-21 13:04:07 +05:30
Deepesh Garg
7e912db4b1 fix: Add is reverse charge in country wise tax 2021-12-21 12:54:40 +05:30
Deepesh Garg
3fed740142 Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into rcm_tax_template_fetch_issue 2021-12-21 12:54:13 +05:30
Subin Tom
824c90fd11 fix: Added filter for dispatch address (#28937)
(cherry picked from commit 98d417602f)
2021-12-21 07:06:25 +00:00
mergify[bot]
ef47570977 test: remove unnecessary creation of companies #28965 (#28967)
test: remove unnecessary creation of companies
(cherry picked from commit d7148adee5)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-20 23:22:34 +05:30
mergify[bot]
87e53e9e46 fix: fetch appointment letter content in the same order as template (#28968) (#28969)
(cherry picked from commit 92e86f2f09)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-12-20 23:17:02 +05:30
Ankush Menat
4107de4514 Merge pull request #28963 from frappe/mergify/bp/version-13-hotfix/pr-28588
fix: incorrect bin qty on backdated reconciliation (backport #28588)
2021-12-20 21:22:56 +05:30
Ankush Menat
9338843dac test: bin qty on backdated SR
(cherry picked from commit 2bb7bca464)
2021-12-20 14:36:40 +00:00
Ankush Menat
2d5f477720 fix: correct bin qty on backdated transactions
When making a backdated transactions current balance qty depends on
evaluation of whole ledger inbetween, instead of doing that just fetch
the last sle's qty_after_transaction when future transactions are
detected against SLE

fix: don't update bin's actual_qty

1. it's already updated by repost_current_voucher
2. update if future sle exists

(cherry picked from commit ff9a6e8e89)
2021-12-20 14:36:40 +00:00
Deepesh Garg
fa7948b5e5 Merge pull request #28961 from frappe/mergify/bp/version-13-hotfix/pr-28959
fix: Error on viewing consolidated financial statement (backport #28959)
2021-12-20 17:15:13 +05:30
Deepesh Garg
aad7a10a7b fix: Error on viewing consolidated financial statement
(cherry picked from commit 5d6199cf35)
2021-12-20 11:43:50 +00:00
Deepesh Garg
c12b00cb4b Merge pull request #28901 from frappe/mergify/bp/version-13-hotfix/pr-28822
feat: Deferred Revenue and Expense report with actual and upcoming postings (backport #28822)
2021-12-20 17:03:50 +05:30
Deepesh Garg
0980c2f981 fix: Linting issues 2021-12-20 13:26:16 +05:30
Deepesh Garg
4b3ef24378 Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into price_fetch_optimize 2021-12-20 12:17:37 +05:30
Deepesh Garg
6087d5a603 fix(UX): Optimize rate updation of changing price list 2021-12-20 12:16:14 +05:30
Devin Slauenwhite
372f91c923 fix: fetch correct selling price. 2021-12-18 16:03:16 -05:00
Ankush Menat
0724a148e6 fix: incorrect rounding off near zero (#28948) 2021-12-18 21:34:25 +05:30
mergify[bot]
d3c62821fb fix: Add Invoice Number field to list view in Opening Invoice Creation Tool (#28941) 2021-12-18 12:07:45 +05:30
mro-admin
c5931e4b1e fix: convert asynchronous field update to synchronous (#28906)
Co-authored-by: Dan Navarro on Ubuntu Work <danjeremynavarro@gmail.com>
2021-12-17 19:06:08 +05:30
Deepesh Garg
a1d5e0bef6 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-28822 2021-12-17 16:06:21 +05:30
Deepesh Garg
10a95b3a98 Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into rcm_tax_template_fetch_issue 2021-12-17 16:03:22 +05:30
Deepesh Garg
7c1bfe6b46 chore: Remove patch comment 2021-12-17 16:02:40 +05:30
Deepesh Garg
b33fd6acc7 fix: Is Reverse Charge check in Tax Category 2021-12-17 15:59:21 +05:30
mergify[bot]
9c2a9c28df fix(ksa): enable ksa pos invoice print format (backport #28911) 2021-12-17 15:27:05 +05:30
Deepesh Garg
349785dcbd Merge pull request #28932 from frappe/mergify/bp/version-13-hotfix/pr-28916
fix: Multicurrency invoices using subscription (backport #28916)
2021-12-17 13:52:30 +05:30
Deepesh Garg
94d3e66428 fix: Add tests for multicurrency subscription
(cherry picked from commit 6ab753015e)
2021-12-17 06:59:59 +00:00
Deepesh Garg
eb36742427 fix: Multicurrency invoices using subscription
(cherry picked from commit 0e4842b986)
2021-12-17 06:59:59 +00:00
mergify[bot]
fcb08d54ea fix: mapping to maintenance visit gets erased (#28917) (#28925)
* fix: mappings to maintenance visit gets erased

* test: mapping of schedule to visit

(cherry picked from commit 4cb5b627a1)

Co-authored-by: Noah Jacob <noahjacobkurian@gmail.com>
2021-12-16 23:33:56 +05:30
mergify[bot]
167e8d3dde chore: ledger invariant check report (#28921) (#28922)
(cherry picked from commit 6ba8f7644d)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-16 22:48:12 +05:30
mergify[bot]
85ded9e7ce perf: remove forcing of posting sort index on stock balance (#28902) (#28904)
mysql is generally smart enough to figure out which index is better
based on cardinality of index. While posting sort index is better for
low item high # of SLE scenario it's bad for high item variety each with
lower count of SLEs.

(cherry picked from commit 228e011819)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-15 19:23:12 +05:30
ruthra
74ac5e7829 refactor: rename checkbox and translations
- rename checkbox and make it
 - added translation text for labels
 - fixes for linter error

(cherry picked from commit 9ec8841b91)
2021-12-15 09:33:46 +00:00
ruthra
804f2ccf3d refactor: remove unused function 'get_postings'
(cherry picked from commit a8a598ace7)
2021-12-15 09:33:45 +00:00
ruthra
039b1fbb76 feat: Deferred Revenue and Expense report
- show deferred revenue and expense with actual and expected postings
 - unit tests added

(cherry picked from commit c3453cd73c)
2021-12-15 09:33:45 +00:00
mergify[bot]
970cdef911 feat: added required_date field to set date in child table (backport #28432)
feat: added required_date field to set date in child table (backport #28432)
2021-12-15 12:49:23 +05:30
mergify[bot]
1988e72710 revert: "fix: expected value after useful life validation" (backport #28853) 2021-12-15 11:09:21 +05:30
Saqib
cd075ce3be Merge pull request #28854 from frappe/mergify/bp/version-13-hotfix/pr-28783
fix: hsn-wise summary is incorrect if an invoice has repeated item code (backport #28783)
2021-12-15 10:35:25 +05:30
Ankush Menat
bdd43274b2 fix: incorrect logic for "Reserved Qty for Production" (#28880)
* fix: reservation for production incorrect

The query uses case to decide what fields to compute reservation on,
this case is outermost case hence the very first Work order's "Skip
transfer" is considered for ALL work orders.

Solution: move the case inside Sum.

Steps to reproduce:
1. Make work order for more than 1 qty (with | without skip transfer)
2. Create manufacture and transfer entries.
3. Keep checking reserved quantities during this process.

* test: use default warehouse for testing reservation

(cherry picked from commit 80f1a8c645)
2021-12-14 18:22:10 +05:30
Ankush Menat
25fd11e24f test: timeout certain tests in work order to avoid stuck tests (#28666) 2021-12-14 18:21:48 +05:30
Ankush Menat
73693ac153 test: remove commits from tests (#27939)
* test: remove commits from tests in selling module

* test: remove commits from manufacturing module

* test: remove commits from accounts module tests

* test: remove commits from controller tests

* test: keep commits in accounting dimension test

* Revert "test: remove commits from controller tests"

This reverts commit f5cfbee5da.

* Revert "test: remove commits from accounts module tests"

This reverts commit df4c27efdd.

(cherry picked from commit 96ef88d935)
2021-12-14 18:21:48 +05:30
mergify[bot]
a4814f5519 fix: remove set only once from valuation method (#28876) (#28882)
(cherry picked from commit 66960cf050)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-14 17:29:40 +05:30
Deepesh Garg
a6f9b523d9 Merge pull request #28867 from frappe/mergify/bp/version-13-hotfix/pr-28832
fix: KSA QR field rename patch (backport #28832)
2021-12-14 16:01:57 +05:30
mergify[bot]
d0d5236a1a Merge pull request #28845 from ruchamahabal/fix-salary-slip-timesheet (#28874)
fix: incorrect amount based on payment days in timesheet salary slip
(cherry picked from commit b027050f77)

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2021-12-14 15:32:22 +05:30
mergify[bot]
b3720f62d8 fix: Nonstock item showing in Itemwise Recommended Reorder Level report bp (#28873)
(cherry picked from commit 304aa44feb)

Co-authored-by: aaronmenezes <ron2805@gmail.com>
2021-12-14 15:19:11 +05:30
mergify[bot]
187a69e390 fix: reorder patch to fix custom field options (backport #28866)
* fix: reorder patch to fix custom field options (#28866)

(cherry picked from commit 7fd06108bf)

# Conflicts:
#	erpnext/patches.txt

* fix: resolve conflict

Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-14 14:03:14 +05:30
Deepesh Garg
55db2f8808 fix: KSA QR field rename patch
(cherry picked from commit 14d03a9446)
2021-12-14 07:18:33 +00:00
mergify[bot]
1a986f0b11 fix(UX): hide RM table(Job Card) if material transfer is against work order (backport #28746)
* fix(UX): hide RM table(Job Card) if material transfer is against work order (#28746)

(cherry picked from commit cc8c0595e2)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.js

* chore: resolve conflicts

Co-authored-by: Noah Jacob <noahjacobkurian@gmail.com>
Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-14 12:44:42 +05:30
mergify[bot]
5798687951 fix: packing slip incorrect item value (bp #28862)
(cherry picked from commit 534c64abcd)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-14 12:16:42 +05:30
mergify[bot]
46a6c8dbfe fix: migration hashes (#28850)
Co-authored-by: Ankush Menat <ankush@frappe.io>
2021-12-14 11:10:57 +05:30
Saqib Ansari
1f41c7fed1 test: test_hsn_summary_for_invoice_with_duplicate_items
(cherry picked from commit 6a239c8ea3)
2021-12-13 17:10:20 +00:00
Saqib Ansari
1c96c6fe08 fix: hsn-wise summary is incorrect if an invoice has repeated item code
(cherry picked from commit e74739a677)
2021-12-13 17:10:19 +00:00
GangaManoj
d000782de0 fix: Edit dates and frequency of depreciation
(cherry picked from commit 4311936b7d)
2021-12-13 16:20:51 +00:00
GangaManoj
855ae399b5 fix: Test if clear_depreciation_schedule() works for multiple finance books
(cherry picked from commit 6ec5a19063)
2021-12-13 16:20:50 +00:00
GangaManoj
ae875ab021 fix: Retain depreciation schedule rows that are linked with JEs while clearing the schedule
(cherry picked from commit 33a0b1db2c)
2021-12-13 16:20:49 +00:00
GangaManoj
57f41bc30a fix: Test if multiple Depreciation Schedules are set up for multiple Finance Books
(cherry picked from commit f455de2924)
2021-12-13 16:20:49 +00:00
GangaManoj
8a4019e501 fix: Clear Depreciation Schedule entries that aren't linked with Journal Entries before modifying the schedule
(cherry picked from commit 475d8394e4)
2021-12-13 16:20:48 +00:00
GangaManoj
54cac351cd fix: Rename loop variable
(cherry picked from commit 059d1f3b74)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.py
2021-12-13 16:20:47 +00:00
GangaManoj
db3d2c31a3 fix: Prevent clearing of Depreciation Schedule on adding more than one Finance Book
(cherry picked from commit a4043c035d)
2021-12-13 16:20:43 +00:00
259 changed files with 6396 additions and 2562 deletions

View File

@@ -28,6 +28,7 @@ ignore =
B007,
B950,
W191,
E124, # closing bracket, irritating while writing QB code
max-line-length = 200
exclude=.github/helper/semgrep_rules

View File

@@ -5,9 +5,14 @@ on:
paths-ignore:
- '**.js'
- '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
concurrency:
group: patch-mariadb-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@@ -25,6 +30,11 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone
uses: actions/checkout@v2

View File

@@ -5,6 +5,7 @@ on:
paths-ignore:
- '**.js'
- '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
push:
branches: [ develop ]
@@ -12,6 +13,10 @@ on:
- '**.js'
- '**.md'
concurrency:
group: server-mariadb-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@@ -35,6 +40,12 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone
uses: actions/checkout@v2
@@ -89,39 +100,8 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload Coverage Data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- name: Clone
uses: actions/checkout@v2
- name: Coveralls Finished
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,6 +6,10 @@ on:
- '**.md'
workflow_dispatch:
concurrency:
group: ui-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
__version__ = '13.17.0'
__version__ = '13.20.0'
def get_default_company(user=None):
'''Get default company for user'''
@@ -56,9 +56,9 @@ def set_perpetual_inventory(enable=1, company=None):
company.enable_perpetual_inventory = enable
company.save()
def encode_company_abbr(name, company):
def encode_company_abbr(name, company=None, abbr=None):
'''Returns name encoded with company abbreviation'''
company_abbr = frappe.get_cached_value('Company', company, "abbr")
company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr")
parts = name.rsplit(" - ", 1)
if parts[-1].lower() != company_abbr.lower():

View File

@@ -255,11 +255,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" \
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): return
account_currency = get_account_currency(item.expense_account)
account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice":
against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account
@@ -280,6 +282,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if not amount:
return
# check if books nor frozen till endate:
if getdate(end_date) >= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
@@ -407,8 +413,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': credit_account,
'credit': base_amount,
'credit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency,
'reference_name': doc.name,
'reference_type': doc.doctype,
@@ -421,8 +425,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': debit_account,
'debit': base_amount,
'debit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency,
'reference_name': doc.name,
'reference_type': doc.doctype,

View File

@@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
frm.trigger('add_toolbar_buttons');
}
if (frm.has_perm('write')) {
frm.add_custom_button(__('Update Account Name / Number'), function () {
frm.trigger("update_account_number");
});
frm.add_custom_button(__('Merge Account'), function () {
frm.trigger("merge_account");
});
}, __('Actions'));
frm.add_custom_button(__('Update Account Name / Number'), function () {
frm.trigger("update_account_number");
}, __('Actions'));
}
}
},
@@ -59,11 +59,12 @@ frappe.ui.form.on('Account', {
}
},
add_toolbar_buttons: function(frm) {
frm.add_custom_button(__('Chart of Accounts'),
function () { frappe.set_route("Tree", "Account"); });
frm.add_custom_button(__('Chart of Accounts'), () => {
frappe.set_route("Tree", "Account");
}, __('View'));
if (frm.doc.is_group == 1) {
frm.add_custom_button(__('Group to Non-Group'), function () {
frm.add_custom_button(__('Convert to Non-Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_group_to_ledger',
@@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
frm.refresh();
}
});
});
}, __('Actions'));
} else if (cint(frm.doc.is_group) == 0
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
frm.add_custom_button(__('Ledger'), function () {
frm.add_custom_button(__('General Ledger'), function () {
frappe.route_options = {
"account": frm.doc.name,
"from_date": frappe.sys_defaults.year_start_date,
@@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
});
}, __('View'));
frm.add_custom_button(__('Non-Group to Group'), function () {
frm.add_custom_button(__('Convert to Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_ledger_to_group',
@@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
frm.refresh();
}
});
});
}, __('Actions'));
}
},

View File

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

View File

@@ -218,6 +218,8 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account')
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0
@@ -226,7 +228,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
total_amount += get_paid_amount(frappe._dict({
'payment_document': voucher['payment_doctype'],
'payment_entry': voucher['payment_name'],
}), transaction.currency)
}), transaction.currency, company_account)
if total_amount > transaction.unallocated_amount:
frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
@@ -261,7 +263,7 @@ def get_linked_payments(bank_transaction_name, document_types = None):
return matching
def check_matching(bank_account, company, transaction, document_types):
# combine all types of vocuhers
# combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types)
filters = {
"amount": transaction.unallocated_amount,
@@ -343,13 +345,11 @@ def get_pe_matching_query(amount_condition, account_from_to, transaction):
def get_je_matching_query(amount_condition, transaction):
# get matching journal entry query
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
root_type = frappe.get_value("Account", company_account, "root_type")
if root_type == "Liability":
cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
else:
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f"""

View File

@@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
"withdrawal",
"description",
"reference_number",
"bank_account"
"bank_account",
"currency"
],
},
});

View File

@@ -17,6 +17,7 @@ from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from six import string_types
INVALID_VALUES = ("", None)
class BankStatementImport(DataImport):
def __init__(self, *args, **kwargs):
@@ -96,6 +97,18 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows()
def parse_data_from_template(raw_data):
data = []
for i, row in enumerate(raw_data):
if all(v in INVALID_VALUES for v in row):
# empty row
continue
data.append(row)
return data
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
"""This method runs in background job"""
@@ -105,7 +118,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
file = import_file_path if import_file_path else google_sheets_url
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
data = import_file.raw_data
data = parse_data_from_template(import_file.raw_data)
if import_file_path:
add_bank_account(data, bank_account)

View File

@@ -102,7 +102,7 @@ def get_total_allocated_amount(payment_entry):
AND
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
def get_paid_amount(payment_entry, currency):
def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
@@ -115,7 +115,7 @@ def get_paid_amount(payment_entry, currency):
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")

View File

@@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", {
if(frm.doc.docstatus==1) {
frm.add_custom_button(__('Reverse Journal Entry'), function() {
return erpnext.journal_entry.reverse_journal_entry(frm);
}, __('Make'));
}, __('Actions'));
}
if (frm.doc.__islocal) {

View File

@@ -13,6 +13,7 @@
"voucher_type",
"naming_series",
"finance_book",
"reversal_of",
"tax_withholding_category",
"column_break1",
"from_template",
@@ -515,13 +516,21 @@
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount "
},
{
"depends_on": "eval:doc.docstatus",
"fieldname": "reversal_of",
"fieldtype": "Link",
"label": "Reversal Of",
"options": "Journal Entry",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2021-09-09 15:31:14.484029",
"modified": "2022-01-04 13:39:36.485954",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -397,13 +397,14 @@ class JournalEntry(AccountsController):
debit_or_credit = 'Debit' if d.debit else 'Credit'
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
debit_or_credit)
against_voucher = ['', against_voucher[1]]
else:
if d.reference_type == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
else:
party_account = against_voucher[1]
if (against_voucher[0] != d.party or party_account != d.account):
if (against_voucher[0] != cstr(d.party) or party_account != d.account):
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
d.reference_type, d.reference_name))
@@ -468,13 +469,22 @@ class JournalEntry(AccountsController):
def set_against_account(self):
accounts_debited, accounts_credited = [], []
for d in self.get("accounts"):
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
for d in self.get('accounts'):
if d.reference_type == 'Sales Invoice':
field = 'customer'
else:
field = 'supplier'
for d in self.get("accounts"):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else:
for d in self.get("accounts"):
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self):
for d in self.get('accounts'):
@@ -1147,9 +1157,8 @@ def make_inter_company_journal_entry(name, voucher_type, company):
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def update_accounts(source, target, source_parent):
target.reference_type = "Journal Entry"
target.reference_name = source_parent.name
def post_process(source, target):
target.reversal_of = source.name
doclist = get_mapped_doc("Journal Entry", source_name, {
"Journal Entry": {
@@ -1167,9 +1176,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
"debit": "credit",
"credit_in_account_currency": "debit_in_account_currency",
"credit": "debit",
},
"postprocess": update_accounts,
}
},
}, target_doc)
}, target_doc, post_process)
return doclist

View File

@@ -75,7 +75,7 @@
],
"hide_toolbar": 1,
"issingle": 1,
"modified": "2019-07-25 14:57:33.187689",
"modified": "2022-01-04 16:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",

View File

@@ -135,7 +135,7 @@ class OpeningInvoiceCreationTool(Document):
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
rate = flt(row.outstanding_amount) / flt(row.qty)
return frappe._dict({
item_dict = frappe._dict({
"uom": default_uom,
"rate": rate or 0.0,
"qty": row.qty,
@@ -146,6 +146,13 @@ class OpeningInvoiceCreationTool(Document):
"cost_center": cost_center
})
for dimension in get_accounting_dimensions():
item_dict.update({
dimension: row.get(dimension)
})
return item_dict
item = get_item_dict()
invoice = frappe._dict({
@@ -159,13 +166,14 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0
"update_stock": 0,
"invoice_number": row.invoice_number
})
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
invoice.update({
dimension: item.get(dimension)
dimension: self.get(dimension) or item.get(dimension)
})
return invoice
@@ -200,10 +208,13 @@ def start_import(invoices):
names = []
for idx, d in enumerate(invoices):
try:
invoice_number = None
if d.invoice_number:
invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
doc.insert()
doc.insert(set_name=invoice_number)
doc.submit()
frappe.db.commit()
names.append(doc.name)

View File

@@ -7,21 +7,26 @@ import frappe
from frappe.cache_manager import clear_doctype_cache
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
test_dependencies = ["Customer", "Supplier"]
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(unittest.TestCase):
def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
create_dimension()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2)
party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department)
doc.update(args)
return doc.make_invoices()
@@ -92,6 +97,33 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def test_renaming_of_invoice_using_invoice_number_field(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
#teardown
for inv in [sales_inv1, sales_inv2]:
doc = frappe.get_doc('Sales Invoice', inv)
doc.cancel()
def test_opening_invoice_with_accounting_dimension(self):
invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC')
expected_value = {
"keys": ["customer", "outstanding_amount", "status", "department"],
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
}
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
def tearDown(self):
disable_dimension()
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
@@ -107,7 +139,8 @@ def get_opening_invoice_creation_dict(**args):
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company)
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": args.get("invoice_number")
},
{
"qty": 2.0,
@@ -116,7 +149,8 @@ def get_opening_invoice_creation_dict(**args):
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company)
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": None
}
]
})
@@ -132,7 +166,7 @@ def make_company():
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
company.country = "India"
company.country = "Pakistan"
company.insert()
return company

View File

@@ -1,9 +1,11 @@
{
"actions": [],
"creation": "2017-08-29 04:26:36.159247",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"invoice_number",
"party_type",
"party",
"temporary_opening_account",
@@ -103,10 +105,18 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"description": "Reference number of the invoice from the previous system",
"fieldname": "invoice_number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Invoice Number"
}
],
"istable": 1,
"modified": "2019-07-25 15:00:00.460695",
"links": [],
"modified": "2022-01-04 18:40:15.927675",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",
@@ -116,4 +126,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -3,6 +3,7 @@
import json
from functools import reduce
import frappe
from frappe import ValidationError, _, scrub, throw
@@ -1524,6 +1525,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']:
pe.project = (doc.get('project') or
reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items
if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party)
pe.set("bank_account", bank_account)
@@ -1709,7 +1714,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage':

View File

@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_multi_mode_option,
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@@ -125,9 +126,26 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos:
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
def validate_pos_reserved_batch_qty(self, item):
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
bold_invalid_batch_no = frappe.bold(item.batch_no)
if (available_batch_qty - reserved_batch_qty) == 0:
frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
.format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
@@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice):
if d.serial_no:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
elif d.batch_no:
self.validate_pos_reserved_batch_qty(d)
else:
if allow_negative_stock:
return
@@ -334,7 +354,6 @@ class POSInvoice(SalesInvoice):
if not for_validate and not self.customer:
self.customer = profile.customer
self.ignore_pricing_rule = profile.ignore_pricing_rule
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.set_warehouse = profile.get('warehouse') or self.set_warehouse

View File

@@ -521,6 +521,72 @@ class TestPOSInvoice(unittest.TestCase):
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400)
def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_batch_item_with_batch,
)
create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01')
item = frappe.get_doc('Item', '_BATCH ITEM')
batch = frappe.get_doc('Batch', 'TestBatch 01')
batch.submit()
item.batch_no = 'TestBatch 01'
item.save()
se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01')
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
pos_inv1.items[0].batch_no = 'TestBatch 01'
pos_inv1.save()
pos_inv1.submit()
pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
pos_inv2.items[0].batch_no = 'TestBatch 01'
pos_inv2.save()
self.assertRaises(frappe.ValidationError, pos_inv2.submit)
#teardown
pos_inv1.reload()
pos_inv1.cancel()
pos_inv1.delete()
pos_inv2.reload()
pos_inv2.delete()
se.cancel()
batch.reload()
batch.cancel()
batch.delete()
def test_ignore_pricing_rule(self):
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
item_price = frappe.get_doc({
'doctype': 'Item Price',
'item_code': '_Test Item',
'price_list': '_Test Price List',
'price_list_rate': '450',
})
item_price.insert()
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
pr.save()
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
# rate shouldn't change
self.assertEquals(pos_inv.items[0].rate, 405)
pos_inv.ignore_pricing_rule = 1
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
# rate should change since pricing rules are ignored
self.assertEquals(pos_inv.items[0].rate, 300)
item_price.delete()
pos_inv.delete()
pr.delete()
def create_pos_invoice(**args):
args = frappe._dict(args)
pos_profile = None
@@ -557,7 +623,8 @@ def create_pos_invoice(**args):
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no
"serial_no": args.serial_no,
"batch_no": args.batch_no
})
if not args.do_not_save:
@@ -570,3 +637,8 @@ def create_pos_invoice(**args):
pos_inv.payment_schedule = []
return pos_inv
def make_batch_item(item_name):
from erpnext.stock.doctype.item.test_item import make_item
if not frappe.db.exists(item_name):
return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1))

View File

@@ -630,6 +630,26 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]:
doc.delete()
def test_multiple_pricing_rules_with_min_qty(self):
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
item = si.items[0]
item.stock_qty = 1
si.save()
self.assertFalse(item.discount_percentage)
item.qty = 5
item.stock_qty = 5
si.save()
self.assertEqual(item.discount_percentage, 30)
si.delete()
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
test_dependencies = ["Campaign"]
def make_pricing_rule(**args):
@@ -652,7 +672,7 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"priority": 1,
"priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
@@ -678,6 +698,8 @@ def make_pricing_rule(**args):
if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for))
return doc
def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({

View File

@@ -73,7 +73,7 @@ def sorted_by_priority(pricing_rules, args, doc=None):
for key in sorted(pricing_rule_dict):
pricing_rules_list.extend(pricing_rule_dict.get(key))
return pricing_rules_list or pricing_rules
return pricing_rules_list
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
filtered_pricing_rules = []

View File

@@ -503,11 +503,11 @@ class PurchaseInvoice(BuyingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Did not use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
@@ -515,8 +515,8 @@ class PurchaseInvoice(BuyingController):
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
@@ -543,6 +543,11 @@ class PurchaseInvoice(BuyingController):
if d.category in ('Valuation', 'Total and Valuation')
and flt(d.base_tax_amount_after_discount_amount)]
provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company,
'enable_provisional_accounting_for_non_stock_items'))
purchase_receipt_doc_map = {}
for item in self.get("items"):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
@@ -637,19 +642,23 @@ class PurchaseInvoice(BuyingController):
else:
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
if auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
provisional_account = self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
'account':service_received_but_not_billed_account}, ['name'])
'account':provisional_account}, ['name'])
if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)
if not self.is_internal_transfer():
gl_entries.append(self.get_gl_dict({

View File

@@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice_from_receipt,
)
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_taxes,
make_purchase_receipt,
@@ -963,7 +968,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
pi.set_posting_time = 1
pi.posting_date = '2019-03-15'
pi.posting_date = '2019-01-10'
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2019-01-10"
pi.items[0].service_end_date = "2019-03-15"
@@ -1124,8 +1129,6 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_purchase_invoice_advance_taxes(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
# create a new supplier to test
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
@@ -1198,6 +1201,45 @@ class TestPurchaseInvoice(unittest.TestCase):
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
item = create_item("_Test Non Stock Item", is_stock_item=0)
provisional_account = create_account(account_name="Provision Account",
parent_account="Current Liabilities - _TC", company="_Test Company")
company = frappe.get_doc('Company', '_Test Company')
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
pi.save()
pi.submit()
# Check GLE for Purchase Invoice
expected_gle = [
['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 250, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
@@ -1213,7 +1255,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
def update_tax_witholding_category(company, account):
from erpnext.accounts.utils import get_fiscal_year
fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021')
fiscal_year = get_fiscal_year(date=nowdate())
if not frappe.db.get_value('Tax Withholding Rate',
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),

View File

@@ -651,7 +651,7 @@
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
"permlevel": 1,
"permlevel": 0,
"print_hide": 1
},
{
@@ -2038,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-10-21 20:19:38.667508",
"modified": "2021-12-23 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -45,6 +45,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -745,8 +746,11 @@ class SalesInvoice(SellingController):
def update_packing_list(self):
if cint(self.update_stock) == 1:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
if cint(self.is_return) and self.return_against:
calculate_mapped_packed_items_return(self)
else:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
else:
self.set('packed_items', [])
@@ -879,11 +883,11 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.debit_to,
@@ -891,8 +895,8 @@ class SalesInvoice(SellingController):
"party": self.customer,
"due_date": self.due_date,
"against": self.against_income_account,
"debit": grand_total_in_company_currency,
"debit_in_account_currency": grand_total_in_company_currency \
"debit": base_grand_total,
"debit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,

View File

@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.regional.india.utils import get_ewb_data
@@ -1800,47 +1801,6 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
frappe.set_user("Administrator")
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
self.assertRaises(frappe.ValidationError, pda1.submit)
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
@@ -2217,9 +2177,9 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48],
["2021-06-30", 20000.0, 21311.48],
["2021-09-30", 5041.1, 26352.58]
["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21366.12],
["2021-09-30", 5041.1, 26407.22]
]
for i, schedule in enumerate(asset.schedules):
@@ -2267,12 +2227,12 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48, True],
["2021-06-30", 20000.0, 21311.48, True],
["2022-06-30", 20000.0, 41311.48, False],
["2023-06-30", 20000.0, 61311.48, False],
["2024-06-30", 20000.0, 81311.48, False],
["2025-06-06", 18688.52, 100000.0, False]
["2020-06-30", 1366.12, 1366.12, True],
["2021-06-30", 20000.0, 21366.12, True],
["2022-06-30", 20000.0, 41366.12, False],
["2023-06-30", 20000.0, 61366.12, False],
["2024-06-30", 20000.0, 81366.12, False],
["2025-06-06", 18633.88, 100000.0, False]
]
for i, schedule in enumerate(asset.schedules):
@@ -2370,6 +2330,41 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
def test_update_invoice_status(self):
today = nowdate()
# Sales Invoice without Payment Schedule
si = create_sales_invoice(posting_date=add_days(today, -5))
# Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
si_with_payment_schedule.extend("payment_schedule", [
{
"due_date": add_days(today, -5),
"invoice_portion": 50,
"payment_amount": si_with_payment_schedule.grand_total / 2
},
{
"due_date": add_days(today, 5),
"invoice_portion": 50,
"payment_amount": si_with_payment_schedule.grand_total / 2
}
])
si_with_payment_schedule.submit()
for invoice in (si, si_with_payment_schedule):
invoice.db_set("status", "Unpaid")
update_invoice_status()
invoice.reload()
self.assertEqual(invoice.status, "Overdue")
invoice.db_set("status", "Unpaid and Discounted")
update_invoice_status()
invoice.reload()
self.assertEqual(invoice.status, "Overdue and Discounted")
def test_sales_commission(self):
si = frappe.copy_doc(test_records[0])
item = copy.deepcopy(si.get('items')[0])
@@ -2431,6 +2426,74 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
def test_multi_currency_deferred_revenue_via_journal_entry(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
item.deferred_revenue_account = deferred_account
item.save()
si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
si.set_posting_time = 1
si.posting_date = '2019-01-01'
si.debit_to = '_Test Receivable USD - _TC'
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-01"
si.items[0].service_end_date = "2019-03-30"
si.items[0].deferred_expense_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
pda1.submit()
expected_gle = [
["Sales - _TC", 0.0, 2089.89, "2019-01-28"],
[deferred_account, 2089.89, 0.0, "2019-01-28"],
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
[deferred_account, 1887.64, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
[deferred_account, 2022.47, 0.0, "2019-03-15"]
]
gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.credit)
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'

View File

@@ -71,7 +71,8 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
if shipping_amount:
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"):

View File

@@ -24,6 +24,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account_currency
class Subscription(Document):
@@ -356,7 +357,10 @@ class Subscription(Document):
if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
invoice.apply_tds = 1
## Add dimensions in invoice for subscription:
# Add party currency to invoice
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
# Add dimensions in invoice for subscription:
accounting_dimensions = get_accounting_dimensions()
for dimension in accounting_dimensions:

View File

@@ -60,15 +60,38 @@ def create_plan():
plan.billing_interval_count = 3
plan.insert()
if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'):
plan = frappe.new_doc('Subscription Plan')
plan.plan_name = '_Test Plan Multicurrency'
plan.item = '_Test Non Stock Item'
plan.price_determination = "Fixed Rate"
plan.cost = 50
plan.currency = 'USD'
plan.billing_interval = 'Month'
plan.billing_interval_count = 1
plan.insert()
def create_parties():
if not frappe.db.exists('Supplier', '_Test Supplier'):
supplier = frappe.new_doc('Supplier')
supplier.supplier_name = '_Test Supplier'
supplier.supplier_group = 'All Supplier Groups'
supplier.insert()
if not frappe.db.exists('Customer', '_Test Subscription Customer'):
customer = frappe.new_doc('Customer')
customer.customer_name = '_Test Subscription Customer'
customer.billing_currency = 'USD'
customer.append('accounts', {
'company': '_Test Company',
'account': '_Test Receivable USD - _TC'
})
customer.insert()
class TestSubscription(unittest.TestCase):
def setUp(self):
create_plan()
create_parties()
def test_create_subscription_with_trial_with_correct_period(self):
subscription = frappe.new_doc('Subscription')
@@ -637,3 +660,22 @@ class TestSubscription(unittest.TestCase):
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
def test_multicurrency_subscription(self):
subscription = frappe.new_doc('Subscription')
subscription.party_type = 'Customer'
subscription.party = '_Test Subscription Customer'
subscription.generate_invoice_at_period_start = 1
subscription.company = '_Test Company'
# select subscription start date as '2018-01-15'
subscription.start_date = '2018-01-01'
subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1})
subscription.save()
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, 'Unpaid')
# Check the currency of the created invoice
currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency')
self.assertEqual(currency, 'USD')

View File

@@ -75,7 +75,8 @@
"fieldname": "cost",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Cost"
"label": "Cost",
"options": "currency"
},
{
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
@@ -147,7 +148,7 @@
}
],
"links": [],
"modified": "2021-08-13 10:53:44.205774",
"modified": "2021-12-10 15:24:15.794477",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Plan",

View File

@@ -28,14 +28,14 @@
{
"columns": 2,
"fieldname": "single_threshold",
"fieldtype": "Currency",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Single Transaction Threshold"
},
{
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Currency",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Cumulative Transaction Threshold"
},
@@ -59,7 +59,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-31 11:42:12.213977",
"modified": "2022-01-13 12:04:42.904263",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
@@ -68,5 +68,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -59,7 +59,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party)
currency = party.default_currency if party.get("default_currency") else get_company_currency(company)
currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
set_contact_details(party_details, party, party_type)

View File

@@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
{
"fieldname":"show_gl_balance",
"label": __("Show GL Balance"),
"fieldtype": "Check",
},
],
onload: function(report) {

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _, scrub
from frappe.utils import cint
from frappe.utils import cint, flt
from six import iteritems
from erpnext.accounts.party import get_partywise_advanced_payment_amount
@@ -37,6 +37,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {}
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date)
for party, party_dict in iteritems(self.party_total):
if party_dict.outstanding == 0:
continue
@@ -56,6 +59,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# but in summary report advance shown in separate column
row.paid -= row.advance
if self.filters.show_gl_balance:
row.gl_balance = gl_balance_map.get(party)
row.diff = flt(row.outstanding) - flt(row.gl_balance)
self.data.append(row)
def get_party_total(self, args):
@@ -115,6 +122,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(_(credit_debit_label), fieldname='credit_note')
self.add_column(_('Outstanding Amount'), fieldname='outstanding')
if self.filters.show_gl_balance:
self.add_column(_('GL Balance'), fieldname='gl_balance')
self.add_column(_('Difference'), fieldname='diff')
self.setup_ageing_columns()
if self.party_type == "Customer":
@@ -141,3 +152,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# Add column for total due amount
self.add_column(label="Total Amount Due", fieldname='total_due')
def get_gl_balance(report_date):
return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'],
filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1))

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
function get_filters() {
let filters = [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"filter_based_on",
"label": __("Filter Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"],
"reqd": 1,
on_change: function() {
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
frappe.query_report.refresh();
}
},
{
"fieldname":"period_start_date",
"label": __("Start Date"),
"fieldtype": "Date",
"hidden": 1,
"reqd": 1
},
{
"fieldname":"period_end_date",
"label": __("End Date"),
"fieldtype": "Date",
"hidden": 1,
"reqd": 1
},
{
"fieldname":"from_fiscal_year",
"label": __("Start Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
},
{
"fieldname":"to_fiscal_year",
"label": __("End Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
},
{
"fieldname": "periodicity",
"label": __("Periodicity"),
"fieldtype": "Select",
"options": [
{ "value": "Monthly", "label": __("Monthly") },
{ "value": "Quarterly", "label": __("Quarterly") },
{ "value": "Half-Yearly", "label": __("Half-Yearly") },
{ "value": "Yearly", "label": __("Yearly") }
],
"default": "Monthly",
"reqd": 1
},
{
"fieldname": "type",
"label": __("Invoice Type"),
"fieldtype": "Select",
"options": [
{ "value": "Revenue", "label": __("Revenue") },
{ "value": "Expense", "label": __("Expense") }
],
"default": "Revenue",
"reqd": 1
},
{
"fieldname" : "with_upcoming_postings",
"label": __("Show with upcoming revenue/expense"),
"fieldtype": "Check",
"default": 1
}
]
return filters;
}
frappe.query_reports["Deferred Revenue and Expense"] = {
"filters": get_filters(),
"formatter": function(value, row, column, data, default_formatter){
return default_formatter(value, row, column, data);
},
onload: function(report){
let fiscal_year = frappe.defaults.get_user_default("fiscal_year");
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
period_start_date: fy.year_start_date,
period_end_date: fy.year_end_date
});
});
}
};

View File

@@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-12-10 19:27:14.654220",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-12-10 19:27:14.654220",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Deferred Revenue and Expense",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Deferred Revenue and Expense",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
}

View File

@@ -0,0 +1,441 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE
import frappe
from frappe import _, qb
from frappe.query_builder import Column, functions
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded
from erpnext.accounts.report.financial_statements import get_period_list
class Deferred_Item(object):
"""
Helper class for processing items with deferred revenue/expense
"""
def __init__(self, item, inv, gle_entries):
self.name = item
self.parent = inv.name
self.item_name = gle_entries[0].item_name
self.service_start_date = gle_entries[0].service_start_date
self.service_end_date = gle_entries[0].service_end_date
self.base_net_amount = gle_entries[0].base_net_amount
self.filters = inv.filters
self.period_list = inv.period_list
if gle_entries[0].deferred_revenue_account:
self.type = "Deferred Sale Item"
self.deferred_account = gle_entries[0].deferred_revenue_account
elif gle_entries[0].deferred_expense_account:
self.type = "Deferred Purchase Item"
self.deferred_account = gle_entries[0].deferred_expense_account
self.gle_entries = []
# holds period wise total for item
self.period_total = []
self.last_entry_date = self.service_start_date
if gle_entries:
self.gle_entries = gle_entries
for x in self.gle_entries:
if self.get_amount(x):
self.last_entry_date = x.gle_posting_date
def report_data(self):
"""
Generate report data for output
"""
ret_data = frappe._dict({"name": self.item_name})
for period in self.period_total:
ret_data[period.key] = period.total
ret_data.indent = 1
return ret_data
def get_amount(self, entry):
"""
For a given GL/Journal posting, get balance based on item type
"""
if self.type == "Deferred Sale Item":
return entry.debit - entry.credit
elif self.type == "Deferred Purchase Item":
return -(entry.credit - entry.debit)
return 0
def get_item_total(self):
"""
Helper method - calculate booked amount. Includes simulated postings as well
"""
total = 0
for gle_posting in self.gle_entries:
total += self.get_amount(gle_posting)
return total
def calculate_amount(self, start_date, end_date):
"""
start_date, end_date - datetime.datetime.date
return - estimated amount to post for given period
Calculated based on already booked amount and item service period
"""
total_months = (
(self.service_end_date.year - self.service_start_date.year) * 12
+ (self.service_end_date.month - self.service_start_date.month)
+ 1
)
prorate = date_diff(self.service_end_date, self.service_start_date) / date_diff(
get_last_day(self.service_end_date), get_first_day(self.service_start_date)
)
actual_months = rounded(total_months * prorate, 1)
already_booked_amount = self.get_item_total()
base_amount = self.base_net_amount / actual_months
if base_amount + already_booked_amount > self.base_net_amount:
base_amount = self.base_net_amount - already_booked_amount
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
partial_month = flt(date_diff(end_date, start_date)) / flt(
date_diff(get_last_day(end_date), get_first_day(start_date))
)
base_amount *= rounded(partial_month, 1)
return base_amount
def make_dummy_gle(self, name, date, amount):
"""
return - frappe._dict() of a dummy gle entry
"""
entry = frappe._dict(
{"name": name, "gle_posting_date": date, "debit": 0, "credit": 0, "posted": "not"}
)
if self.type == "Deferred Sale Item":
entry.debit = amount
elif self.type == "Deferred Purchase Item":
entry.credit = amount
return entry
def simulate_future_posting(self):
"""
simulate future posting by creating dummy gl entries. starts from the last posting date.
"""
if self.service_start_date != self.service_end_date:
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
self.estimate_for_period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
add_days(self.last_entry_date, 1),
self.period_list[-1].to_date,
"Date Range",
"Monthly",
company=self.filters.company,
)
for period in self.estimate_for_period_list:
amount = self.calculate_amount(period.from_date, period.to_date)
gle = self.make_dummy_gle(period.key, period.to_date, amount)
self.gle_entries.append(gle)
def calculate_item_revenue_expense_for_period(self):
"""
calculate item postings for each period and update period_total list
"""
for period in self.period_list:
period_sum = 0
actual = 0
for posting in self.gle_entries:
# if period.from_date <= posting.posting_date <= period.to_date:
if period.from_date <= posting.gle_posting_date <= period.to_date:
period_sum += self.get_amount(posting)
if posting.posted == "posted":
actual += self.get_amount(posting)
self.period_total.append(
frappe._dict({"key": period.key, "total": period_sum, "actual": actual})
)
return self.period_total
class Deferred_Invoice(object):
def __init__(self, invoice, items, filters, period_list):
"""
Helper class for processing invoices with deferred revenue/expense items
invoice - string : invoice name
items - list : frappe._dict() with item details. Refer Deferred_Item for required fields
"""
self.name = invoice
self.posting_date = items[0].posting_date
self.filters = filters
self.period_list = period_list
# holds period wise total for invoice
self.period_total = []
if items[0].deferred_revenue_account:
self.type = "Sales"
elif items[0].deferred_expense_account:
self.type = "Purchase"
self.items = []
# for each uniq items
self.uniq_items = set([x.item for x in items])
for item in self.uniq_items:
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
def calculate_invoice_revenue_expense_for_period(self):
"""
calculate deferred revenue/expense for all items in invoice
"""
# initialize period_total list for invoice
for period in self.period_list:
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
for item in self.items:
item_total = item.calculate_item_revenue_expense_for_period()
# update invoice total
for idx, period in enumerate(self.period_list, 0):
self.period_total[idx].total += item_total[idx].total
self.period_total[idx].actual += item_total[idx].actual
return self.period_total
def estimate_future(self):
"""
create dummy GL entries for upcoming months for all items in invoice
"""
[item.simulate_future_posting() for item in self.items]
def report_data(self):
"""
generate report data for invoice, includes invoice total
"""
ret_data = []
inv_total = frappe._dict({"name": self.name})
for x in self.period_total:
inv_total[x.key] = x.total
inv_total.indent = 0
ret_data.append(inv_total)
list(map(lambda item: ret_data.append(item.report_data()), self.items))
return ret_data
class Deferred_Revenue_and_Expense_Report(object):
def __init__(self, filters=None):
"""
Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided
"""
# If no filters are provided, get user defaults
if not filters:
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
self.filters = frappe._dict(
{
"company": frappe.defaults.get_user_default("Company"),
"filter_based_on": "Fiscal Year",
"period_start_date": fiscal_year.year_start_date,
"period_end_date": fiscal_year.year_end_date,
"from_fiscal_year": fiscal_year.year,
"to_fiscal_year": fiscal_year.year,
"periodicity": "Monthly",
"type": "Revenue",
"with_upcoming_postings": True,
}
)
else:
self.filters = frappe._dict(filters)
self.period_list = None
self.deferred_invoices = []
# holds period wise total for report
self.period_total = []
def get_period_list(self):
"""
Figure out selected period based on filters
"""
self.period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
self.filters.period_start_date,
self.filters.period_end_date,
self.filters.filter_based_on,
self.filters.periodicity,
company=self.filters.company,
)
def get_invoices(self):
"""
Get all sales and purchase invoices which has deferred revenue/expense items
"""
gle = qb.DocType("GL Entry")
# column doesn't have an alias option
posted = Column("posted")
if self.filters.type == "Revenue":
inv = qb.DocType("Sales Invoice")
inv_item = qb.DocType("Sales Invoice Item")
deferred_flag_field = inv_item["enable_deferred_revenue"]
deferred_account_field = inv_item["deferred_revenue_account"]
elif self.filters.type == "Expense":
inv = qb.DocType("Purchase Invoice")
inv_item = qb.DocType("Purchase Invoice Item")
deferred_flag_field = inv_item["enable_deferred_expense"]
deferred_account_field = inv_item["deferred_expense_account"]
query = (
qb.from_(inv_item)
.join(inv)
.on(inv.name == inv_item.parent)
.join(gle)
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
.select(
inv.name.as_("doc"),
inv.posting_date,
inv_item.name.as_("item"),
inv_item.item_name,
inv_item.service_start_date,
inv_item.service_end_date,
inv_item.base_net_amount,
deferred_account_field,
gle.posting_date.as_("gle_posting_date"),
functions.Sum(gle.debit).as_("debit"),
functions.Sum(gle.credit).as_("credit"),
posted,
)
.where(
(inv.docstatus == 1)
& (deferred_flag_field == 1)
& (
(
(self.period_list[0].from_date >= inv_item.service_start_date)
& (inv_item.service_end_date >= self.period_list[0].from_date)
)
| (
(inv_item.service_start_date >= self.period_list[0].from_date)
& (inv_item.service_start_date <= self.period_list[-1].to_date)
)
)
)
.groupby(inv.name, inv_item.name, gle.posting_date)
.orderby(gle.posting_date)
)
self.invoices = query.run(as_dict=True)
uniq_invoice = set([x.doc for x in self.invoices])
for inv in uniq_invoice:
self.deferred_invoices.append(
Deferred_Invoice(
inv, [x for x in self.invoices if x.doc == inv], self.filters, self.period_list
)
)
def estimate_future(self):
"""
For all Invoices estimate upcoming postings
"""
for x in self.deferred_invoices:
x.estimate_future()
def calculate_revenue_and_expense(self):
"""
calculate the deferred revenue/expense for all invoices
"""
# initialize period_total list for report
for period in self.period_list:
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
for inv in self.deferred_invoices:
inv_total = inv.calculate_invoice_revenue_expense_for_period()
# calculate total for whole report
for idx, period in enumerate(self.period_list, 0):
self.period_total[idx].total += inv_total[idx].total
self.period_total[idx].actual += inv_total[idx].actual
def get_columns(self):
columns = []
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
for period in self.period_list:
columns.append(
{
"label": _(period.label),
"fieldname": period.key,
"fieldtype": "Currency",
"read_only": 1,
})
return columns
def generate_report_data(self):
"""
Generate report data for all invoices. Adds total rows for revenue and expense
"""
ret = []
for inv in self.deferred_invoices:
ret += inv.report_data()
# empty row for padding
ret += [{}]
# add total row
if ret is not []:
if self.filters.type == "Revenue":
total_row = frappe._dict({"name": "Total Deferred Income"})
elif self.filters.type == "Expense":
total_row = frappe._dict({"name": "Total Deferred Expense"})
for idx, period in enumerate(self.period_list, 0):
total_row[period.key] = self.period_total[idx].total
ret.append(total_row)
return ret
def prepare_chart(self):
chart = {
"data": {
"labels": [period.label for period in self.period_list],
"datasets": [
{
"name": "Actual Posting",
"chartType": "bar",
"values": [x.actual for x in self.period_total],
}
],
},
"type": "axis-mixed",
"height": 500,
"axisOptions": {"xAxisMode": "Tick", "xIsSeries": True},
"barOptions": {"stacked": False, "spaceRatio": 0.5},
}
if self.filters.with_upcoming_postings:
chart["data"]["datasets"].append({
"name": "Expected",
"chartType": "line",
"values": [x.total for x in self.period_total]
})
return chart
def run(self, *args, **kwargs):
"""
Run report and generate data
"""
self.deferred_invoices.clear()
self.get_period_list()
self.get_invoices()
if self.filters.with_upcoming_postings:
self.estimate_future()
self.calculate_revenue_and_expense()
def execute(filters=None):
report = Deferred_Revenue_and_Expense_Report(filters=filters)
report.run()
columns = report.get_columns()
data = report.generate_report_data()
message = []
chart = report.prepare_chart()
return columns, data, message, chart

View File

@@ -0,0 +1,345 @@
import unittest
import frappe
from frappe import qb
from frappe.utils import nowdate
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
Deferred_Revenue_and_Expense_Report,
)
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.stock.doctype.item.test_item import create_item
class TestDeferredRevenueAndExpense(unittest.TestCase):
@classmethod
def setUpClass(self):
clear_accounts_and_items()
create_company()
self.maxDiff = None
def clear_old_entries(self):
sinv = qb.DocType("Sales Invoice")
sinv_item = qb.DocType("Sales Invoice Item")
pinv = qb.DocType("Purchase Invoice")
pinv_item = qb.DocType("Purchase Invoice Item")
# delete existing invoices with deferred items
deferred_invoices = (
qb.from_(sinv)
.join(sinv_item)
.on(sinv.name == sinv_item.parent)
.select(sinv.name)
.where(sinv_item.enable_deferred_revenue == 1)
.run()
)
if deferred_invoices:
qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
deferred_invoices = (
qb.from_(pinv)
.join(pinv_item)
.on(pinv.name == pinv_item.parent)
.select(pinv.name)
.where(pinv_item.enable_deferred_expense == 1)
.run()
)
if deferred_invoices:
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
def test_deferred_revenue(self):
self.clear_old_entries()
# created deferred expense accounts, if not found
deferred_revenue_account = create_account(
account_name="Deferred Revenue",
parent_account="Current Liabilities - _CD",
company="_Test Company DR",
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Customer DR"
customer.type = "Individual"
customer.insert()
item = create_item(
"_Test Internet Subscription",
is_stock_item=0,
warehouse="All Warehouses - _CD",
company="_Test Company DR",
)
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_revenue_account
item.no_of_months = 3
item.save()
si = create_sales_invoice(
item=item.name,
company="_Test Company DR",
customer="_Test Customer DR",
debit_to="Debtors - _CD",
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
do_not_submit=True,
rate=300,
price_list_rate=300,
)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2021-05-01"
si.items[0].service_end_date = "2021-08-01"
si.items[0].deferred_revenue_account = deferred_revenue_account
si.items[0].income_account = "Sales - _CD"
si.save()
si.submit()
pda = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
posting_date=nowdate(),
start_date="2021-05-01",
end_date="2021-08-01",
type="Income",
company="_Test Company DR",
)
)
pda.insert()
pda.submit()
# execute report
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
self.filters = frappe._dict(
{
"company": frappe.defaults.get_user_default("Company"),
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
"from_fiscal_year": fiscal_year.year,
"to_fiscal_year": fiscal_year.year,
"periodicity": "Monthly",
"type": "Revenue",
"with_upcoming_postings": False,
}
)
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
report.run()
expected = [
{"key": "may_2021", "total": 100.0, "actual": 100.0},
{"key": "jun_2021", "total": 100.0, "actual": 100.0},
{"key": "jul_2021", "total": 100.0, "actual": 100.0},
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
def test_deferred_expense(self):
self.clear_old_entries()
# created deferred expense accounts, if not found
deferred_expense_account = create_account(
account_name="Deferred Expense",
parent_account="Current Assets - _CD",
company="_Test Company DR",
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
supplier = create_supplier(
supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
)
supplier.save()
item = create_item(
"_Test Office Desk",
is_stock_item=0,
warehouse="All Warehouses - _CD",
company="_Test Company DR",
)
item.enable_deferred_expense = 1
item.deferred_expense_account = deferred_expense_account
item.no_of_months_exp = 3
item.save()
pi = make_purchase_invoice(
item=item.name,
company="_Test Company DR",
supplier="_Test Furniture Supplier",
is_return=False,
update_stock=False,
posting_date=frappe.utils.datetime.date(2021, 5, 1),
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
do_not_save=True,
rate=300,
price_list_rate=300,
warehouse="All Warehouses - _CD",
qty=1,
)
pi.set_posting_time = True
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2021-05-01"
pi.items[0].service_end_date = "2021-08-01"
pi.items[0].deferred_expense_account = deferred_expense_account
pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
pi.save()
pi.submit()
pda = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
posting_date=nowdate(),
start_date="2021-05-01",
end_date="2021-08-01",
type="Expense",
company="_Test Company DR",
)
)
pda.insert()
pda.submit()
# execute report
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
self.filters = frappe._dict(
{
"company": frappe.defaults.get_user_default("Company"),
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
"from_fiscal_year": fiscal_year.year,
"to_fiscal_year": fiscal_year.year,
"periodicity": "Monthly",
"type": "Expense",
"with_upcoming_postings": False,
}
)
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
report.run()
expected = [
{"key": "may_2021", "total": -100.0, "actual": -100.0},
{"key": "jun_2021", "total": -100.0, "actual": -100.0},
{"key": "jul_2021", "total": -100.0, "actual": -100.0},
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
def test_zero_months(self):
self.clear_old_entries()
# created deferred expense accounts, if not found
deferred_revenue_account = create_account(
account_name="Deferred Revenue",
parent_account="Current Liabilities - _CD",
company="_Test Company DR",
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Customer DR"
customer.type = "Individual"
customer.insert()
item = create_item(
"_Test Internet Subscription",
is_stock_item=0,
warehouse="All Warehouses - _CD",
company="_Test Company DR",
)
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_revenue_account
item.no_of_months = 0
item.save()
si = create_sales_invoice(
item=item.name,
company="_Test Company DR",
customer="_Test Customer DR",
debit_to="Debtors - _CD",
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
do_not_submit=True,
rate=300,
price_list_rate=300,
)
si.items[0].enable_deferred_revenue = 1
si.items[0].deferred_revenue_account = deferred_revenue_account
si.items[0].income_account = "Sales - _CD"
si.save()
si.submit()
pda = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
posting_date=nowdate(),
start_date="2021-05-01",
end_date="2021-08-01",
type="Income",
company="_Test Company DR",
)
)
pda.insert()
pda.submit()
# execute report
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
self.filters = frappe._dict(
{
"company": frappe.defaults.get_user_default("Company"),
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
"from_fiscal_year": fiscal_year.year,
"to_fiscal_year": fiscal_year.year,
"periodicity": "Monthly",
"type": "Revenue",
"with_upcoming_postings": False,
}
)
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
report.run()
expected = [
{"key": "may_2021", "total": 300.0, "actual": 300.0},
{"key": "jun_2021", "total": 0, "actual": 0},
{"key": "jul_2021", "total": 0, "actual": 0},
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
def create_company():
company = frappe.db.exists("Company", "_Test Company DR")
if not company:
company = frappe.new_doc("Company")
company.company_name = "_Test Company DR"
company.default_currency = "INR"
company.chart_of_accounts = "Standard"
company.insert()
def clear_accounts_and_items():
item = qb.DocType("Item")
account = qb.DocType("Account")
customer = qb.DocType("Customer")
supplier = qb.DocType("Supplier")
qb.from_(account).delete().where(
(account.account_name == "Deferred Revenue")
| (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
).run()
qb.from_(item).delete().where(
(item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
).run()
qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()

View File

@@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "include_dimensions",
"label": __("Consider Accounting Dimensions"),
"fieldtype": "Check",
"default": 0
"default": 1
},
{
"fieldname": "show_opening_entries",

View File

@@ -449,9 +449,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
key = tuple(keylist)
if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle)
@@ -595,14 +597,14 @@ def get_columns(filters):
"fieldname": dim.fieldname,
"width": 100
})
columns.extend([
{
columns.append({
"label": _("Cost Center"),
"options": "Cost Center",
"fieldname": "cost_center",
"width": 100
},
})
columns.extend([
{
"label": _("Against Voucher Type"),
"fieldname": "against_voucher_type",

View File

@@ -186,83 +186,85 @@ class Asset(AccountsController):
if not self.available_for_use_date:
return
for d in self.get('finance_books'):
self.validate_asset_finance_books(d)
start = self.clear_depreciation_schedule()
start = self.clear_depreciation_schedule()
for finance_book in self.get('finance_books'):
self.validate_asset_finance_books(finance_book)
# value_after_depreciation - current Asset value
if self.docstatus == 1 and d.value_after_depreciation:
value_after_depreciation = flt(d.value_after_depreciation)
if self.docstatus == 1 and finance_book.value_after_depreciation:
value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked)
has_pro_rata = self.check_is_pro_rata(d)
has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
for n in range(start, number_of_pending_depreciations):
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation))
schedule_date = add_months(finance_book.depreciation_start_date,
n * cint(finance_book.frequency_of_depreciation))
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
# if asset is being sold
if date_of_sale:
from_date = self.get_from_date(d.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, date_of_sale)
if depreciation_amount > 0:
self.append("schedules", {
"schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
break
# For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date)
from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, finance_book.depreciation_start_date)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(d,
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, d.finance_book)
depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
@@ -273,10 +275,10 @@ class Asset(AccountsController):
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != d.expected_value_after_useful_life)
or value_after_depreciation < d.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0:
@@ -286,7 +288,7 @@ class Asset(AccountsController):
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else d.frequency_of_depreciation
else finance_book.frequency_of_depreciation
for r in range(month_range):
if (has_pro_rata and n == 0):
@@ -312,27 +314,52 @@ class Asset(AccountsController):
self.append("schedules", {
"schedule_date": date,
"depreciation_amount": amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
else:
self.append("schedules", {
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
# used when depreciation schedule needs to be modified due to increase in asset life
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
def clear_depreciation_schedule(self):
start = 0
for n in range(len(self.schedules)):
if not self.schedules[n].journal_entry:
del self.schedules[n:]
start = n
break
start = []
num_of_depreciations_completed = 0
depr_schedule = []
for schedule in self.get('schedules'):
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to ensure that start will only be updated once for each FB
if len(start) == (int(schedule.finance_book_id) - 1):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
if len(start) == (len(self.finance_books) - 1):
start.append(num_of_depreciations_completed)
# when the Depreciation Schedule is being created for the first time
if start == []:
start = [0] * len(self.finance_books)
else:
self.schedules = depr_schedule
return start
def get_from_date(self, finance_book):
@@ -349,7 +376,9 @@ class Asset(AccountsController):
if from_date:
return from_date
return self.available_for_use_date
# since depr for available_for_use_date is not yet booked
return add_days(self.available_for_use_date, -1)
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
@@ -583,7 +612,17 @@ class Asset(AccountsController):
return purchase_document
def get_fixed_asset_account(self):
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
if not fixed_asset_account:
frappe.throw(
_("Set {0} in asset category {1} for company {2}").format(
frappe.bold("Fixed Asset Account"),
frappe.bold(self.asset_category),
frappe.bold(self.company),
),
title=_("Account not Found"),
)
return fixed_asset_account
def get_cwip_account(self, cwip_enabled=False):
cwip_account = None

View File

@@ -207,9 +207,9 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = (
("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
("Debtors - _TC", 25000.0, 0.0)
)
@@ -491,10 +491,10 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2030-12-31", 27534.25, 27534.25],
["2031-12-31", 30000.0, 57534.25],
["2032-12-31", 30000.0, 87534.25],
["2033-01-30", 2465.75, 90000.0]
['2030-12-31', 27616.44, 27616.44],
['2031-12-31', 30000.0, 57616.44],
['2032-12-31', 30000.0, 87616.44],
['2033-01-30', 2383.56, 90000.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -544,10 +544,10 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 28493.15, 28493.15],
["2031-12-31", 35753.43, 64246.58],
["2032-12-31", 17876.71, 82123.29],
["2033-06-06", 5376.71, 87500.0]
['2030-12-31', 28630.14, 28630.14],
['2031-12-31', 35684.93, 64315.07],
['2032-12-31', 17842.47, 82157.54],
['2033-06-06', 5342.46, 87500.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -580,10 +580,10 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 11780.82, 11780.82],
["2031-12-31", 44109.59, 55890.41],
["2032-12-31", 22054.8, 77945.21],
["2033-07-12", 9554.79, 87500.0]
["2030-12-31", 11849.32, 11849.32],
["2031-12-31", 44075.34, 55924.66],
["2032-12-31", 22037.67, 77962.33],
["2033-07-12", 9537.67, 87500.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -642,7 +642,7 @@ class TestDepreciationBasics(AssetSetup):
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = getdate("2019-12-31"),
available_for_use_date = getdate("2020-01-01"),
total_number_of_depreciations = 3,
expected_value_after_useful_life = 10000,
depreciation_start_date = getdate("2020-07-01"),
@@ -653,7 +653,7 @@ class TestDepreciationBasics(AssetSetup):
["2020-07-01", 15000, 15000],
["2021-07-01", 30000, 45000],
["2022-07-01", 30000, 75000],
["2022-12-31", 15000, 90000]
["2023-01-01", 15000, 90000]
]
for i, schedule in enumerate(asset.schedules):
@@ -976,6 +976,82 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(len(asset.schedules), 1)
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.submit()
post_depreciation_entries(date="2020-04-01")
asset.load_from_db()
asset.clear_depreciation_schedule()
self.assertEqual(len(asset.schedules), 6)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, "1")
else:
self.assertEqual(schedule.finance_book_id, "2")
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.save()
self.assertEqual(len(asset.schedules), 9)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, 1)
else:
self.assertEqual(schedule.finance_book_id, 2)
def test_depreciation_entry_cancellation(self):
asset = create_asset(
item_code = "Macbook Pro",

View File

@@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{
dialog.show()
},
schedule_date(frm) {
if(frm.doc.schedule_date){
frm.doc.items.forEach((item) => {
item.schedule_date = frm.doc.schedule_date;
})
}
refresh_field("items");
},
preview: (frm) => {
let dialog = new frappe.ui.Dialog({
title: __('Preview Email'),
@@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{
dialog.show();
}
})
frappe.ui.form.on("Request for Quotation Item", {
items_add(frm, cdt, cdn) {
if (frm.doc.schedule_date) {
frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date);
}
}
});
frappe.ui.form.on("Request for Quotation Supplier",{
supplier: function(frm, cdt, cdn) {
var d = locals[cdt][cdn]

View File

@@ -12,6 +12,7 @@
"vendor",
"column_break1",
"transaction_date",
"schedule_date",
"status",
"amended_from",
"suppliers_section",
@@ -246,16 +247,22 @@
"fieldname": "sec_break_email_2",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "schedule_date",
"fieldtype": "Date",
"label": "Required Date"
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-05 22:04:29.017134",
"modified": "2021-11-24 17:47:49.909000",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@@ -0,0 +1,43 @@
# Version 13.18.0 Release Notes
### Features & Enhancements
- Deferred Revenue and Expense report with actual and upcoming postings ([#28822](https://github.com/frappe/erpnext/pull/28822))
- 'Invoice Number' field in Opening Invoice Creation Tool ([#29147](https://github.com/frappe/erpnext/pull/29147))
- Added required_date field to set date in child table ([#28432](https://github.com/frappe/erpnext/pull/28432))
### Fixes
- Enable ksa POS Invoice print format ([#28911](https://github.com/frappe/erpnext/pull/28911))
- Rename non existent doctype field to the right one ([#29055](https://github.com/frappe/erpnext/pull/29055))
- Mapped accounting dimensions for Bank Entry against Payroll Entry ([#29142](https://github.com/frappe/erpnext/pull/29142))
- Validate Finished Goods for independent Manufacture entries ([#28555](https://github.com/frappe/erpnext/pull/28555))
- Incorrect posting time fetching incorrect stock quantity in stock reconciliation ([#29103](https://github.com/frappe/erpnext/pull/29103))
- Stock Ageing Report - Negative Opening Stock ([#28966](https://github.com/frappe/erpnext/pull/28966))
- Can't change valuation_method on item ([#28876](https://github.com/frappe/erpnext/pull/28876))
- Optimize rate updation on changing price list ([#28953](https://github.com/frappe/erpnext/pull/28953))
- Added filter for dispatch address ([#28937](https://github.com/frappe/erpnext/pull/28937))
- Convert Item links to Website Item links in `Item Card Group` template data ([#28985](https://github.com/frappe/erpnext/pull/28985))
- Earned Leave allocation from Leave Policy Assignment ([#29163](https://github.com/frappe/erpnext/pull/29163))
- Items not mapped when trying to create a Maintenance Visit via Maintenance Schedule ([#28917](https://github.com/frappe/erpnext/pull/28917))
- For performance improvement, removed forcing of posting sort index on stock balance report ([#28902](https://github.com/frappe/erpnext/pull/28902))
- Future recurring period calculation ([#29083](https://github.com/frappe/erpnext/pull/29083))
- Nonstock items are showing in the Itemwise Recommended Reorder Level report ([#28873](https://github.com/frappe/erpnext/pull/28873))
- Incorrect amount based on payment days in timesheet salary slip ([#28845](https://github.com/frappe/erpnext/pull/28845))
- Currency fix for `cost` field in subscription plan ([#28821](https://github.com/frappe/erpnext/pull/28821))
- Fetch selling price with pricing rule ([#28951](https://github.com/frappe/erpnext/pull/28951))
- Filter out Claimed employee advances in Expense Claim ([#29046](https://github.com/frappe/erpnext/pull/29046))
- Tax and Charges template not getting fetched based on tax category assigned ([#29092](https://github.com/frappe/erpnext/pull/29092))
- Ignore links while setting default notification templates in Settings ([#29042](https://github.com/frappe/erpnext/pull/29042))
- Reset "Value After Depreciation" on reversing journal entry during Asset return ([#28975](https://github.com/frappe/erpnext/pull/28975))
- Multicurrency invoices using subscription ([#28916](https://github.com/frappe/erpnext/pull/28916))
- Fetch the appointment letter content in the same order as template ([#28968](https://github.com/frappe/erpnext/pull/28968))
- Incorrect serial no valuation report showing cancelled entries ([#29172](https://github.com/frappe/erpnext/pull/29172))
- Start date validation for deferred invoices ([#29009](https://github.com/frappe/erpnext/pull/29009))
- HSN-Wise summary report is incorrect if an invoice has same item code multiple times ([#28783](https://github.com/frappe/erpnext/pull/28783))
- Incorrect logic for the "Reserved Qty for Production" field in BIN ([#28880](https://github.com/frappe/erpnext/pull/28880))
- Issues in Bank Reconciliation tool ([#28996](https://github.com/frappe/erpnext/pull/28996))
- Hide Raw Material table in the Job Card if material transfer is against work order ([#28746](https://github.com/frappe/erpnext/pull/28746))
- Added "Is Reverse Charge" checkbox in Tax Category for Indian Companies ([#28935](https://github.com/frappe/erpnext/pull/28935))
- Updates in term loan processing ([#28034](https://github.com/frappe/erpnext/pull/28034))
- Incorrect bin qty on backdated reconciliation ([#28588](https://github.com/frappe/erpnext/pull/28588))

View File

@@ -0,0 +1,52 @@
## Version 13.19.0 Release Notes
### Features & Enhancements
- Allow user to change the parent company ([#28983](https://github.com/frappe/erpnext/pull/28983))
- Option to exclude holidays while marking monthly attendance ([#29185](https://github.com/frappe/erpnext/pull/29185))
- Early payment discount on sales & purchase orders ([#29101](https://github.com/frappe/erpnext/pull/29101))
### Fixes
- Filter query in bank reconciliation tool ([#29098](https://github.com/frappe/erpnext/pull/29098))
- Compute batch ledger in python ([#29324](https://github.com/frappe/erpnext/pull/29324))
- GL Entries for loan repayment via Salary ([#29169](https://github.com/frappe/erpnext/pull/29169))
- Group by Cost Center in General Ledger report only if include_dimensions is checked ([#28883](https://github.com/frappe/erpnext/pull/28883))
- Filter for leave period in Bulk Leave Policy Assignment ([#29272](https://github.com/frappe/erpnext/pull/29272))
- Update idx after updating items in so/po ([#29134](https://github.com/frappe/erpnext/pull/29134))
- Avoid resetting default warehouse fields for Manufacture Entry ([#29257](https://github.com/frappe/erpnext/pull/29257))
- Don't validate FG in repack entry ([#29271](https://github.com/frappe/erpnext/pull/29271))
- Map Accounting Dimensions for Bank Entry against Payroll Entry ([#29142](https://github.com/frappe/erpnext/pull/29142))
- Ignore cancelled SLEs ([#29303](https://github.com/frappe/erpnext/pull/29303))
- Show work order progress bar even it is closed ([#29312](https://github.com/frappe/erpnext/pull/29312))
- Incorrect serial no valuation report showing cancelled entries ([#29172](https://github.com/frappe/erpnext/pull/29172))
- Not able to make a reverse journal entry ([#29125](https://github.com/frappe/erpnext/pull/29125))
- Show ledger balance in Accounts Receivable and Payable summary ([#29135](https://github.com/frappe/erpnext/pull/29135))
- Add stock queue in SLE for FIFO valuation method ([#29302](https://github.com/frappe/erpnext/pull/29302))
- Threshold fields shows incorrect currency ([#29270](https://github.com/frappe/erpnext/pull/29270))
- Added patch to trim whitespace from the serial numbers ([#29306](https://github.com/frappe/erpnext/pull/29306))
- Task Depends on not removed from Gantt chart ([#28309](https://github.com/frappe/erpnext/pull/28309))
- Earned Leave allocation from Leave Policy Assignment ([#29163](https://github.com/frappe/erpnext/pull/29163))
- Exclude existing serial numbers while auto creating new serial numbers ([#29292](https://github.com/frappe/erpnext/pull/29292))
- Deferred revenue booking for multi currency invoices via Journal Entry ([#29115](https://github.com/frappe/erpnext/pull/29115))
- Fixed autoname generated for Job Applicant ([#29260](https://github.com/frappe/erpnext/pull/29260))
- Incorrect scrap item quantity calculated in the Manufacture type stock entry ([#29179](https://github.com/frappe/erpnext/pull/29179))
- Inconsistency in calculating outstanding amount ([#29176](https://github.com/frappe/erpnext/pull/29176))
- Accounts are coming from different company in the dropdown ([#29280](https://github.com/frappe/erpnext/pull/29280))
- Can't create debit note with zero quantity ([#28994](https://github.com/frappe/erpnext/pull/28994))
- "Update Cost" should ignore overridden routing times ([#29154](https://github.com/frappe/erpnext/pull/29154))
- Modifying Opening invoice creation tool timestamp ([#29127](https://github.com/frappe/erpnext/pull/29127))
- Future recurring period calculation ([#29083](https://github.com/frappe/erpnext/pull/29083))
- India localization: NIL Rated, Exempted and Non GST Invoices in GSTR-1 report ([#29208](https://github.com/frappe/erpnext/pull/29208))
- Purchase to Stock UOM conversion on Production Plan ([#28570](https://github.com/frappe/erpnext/pull/28570))
- Validation in POS for item batch no stock quantity ([#28907](https://github.com/frappe/erpnext/pull/28907))
- Shopping cart total quantity ([#29076](https://github.com/frappe/erpnext/pull/29076))
- POS items added to cart despite low quantity ([#29126](https://github.com/frappe/erpnext/pull/29126))
- Exclude unpublished items while fetching items from other item groups ([#29211](https://github.com/frappe/erpnext/pull/29211))
- Get project from PO into payment entry ([#29182](https://github.com/frappe/erpnext/pull/29182))
- Cover case when all material needs to be bought ([#29326](https://github.com/frappe/erpnext/pull/29326))
- Validate setup on clicking Mark Attendance button in Shift Type ([#29146](https://github.com/frappe/erpnext/pull/29146))
- Can't ignore pricing rule for one particular POS invoice ([#29222](https://github.com/frappe/erpnext/pull/29222))
- Cart & Popup Logic of Item variant without Website Item ([#29383](https://github.com/frappe/erpnext/pull/29383))
- Not able to submit salary slips from amended payroll entry. ([#29228](https://github.com/frappe/erpnext/pull/29228))
- Tax and Charges template not getting fetched based on tax category assigned ([#29092](https://github.com/frappe/erpnext/pull/29092))

View File

@@ -0,0 +1,32 @@
## Version 13.20.0 Release Notes
### Features & Enhancements
- Provisional accounting for expenses ([#29451](https://github.com/frappe/erpnext/pull/29451))
### Fixes
- Incorrect number of items fetched while creating delivery note ([#29454](https://github.com/frappe/erpnext/pull/29454))
- Incorrect raw materials quantity in manufacture stock entry ([#29419](https://github.com/frappe/erpnext/pull/29419))
- Refactored the update_serial_no function for old Maintenance Visits ([#28843](https://github.com/frappe/erpnext/pull/28843))
- Ignore empty customer/supplier in item query ([#29610](https://github.com/frappe/erpnext/pull/29610))
- The "Bypass Credit Limit Check" from customer has not fetched in the Customer Credit Balance report ([#29367](https://github.com/frappe/erpnext/pull/29367))
- Reset conversion facture after changing the Stock UOM ([#29062](https://github.com/frappe/erpnext/pull/29062))
- Cart Items rendering issue ([#29398](https://github.com/frappe/erpnext/pull/29398))
- Honour 'include holidays' setting while marking attendance for leave application ([#29425](https://github.com/frappe/erpnext/pull/29425))
- Cost of poor quality report time filters not working ([#28958](https://github.com/frappe/erpnext/pull/28958))
- Incorrect packing items getting fetched on Sales Return / Credit Note ([#28607](https://github.com/frappe/erpnext/pull/28607))
- Regenerate packing items on newly mapped doc ([#29642](https://github.com/frappe/erpnext/pull/29642))
- From Time and To Time not updated in drag and drop action for Course Schedule ([#29114](https://github.com/frappe/erpnext/pull/29114))
- Employee: set user image and validate user id only if user data is found ([#29452](https://github.com/frappe/erpnext/pull/29452))
- Clear Depreciation Schedule before modification ([#28507](https://github.com/frappe/erpnext/pull/28507))
- Fixed shopping cart qty badge ([#29077](https://github.com/frappe/erpnext/pull/29077))
- Fetch "transfer material against" from BOM ([#29435](https://github.com/frappe/erpnext/pull/29435))
- Cart & Popup Logic of Item variant without Website Item ([#29383](https://github.com/frappe/erpnext/pull/29383))
- Timesheets: calculate to time based on from time and hours ([#28589](https://github.com/frappe/erpnext/pull/28589))
- Dynamically compute BOM Level ([#29522](https://github.com/frappe/erpnext/pull/29522))
- Contact duplication on converting lead to customer ([#29337](https://github.com/frappe/erpnext/pull/29337))
- Fixed populate practitioner selected in form to check availability popup ([#29405](https://github.com/frappe/erpnext/pull/29405))
- Compute batch ledger in python ([#29324](https://github.com/frappe/erpnext/pull/29324))
- Incorrect packing list for recurring items & code cleanup ([#29456](https://github.com/frappe/erpnext/pull/29456))
- Opening invoice creation tool can fetch multiple accounting dimension ([#29407](https://github.com/frappe/erpnext/pull/29407))

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder.functions import Sum
from frappe.utils import (
add_days,
add_months,
@@ -113,7 +114,7 @@ class AccountsController(TransactionBase):
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
def validate(self):
if not self.get('is_return'):
if not self.get('is_return') and not self.get('is_debit_note'):
self.validate_qty_is_not_zero()
if self.get("_action") and self._action != "update_after_submit":
@@ -1690,58 +1691,69 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
def update_invoice_status():
"""Updates status as Overdue for applicable invoices. Runs daily."""
today = getdate()
payment_schedule = frappe.qb.DocType("Payment Schedule")
for doctype in ("Sales Invoice", "Purchase Invoice"):
frappe.db.sql("""
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
WHERE invoice.docstatus = 1
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
AND invoice.outstanding_amount > 0
AND (
{or_condition}
(
(
CASE
WHEN invoice.party_account_currency = invoice.currency
THEN (
CASE
WHEN invoice.disable_rounded_total
THEN invoice.grand_total
ELSE invoice.rounded_total
END
)
ELSE (
CASE
WHEN invoice.disable_rounded_total
THEN invoice.base_grand_total
ELSE invoice.base_rounded_total
END
)
END
) - invoice.outstanding_amount
) < (
SELECT SUM(
CASE
WHEN invoice.party_account_currency = invoice.currency
THEN ps.payment_amount
ELSE ps.base_payment_amount
END
)
FROM `tabPayment Schedule` ps
WHERE ps.parent = invoice.name
AND ps.due_date < %(today)s
)
)
""".format(
doctype=doctype,
or_condition=(
"invoice.is_pos AND invoice.due_date < %(today)s OR"
if doctype == "Sales Invoice"
else ""
)
), {"today": today}
invoice = frappe.qb.DocType(doctype)
consider_base_amount = invoice.party_account_currency != invoice.currency
payment_amount = (
frappe.qb.terms.Case()
.when(consider_base_amount, payment_schedule.base_payment_amount)
.else_(payment_schedule.payment_amount)
)
payable_amount = (
frappe.qb.from_(payment_schedule)
.select(Sum(payment_amount))
.where(
(payment_schedule.parent == invoice.name)
& (payment_schedule.due_date < today)
)
)
total = (
frappe.qb.terms.Case()
.when(invoice.disable_rounded_total, invoice.grand_total)
.else_(invoice.rounded_total)
)
base_total = (
frappe.qb.terms.Case()
.when(invoice.disable_rounded_total, invoice.base_grand_total)
.else_(invoice.base_rounded_total)
)
total_amount = (
frappe.qb.terms.Case()
.when(consider_base_amount, base_total)
.else_(total)
)
is_overdue = total_amount - invoice.outstanding_amount < payable_amount
conditions = (
(invoice.docstatus == 1)
& (invoice.outstanding_amount > 0)
& (
invoice.status.like("Unpaid%")
| invoice.status.like("Partly Paid%")
)
& (
((invoice.is_pos & invoice.due_date < today) | is_overdue)
if doctype == "Sales Invoice"
else is_overdue
)
)
status = (
frappe.qb.terms.Case()
.when(invoice.status.like("%Discounted"), "Overdue and Discounted")
.else_("Overdue")
)
frappe.qb.update(invoice).set("status", status).where(conditions).run()
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
if not terms_template:
@@ -2111,6 +2123,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_status_updater()
else:
parent.check_credit_limit()
# reset index of child table
for idx, row in enumerate(parent.get(child_docname), start=1):
row.idx = idx
parent.save()
if parent_doctype == 'Purchase Order':

View File

@@ -249,6 +249,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
del filters['customer']
else:
del filters['supplier']
else:
filters.pop('customer', None)
filters.pop('supplier', None)
description_cond = ''

View File

@@ -385,7 +385,7 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get('stock_qty') or d.get('actual_qty'))
if not d.incoming_rate:
if not (self.get("is_return") and d.incoming_rate):
d.incoming_rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": d.warehouse,

View File

@@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import (
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate
from erpnext.stock.stock_ledger import get_items_to_be_repost
class QualityInspectionRequiredError(frappe.ValidationError): pass
@@ -40,7 +40,10 @@ class StockController(AccountsController):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if cint(erpnext.is_perpetual_inventory_enabled(self.company)):
provisional_accounting_for_non_stock_items = \
cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))
if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items:
warehouse_account = get_warehouse_account_map(self.company)
if self.docstatus==1:
@@ -77,17 +80,17 @@ class StockController(AccountsController):
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
def clean_serial_nos(self):
from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
for row in self.get("items"):
if hasattr(row, "serial_no") and row.serial_no:
# replace commas by linefeed
row.serial_no = row.serial_no.replace(",", "\n")
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
# strip preceeding and succeeding spaces for each SN
# (SN could have valid spaces in between e.g. SN - 123 - 2021)
serial_no_list = row.serial_no.split("\n")
serial_no_list = [sn.strip() for sn in serial_no_list]
row.serial_no = "\n".join(serial_no_list)
for row in self.get('packed_items') or []:
if hasattr(row, "serial_no") and row.serial_no:
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None):
@@ -111,17 +114,6 @@ class StockController(AccountsController):
self.check_expense_account(item_row)
# If the item does not have the allow zero valuation rate flag set
# and ( valuation rate not mentioned in an incoming entry
# or incoming entry not found while delivering the item),
# try to pick valuation rate from previous sle or Item master and update in SLE
# Otherwise, throw an exception
if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \
and not item_row.get("allow_zero_valuation_rate"):
sle = self.update_stock_ledger_entries(sle)
# expense account/ target_warehouse / source_warehouse
if item_row.get('target_warehouse'):
warehouse = item_row.get('target_warehouse')
@@ -164,26 +156,6 @@ class StockController(AccountsController):
return frappe.flags.debit_field_precision
def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
self.doctype, self.name, currency=self.company_currency, company=self.company)
sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate)
sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate)
if sle.name:
frappe.db.sql("""
update
`tabStock Ledger Entry`
set
stock_value = %(stock_value)s,
valuation_rate = %(valuation_rate)s,
stock_value_difference = %(stock_value_difference)s
where
name = %(name)s""", (sle))
return sle
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
if self.doctype == "Stock Reconciliation":
reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose")
@@ -287,11 +259,7 @@ class StockController(AccountsController):
for d in self.items:
if not d.batch_no: continue
serial_nos = [sr.name for sr in frappe.get_all("Serial No",
{'batch_no': d.batch_no, 'status': 'Inactive'})]
if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None)
d.batch_no = None
d.db_set("batch_no", None)

View File

@@ -139,6 +139,8 @@ class calculate_taxes_and_totals(object):
if not item.qty and self.doc.get("is_return"):
item.amount = flt(-1 * item.rate, item.precision("amount"))
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
else:
item.amount = flt(item.rate * item.qty, item.precision("amount"))
@@ -594,13 +596,14 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
grand_total = self.doc.rounded_total or self.doc.grand_total
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
if self.doc.party_account_currency == self.doc.currency:
total_amount_to_pay = flt(grand_total - self.doc.total_advance
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
else:
total_amount_to_pay = flt(flt(grand_total *
self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance
- flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance
- flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total"))
self.doc.round_floats_in(self.doc, ["paid_amount"])
change_amount = 0

View File

@@ -1,6 +1,8 @@
import unittest
from functools import partial
import frappe
from erpnext.controllers import queries
@@ -54,6 +56,12 @@ class TestQueries(unittest.TestCase):
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
self.assertEqual(len(bundled_stock_items), 0)
# empty customer/supplier should be stripped of instead of failure
query(txt="", filters={"customer": None})
query(txt="", filters={"customer": ""})
query(txt="", filters={"supplier": None})
query(txt="", filters={"supplier": ""})
def test_bom_qury(self):
query = add_default_params(queries.bom, "BOM")
@@ -85,3 +93,6 @@ class TestQueries(unittest.TestCase):
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1)
def test_default_uoms(self):
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)

View File

@@ -4,19 +4,72 @@ import frappe
class TestUtils(unittest.TestCase):
def test_reset_default_field_value(self):
doc = frappe.get_doc({
"doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1",
})
def test_reset_default_field_value(self):
doc = frappe.get_doc({
"doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1",
})
# Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None)
# Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None)
def test_reset_default_field_value_in_mfg_stock_entry(self):
# manufacture stock entry with rows having blank source/target wh
se = frappe.get_doc(
doctype="Stock Entry",
purpose="Manufacture",
stock_entry_type="Manufacture",
company="_Test Company",
from_warehouse="_Test Warehouse - _TC",
to_warehouse="_Test Warehouse 1 - _TC",
items=[
frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1)
]
)
se.save()
# default fields must be untouched
self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC")
se.delete()
def test_reset_default_field_value_in_transfer_stock_entry(self):
doc = frappe.get_doc({
"doctype": "Stock Entry",
"purpose": "Material Receipt",
"from_warehouse": "Warehouse 1",
"to_warehouse": "Warehouse 2",
})
# Same values
doc.items = [
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
]
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.assertEqual(doc.from_warehouse, "Warehouse 1")
self.assertEqual(doc.to_warehouse, "Warehouse 2")
# Mixed values in source wh
doc.items = [
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
]
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.assertEqual(doc.from_warehouse, None)
self.assertEqual(doc.to_warehouse, "Warehouse 2")

View File

@@ -46,12 +46,26 @@ class TestWebsiteItem(unittest.TestCase):
]
})
elif self._testMethodName in WEBITEM_PRICE_TESTS:
create_user_and_customer_if_not_exists("test_contact_customer@example.com", "_Test Contact For _Test Customer")
create_regular_web_item()
make_web_item_price(item_code="Test Mobile Phone")
# Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass.
# This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor,
# when testing for logged-in user the test will get the previous pricing rule because "selling" is still true.
#
# I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test.
make_web_pricing_rule(
title="Test Pricing Rule for Test Mobile Phone",
item_code="Test Mobile Phone",
selling=1)
make_web_pricing_rule(
title="Test Pricing Rule for Test Mobile Phone (Customer)",
item_code="Test Mobile Phone",
selling=1,
discount_percentage="25",
applicable_for="Customer",
customer="_Test Customer")
def test_index_creation(self):
"Check if index is getting created in db."
@@ -188,22 +202,27 @@ class TestWebsiteItem(unittest.TestCase):
# price and pricing rule added via setUp
# login as customer with pricing rule
frappe.set_user("test_contact_customer@example.com")
# check if price and slashed price is fetched correctly
frappe.local.shopping_cart_settings = None
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertTrue(bool(data.product_info["price"]))
price_object = data.product_info["price"]
self.assertEqual(price_object.get("discount_percent"), 10)
self.assertEqual(price_object.get("price_list_rate"), 900)
self.assertEqual(price_object.get("discount_percent"), 25)
self.assertEqual(price_object.get("price_list_rate"), 750)
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
self.assertEqual(price_object.get("formatted_price"), "900.00")
self.assertEqual(price_object.get("formatted_discount_percent"), "10%")
self.assertEqual(price_object.get("formatted_price"), "750.00")
self.assertEqual(price_object.get("formatted_discount_percent"), "25%")
# disable show price
# switch to admin and disable show price
frappe.set_user("Administrator")
setup_e_commerce_settings({"show_price": 0})
# price should not be fetched
# price should not be fetched for logged in user.
frappe.set_user("test_contact_customer@example.com")
frappe.local.shopping_cart_settings = None
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertFalse(bool(data.product_info["price"]))
@@ -485,10 +504,34 @@ def make_web_pricing_rule(**kwargs):
"discount_percentage": kwargs.get("discount_percentage") or 10,
"company": kwargs.get("company") or "_Test Company",
"currency": kwargs.get("currency") or "INR",
"for_price_list": kwargs.get("price_list") or "_Test Price List India"
"for_price_list": kwargs.get("price_list") or "_Test Price List India",
"applicable_for": kwargs.get("applicable_for") or "",
"customer": kwargs.get("customer") or "",
})
pricing_rule.insert()
else:
pricing_rule = frappe.get_doc("Pricing Rule", {"title": title})
return pricing_rule
return pricing_rule
def create_user_and_customer_if_not_exists(email, first_name = None):
if frappe.db.exists("User", email):
return
frappe.get_doc({
"doctype": "User",
"user_type": "Website User",
"email": email,
"send_welcome_email": 0,
"first_name": first_name or email.split("@")[0]
}).insert(ignore_permissions=True)
contact = frappe.get_last_doc("Contact", filters={"email_id": email})
link = contact.append('links', {})
link.link_doctype = "Customer"
link.link_name = "_Test Customer"
link.link_title = "_Test Customer"
contact.save()
test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"]

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import itertools
import json
import frappe
@@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator):
context.body_class = "product-page"
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
self.attributes = frappe.get_all("Item Variant Attribute",
self.attributes = frappe.get_all(
"Item Variant Attribute",
fields=["attribute", "attribute_value"],
filters={"parent": self.item_code})
filters={"parent": self.item_code}
)
if self.slideshow:
context.update(get_slideshow(self))
self.set_variant_context(context)
self.set_attribute_context(context)
self.set_disabled_attributes(context)
self.set_metatags(context)
self.set_shopping_cart_data(context)
@@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator):
return context
def set_variant_context(self, context):
if not self.has_variants:
return
context.no_cache = True
variant = frappe.form_dict.variant
# load variants
# also used in set_attribute_context
context.variants = frappe.get_all(
"Item",
filters={
"variant_of": self.item_code,
"published_in_website": 1
},
order_by="name asc")
# the case when the item is opened for the first time from its list
if not variant and context.variants:
variant = context.variants[0]
if variant:
context.variant = frappe.get_doc("Item", variant)
fields = ("website_image", "website_image_alt", "web_long_description", "description",
"website_specifications")
for fieldname in fields:
if context.variant.get(fieldname):
value = context.variant.get(fieldname)
if isinstance(value, list):
value = [d.as_dict() for d in value]
context[fieldname] = value
if self.slideshow and context.variant and context.variant.slideshow:
context.update(get_slideshow(context.variant))
def set_attribute_context(self, context):
if not self.has_variants:
return
attribute_values_available = {}
context.attribute_values = {}
context.selected_attributes = {}
# load attributes
self.set_selected_attributes(context.variants, context, attribute_values_available)
# filter attributes, order based on attribute table
item = frappe.get_cached_doc("Item", self.item_code)
self.set_attribute_values(item.attributes, context, attribute_values_available)
context.variant_info = json.dumps(context.variants)
def set_selected_attributes(self, variants, context, attribute_values_available):
for variant in variants:
variant.attributes = frappe.get_all(
@@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator):
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
values.append(attr_value.attribute_value)
def set_disabled_attributes(self, context):
"""Disable selection options of attribute combinations that do not result in a variant"""
if not self.attributes or not self.has_variants:
return
context.disabled_attributes = {}
attributes = [attr.attribute for attr in self.attributes]
def find_variant(combination):
for variant in context.variants:
if len(variant.attributes) < len(attributes):
continue
if "combination" not in variant:
ref_combination = []
for attr in variant.attributes:
idx = attributes.index(attr.attribute)
ref_combination.insert(idx, attr.attribute_value)
variant["combination"] = ref_combination
if not (set(combination) - set(variant["combination"])):
# check if the combination is a subset of a variant combination
# eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
return True
for i, attr in enumerate(self.attributes):
if i == 0:
continue
combination_source = []
# loop through previous attributes
for prev_attr in self.attributes[:i]:
combination_source.append([context.selected_attributes.get(prev_attr.attribute)])
combination_source.append(context.attribute_values[attr.attribute])
for combination in itertools.product(*combination_source):
if not find_variant(combination):
context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
def set_metatags(self, context):
context.metatags = frappe._dict({})

View File

@@ -197,7 +197,10 @@ class ProductQuery:
website_item_groups = frappe.db.get_all(
"Website Item",
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
filters=[["Website Item Group", "item_group", "=", item_group]]
filters=[
["Website Item Group", "item_group", "=", item_group],
["published", "=", 1]
]
)
return website_item_groups

View File

@@ -24,7 +24,7 @@ def set_cart_count(quotation=None):
if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
if not quotation:
quotation = _get_cart_quotation()
cart_count = cstr(len(quotation.get("items")))
cart_count = cstr(cint(quotation.get("total_qty")))
if hasattr(frappe.local, "cookie_manager"):
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
@@ -276,10 +276,29 @@ def guess_territory():
def decorate_quotation_doc(doc):
for d in doc.get("items", []):
item_code = d.item_code
fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
# Variant Item
if not frappe.db.exists("Website Item", {"item_code": item_code}):
variant_data = frappe.db.get_values(
"Item",
filters={"item_code": item_code},
fieldname=["variant_of", "item_name", "image"],
as_dict=True
)[0]
item_code = variant_data.variant_of
fields = fields[1:]
d.web_item_name = variant_data.item_name
if variant_data.image: # get image from variant or template web item
d.thumbnail = variant_data.image
fields = fields[2:]
d.update(frappe.db.get_value(
"Website Item",
{"item_code": d.item_code},
["web_item_name", "thumbnail", "website_image", "description", "route"],
{"item_code": item_code},
fields,
as_dict=True)
)

View File

@@ -9,8 +9,13 @@ from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
from erpnext.tests.utils import create_test_contact_and_address
from erpnext.e_commerce.shopping_cart.cart import (
_get_cart_quotation,
get_cart_quotation,
get_party,
update_cart,
)
from erpnext.tests.utils import change_settings, create_test_contact_and_address
class TestShoppingCart(unittest.TestCase):
@@ -34,6 +39,7 @@ class TestShoppingCart(unittest.TestCase):
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator")
self.disable_shopping_cart()
@@ -128,6 +134,43 @@ class TestShoppingCart(unittest.TestCase):
self.remove_test_quotation(quotation)
@change_settings("E Commerce Settings",{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
def test_add_item_variant_without_web_item_to_cart(self):
"Test adding Variants having no Website Items in cart via Template Web Item."
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.stock.doctype.item.test_item import make_item
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small", "Test Colour": "Red"
})
variant.save()
make_website_item(template_item) # publish template not variant
update_cart("Test-Tshirt-Temp-S-R", 1)
cart = get_cart_quotation() # test if cart page gets data without errors
doc = cart.get("doc")
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
# test if items are rendered without error
frappe.render_template("templates/includes/cart/cart_items.html", cart)
def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:

View File

@@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
val = frappe.cache().get_value('ordered_attribute_values_map')
if val: return val
all_attribute_values = frappe.db.get_all('Item Attribute Value',
all_attribute_values = frappe.get_all('Item Attribute Value',
['attribute_value', 'idx', 'parent'], order_by='idx asc')
ordered_attribute_values_map = frappe._dict({})
@@ -57,25 +57,34 @@ class ItemVariantsCacheManager:
def build_cache(self):
parent_item_code = self.item_code
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
attributes = [
a.attribute for a in frappe.get_all(
'Item Variant Attribute',
{'parent': parent_item_code},
['attribute'],
order_by='idx asc'
)
]
item_variants_data = frappe.db.get_all('Item Variant Attribute',
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
# join with Website Item
item_variants_data = frappe.get_all(
'Item Variant Attribute',
{'variant_of': parent_item_code},
['parent', 'attribute', 'attribute_value'],
order_by='name',
as_list=1
)
unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
disabled_items = set(
[i.name for i in frappe.db.get_all('Item', {'disabled': 1})]
)
attribute_value_item_map = frappe._dict({})
item_attribute_value_map = frappe._dict({})
attribute_value_item_map = frappe._dict()
item_attribute_value_map = frappe._dict()
# dont consider variants that are unpublished
# (either have no Website Item or are unpublished in Website Item)
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
# dont consider variants that are disabled
# pull all other variants
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items]
for row in item_variants_data:
item_code, attribute, attribute_value = row

View File

@@ -1,11 +1,119 @@
# import frappe
import unittest
# from erpnext.e_commerce.product_data_engine.query import ProductQuery
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
import frappe
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
setup_e_commerce_settings,
)
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Item"]
class TestVariantSelector(unittest.TestCase):
# TODO: Variant Selector Tests
pass
class TestVariantSelector(ERPNextTestCase):
@classmethod
def setUpClass(cls):
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
# create L-R, L-G, M-R, M-G and S-R
for size in ("Large", "Medium",):
for colour in ("Red", "Green",):
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": size, "Test Colour": colour
})
variant.save()
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small", "Test Colour": "Red"
})
variant.save()
make_website_item(template_item) # publish template not variants
def test_item_attributes(self):
"""
Test if the right attributes are fetched in the popup.
(Attributes must only come from active items)
Attribute selection must not be linked to Website Items.
"""
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
self.assertEqual(attr_data[0]["attribute"], "Test Size")
self.assertEqual(attr_data[1]["attribute"], "Test Colour")
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
# disable small red tshirt, now there are no small tshirts.
# but there are some red tshirts
small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
small_variant.disabled = 1
small_variant.save() # trigger cache rebuild
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
# Only L and M attribute values must be fetched since S is disabled
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
# teardown
small_variant.disabled = 0
small_variant.save()
def test_next_item_variant_values(self):
"""
Test if on selecting an attribute value, the next possible values
are filtered accordingly.
Values that dont apply should not be fetched.
E.g.
There is a ** Small-Red ** Tshirt. No other colour in this size.
On selecting ** Small **, only ** Red ** should be selectable next.
"""
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
filtered_items = next_values["filtered_items"]
self.assertEqual(len(next_colours), 1)
self.assertEqual(next_colours.pop(), "Red")
self.assertEqual(len(filtered_items), 1)
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
def test_exact_match_with_price(self):
"""
Test price fetching and matching of variant without Website Item
"""
from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
frappe.set_user("Administrator")
setup_e_commerce_settings({
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
next_values = get_next_attribute_and_values(
"Test-Tshirt-Temp",
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
)
print(">>>>", next_values)
price_info = next_values["product_info"]["price"]
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(price_info["price_list_rate"], 100.0)
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")

View File

@@ -1,7 +1,12 @@
import frappe
from frappe.utils import cint
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
)
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
from erpnext.utilities.product import get_price
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
@@ -143,14 +148,13 @@ def get_next_attribute_and_values(item_code, selected_attributes):
filtered_items_count = len(filtered_items)
# get product info if exact match
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
# from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
if exact_match:
data = get_product_info_for_website(exact_match[0])
product_info = data.product_info
cart_settings = get_shopping_cart_settings()
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
if product_info:
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
if not data.cart_settings.show_price:
product_info = None
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
else:
product_info = None
@@ -195,3 +199,20 @@ def get_item_attributes(item_code):
return attributes
def get_item_variant_price_dict(item_code, cart_settings):
if cart_settings.enabled and cart_settings.show_price:
is_guest = frappe.session.user == "Guest"
# Show Price if logged in.
# If not logged in, check if price is hidden for guest.
if not is_guest or not cart_settings.hide_price_for_guest:
price_list = _set_price_list(cart_settings, None)
price = get_price(
item_code,
price_list,
cart_settings.default_customer_group,
cart_settings.company
)
return {"price": price}
return None

View File

@@ -23,9 +23,9 @@
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
{%- set item = values['card_' + index + '_item'] -%}
{%- if item -%}
{%- set item = frappe.get_doc("Item", item) -%}
{%- set web_item = frappe.get_doc("Website Item", item) -%}
{{ item_card(
item, is_featured=values['card_' + index + '_featured'],
web_item, is_featured=values['card_' + index + '_featured'],
is_full_width=True, align="Center"
) }}
{%- endif -%}

View File

@@ -37,8 +37,8 @@
{
"fieldname": "card_1_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -56,8 +56,8 @@
{
"fieldname": "card_2_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -76,8 +76,8 @@
{
"fieldname": "card_3_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -95,8 +95,8 @@
{
"fieldname": "card_4_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -114,8 +114,8 @@
{
"fieldname": "card_5_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -133,8 +133,8 @@
{
"fieldname": "card_6_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -152,8 +152,8 @@
{
"fieldname": "card_7_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -171,8 +171,8 @@
{
"fieldname": "card_8_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -190,8 +190,8 @@
{
"fieldname": "card_9_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -209,8 +209,8 @@
{
"fieldname": "card_10_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -228,8 +228,8 @@
{
"fieldname": "card_11_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -247,8 +247,8 @@
{
"fieldname": "card_12_item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"label": "Website Item",
"options": "Website Item",
"reqd": 0
},
{
@@ -259,7 +259,7 @@
}
],
"idx": 0,
"modified": "2021-02-24 16:05:31.242342",
"modified": "2021-12-21 14:44:59.821335",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Item Card Group",

View File

@@ -201,8 +201,8 @@ def get_course_schedule_events(start, end, filters=None):
conditions = get_event_conditions("Course Schedule", filters)
data = frappe.db.sql("""select name, course, color,
timestamp(schedule_date, from_time) as from_datetime,
timestamp(schedule_date, to_time) as to_datetime,
timestamp(schedule_date, from_time) as from_time,
timestamp(schedule_date, to_time) as to_time,
room, student_group, 0 as 'allDay'
from `tabCourse Schedule`
where ( schedule_date between %(start)s and %(end)s )

View File

@@ -3,6 +3,8 @@
# For license information, please see license.txt
from datetime import datetime
import frappe
from frappe import _
from frappe.model.document import Document
@@ -30,6 +32,14 @@ class CourseSchedule(Document):
if self.from_time > self.to_time:
frappe.throw(_("From Time cannot be greater than To Time."))
"""Handles specicfic case to update schedule date in calendar """
if isinstance(self.from_time, str):
try:
datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S')
self.schedule_date = datetime_obj
except ValueError:
pass
def validate_overlap(self):
"""Validates overlap for Student Group, Instructor, Room"""
@@ -47,4 +57,4 @@ class CourseSchedule(Document):
validate_overlap_for(self, "Assessment Plan", "student_group")
validate_overlap_for(self, "Assessment Plan", "room")
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)

View File

@@ -1,11 +1,10 @@
frappe.views.calendar["Course Schedule"] = {
field_map: {
// from_datetime and to_datetime don't exist as docfields but are used in onload
"start": "from_datetime",
"end": "to_datetime",
"start": "from_time",
"end": "to_time",
"id": "name",
"title": "course",
"allDay": "allDay"
"allDay": "allDay",
},
gantt: false,
order_by: "schedule_date",

View File

@@ -6,6 +6,7 @@ import unittest
import frappe
from frappe.utils import to_timedelta, today
from frappe.utils.data import add_to_date
from erpnext.education.utils import OverlapError
@@ -39,6 +40,11 @@ class TestCourseSchedule(unittest.TestCase):
make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
def test_update_schedule_date(self):
doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1))
doc.schedule_date = add_to_date(doc.schedule_date, days=1)
doc.save()
def make_course_schedule_test_record(**args):
args = frappe._dict(args)

View File

@@ -99,7 +99,6 @@
"search_index": 1
},
{
"fetch_from": "inpatient_record.patient",
"fieldname": "patient",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -559,7 +558,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-11-30 11:04:17.195848",
"modified": "2022-01-20 12:37:07.943153",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Lab Test",

View File

@@ -143,7 +143,7 @@ class Patient(Document):
age = self.age
if not age:
return
age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
age_str = str(age.years) + ' ' + _("Year(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str
@frappe.whitelist()

View File

@@ -337,9 +337,13 @@ let check_and_set_availability = function(frm) {
});
d.fields_dict['department'].df.onchange = () => {
d.set_values({
'practitioner': ''
});
if (d.get_value('department') == frm.doc.department) {
d.set_value('practitioner', frm.doc.practitioner);
} else {
d.set_value('practitioner', '');
d.fields_dict.available_slots.html('');
d.get_primary_btn().attr('disabled', true);
}
let department = d.get_value('department');
if (department) {
d.fields_dict.practitioner.get_query = function() {
@@ -426,7 +430,8 @@ let check_and_set_availability = function(frm) {
slot_details.forEach((slot_info) => {
slot_html += `<div class="slot-info">
<span> <b> ${__('Practitioner Schedule:')} </b> ${slot_info.slot_name} </span><br>
<span> <b> ${slot_info.practitioner_name} </b> </span><br>
<span> <b> ${__('Schedule:')} </b> ${slot_info.slot_name} </span><br>
<span> <b> ${__('Service Unit:')} </b> ${slot_info.service_unit} </span>`;
if (slot_info.service_unit_capacity) {

View File

@@ -388,7 +388,8 @@ def get_available_slots(practitioner_doc, date):
fields=['name', 'appointment_time', 'duration', 'status'])
slot_details.append({'slot_name': slot_name, 'service_unit': schedule_entry.service_unit, 'avail_slot': available_slots,
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity})
'appointments': appointments, 'allow_overlap': allow_overlap, 'service_unit_capacity': service_unit_capacity,
'practitioner_name': practitioner_doc.practitioner_name})
return slot_details

View File

@@ -66,7 +66,6 @@
"search_index": 1
},
{
"fetch_from": "inpatient_record.patient",
"fieldname": "patient",
"fieldtype": "Link",
"hide_days": 1,
@@ -224,7 +223,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-07-30 16:53:13.076104",
"modified": "2022-01-20 12:38:55.382621",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Sample Collection",

View File

@@ -51,7 +51,6 @@
"read_only": 1
},
{
"fetch_from": "inpatient_record.patient",
"fieldname": "patient",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -259,7 +258,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-05-17 22:23:24.632286",
"modified": "2022-01-20 12:30:07.515185",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Vital Signs",

View File

@@ -388,7 +388,7 @@ scheduler_events = {
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
"erpnext.non_profit.doctype.membership.membership.set_expired_status",
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
],
"daily_long": [

View File

@@ -12,14 +12,15 @@ class AppointmentLetter(Document):
@frappe.whitelist()
def get_appointment_letter_details(template):
body = []
intro= frappe.get_list("Appointment Letter Template",
fields = ['introduction', 'closing_notes'],
filters={'name': template
})[0]
content = frappe.get_list("Appointment Letter content",
fields = ['title', 'description'],
filters={'parent': template
})
intro = frappe.get_list('Appointment Letter Template',
fields=['introduction', 'closing_notes'],
filters={'name': template}
)[0]
content = frappe.get_all('Appointment Letter content',
fields=['title', 'description'],
filters={'parent': template},
order_by='idx'
)
body.append(intro)
body.append({'description': content})
return body

View File

@@ -5,9 +5,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, formatdate, get_datetime, getdate, nowdate
from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate
from erpnext.hr.utils import validate_active_employee
from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
class Attendance(Document):
@@ -171,7 +171,7 @@ def get_month_map():
})
@frappe.whitelist()
def get_unmarked_days(employee, month):
def get_unmarked_days(employee, month, exclude_holidays=0):
import calendar
month_map = get_month_map()
@@ -191,6 +191,11 @@ def get_unmarked_days(employee, month):
])
marked_days = [get_datetime(record.attendance_date) for record in records]
if cint(exclude_holidays):
holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end)
holidays = [get_datetime(record) for record in holiday_dates]
marked_days.extend(holidays)
unmarked_days = []
for date in dates_of_month:

View File

@@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = {
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("exclude_holidays", "hidden", 1);
dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
@@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = {
onchange: function() {
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("exclude_holidays", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
me.get_multi_select_options(
dialog.fields_dict.employee.value,
dialog.fields_dict.month.value,
dialog.fields_dict.exclude_holidays.get_value()
).then(options => {
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
@@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = {
reqd: 1,
},
{
label: __("Exclude Holidays"),
fieldtype: "Check",
fieldname: "exclude_holidays",
hidden: 1,
onchange: function() {
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
me.get_multi_select_options(
dialog.fields_dict.employee.value,
dialog.fields_dict.month.value,
dialog.fields_dict.exclude_holidays.get_value()
).then(options => {
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
} else {
dialog.no_unmarked_days_left = true;
}
});
}
}
},
{
label: __("Unmarked Attendance for days"),
fieldname: "unmarked_days",
@@ -105,7 +136,7 @@ frappe.listview_settings['Attendance'] = {
});
},
get_multi_select_options: function(employee, month) {
get_multi_select_options: function(employee, month, exclude_holidays) {
return new Promise(resolve => {
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
@@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = {
args: {
employee: employee,
month: month,
exclude_holidays: exclude_holidays
}
}).then(r => {
var options = [];

View File

@@ -68,12 +68,18 @@ class Employee(NestedSet):
self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name]))
def validate_user_details(self):
data = frappe.db.get_value('User',
self.user_id, ['enabled', 'user_image'], as_dict=1)
if data.get("user_image") and self.image == '':
self.image = data.get("user_image")
self.validate_for_enabled_user_id(data.get("enabled", 0))
self.validate_duplicate_user_id()
if self.user_id:
data = frappe.db.get_value("User",
self.user_id, ["enabled", "user_image"], as_dict=1)
if not data:
self.user_id = None
return
if data.get("user_image") and self.image == "":
self.image = data.get("user_image")
self.validate_for_enabled_user_id(data.get("enabled", 0))
self.validate_duplicate_user_id()
def update_nsm_model(self):
frappe.utils.nestedset.update_nsm(self)

View File

@@ -17,7 +17,10 @@ class TestEmployeeOnboarding(unittest.TestCase):
def test_employee_onboarding_incomplete_task(self):
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
_set_up()
frappe.db.sql("delete from `tabEmployee Onboarding`")
project = "Employee Onboarding : test@researcher.com"
frappe.db.sql("delete from tabProject where name=%s", project)
frappe.db.sql("delete from tabTask where project=%s", project)
applicant = get_job_applicant()
job_offer = create_job_offer(job_applicant=applicant.name)
@@ -42,7 +45,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
onboarding.submit()
project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com')
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
@@ -65,8 +68,8 @@ class TestEmployeeOnboarding(unittest.TestCase):
self.assertEqual(employee.employee_name, 'Test Researcher')
def get_job_applicant():
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
if frappe.db.exists('Job Applicant', 'test@researcher.com'):
return frappe.get_doc('Job Applicant', 'test@researcher.com')
applicant = frappe.new_doc('Job Applicant')
applicant.applicant_name = 'Test Researcher'
applicant.email_id = 'test@researcher.com'

View File

@@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", {
['docstatus', '=', 1],
['employee', '=', frm.doc.employee],
['paid_amount', '>', 0],
['paid_amount', '>', 'claimed_amount']
['status', '!=', 'Claimed']
]
};
});

View File

@@ -10,15 +10,17 @@ from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
test_records = frappe.get_test_records('Expense Claim')
test_dependencies = ['Employee']
company_name = '_Test Company 4'
company_name = '_Test Company 3'
class TestExpenseClaim(unittest.TestCase):
def tearDown(self):
frappe.db.rollback()
def test_total_expense_claim_for_project(self):
frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """)
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
frappe.db.sql("""delete from `tabTask`""")
frappe.db.sql("""delete from `tabProject`""")
frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
project = frappe.get_doc({
@@ -37,12 +39,12 @@ class TestExpenseClaim(unittest.TestCase):
task_name = task.name
payable_account = get_payable_account(company_name)
make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name)
make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name)
expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700)
@@ -54,7 +56,7 @@ class TestExpenseClaim(unittest.TestCase):
def test_expense_claim_status(self):
payable_account = get_payable_account(company_name)
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4")
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3")
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
je = frappe.get_doc(je_dict)
@@ -73,7 +75,7 @@ class TestExpenseClaim(unittest.TestCase):
def test_expense_claim_gl_entry(self):
payable_account = get_payable_account(company_name)
taxes = generate_taxes()
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3",
do_not_submit=True, taxes=taxes)
expense_claim.submit()
@@ -84,9 +86,9 @@ class TestExpenseClaim(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
['Output Tax CGST - _TC4',18.0, 0.0],
['Output Tax CGST - _TC3',18.0, 0.0],
[payable_account, 0.0, 218.0],
["Travel Expenses - _TC4", 200.0, 0.0]
["Travel Expenses - _TC3", 200.0, 0.0]
])
for gle in gl_entries:
@@ -102,7 +104,7 @@ class TestExpenseClaim(unittest.TestCase):
"payable_account": payable_account,
"approval_status": "Rejected",
"expenses":
[{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }]
[{"expense_type": "Travel", "default_account": "Travel Expenses - _TC3", "amount": 300, "sanctioned_amount": 200}]
})
expense_claim.submit()

View File

@@ -192,10 +192,11 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-29 23:06:10.904260",
"modified": "2022-01-12 16:28:53.196881",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -210,10 +211,11 @@
"write": 1
}
],
"search_fields": "applicant_name",
"search_fields": "applicant_name, email_id, job_title, phone_number",
"sender_field": "email_id",
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"subject_field": "notes",
"title_field": "applicant_name"
}

View File

@@ -7,6 +7,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.utils import validate_email_address
from erpnext.hr.doctype.interview.interview import get_interviewers
@@ -21,10 +22,11 @@ class JobApplicant(Document):
self.get("__onload").job_offer = job_offer[0].name
def autoname(self):
keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
if not keys:
frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
self.name = " - ".join(keys)
self.name = self.email_id
# applicant can apply more than once for a different job title or reapply
if frappe.db.exists("Job Applicant", self.name):
self.name = append_number_if_name_exists("Job Applicant", self.name)
def validate(self):
if self.email_id:

View File

@@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
class TestJobApplicant(unittest.TestCase):
pass
def test_job_applicant_naming(self):
applicant = frappe.get_doc({
"doctype": "Job Applicant",
"status": "Open",
"applicant_name": "_Test Applicant",
"email_id": "job_applicant_naming@example.com"
}).insert()
self.assertEqual(applicant.name, 'job_applicant_naming@example.com')
applicant = frappe.get_doc({
"doctype": "Job Applicant",
"status": "Open",
"applicant_name": "_Test Applicant",
"email_id": "job_applicant_naming@example.com"
}).insert()
self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1')
def tearDown(self):
frappe.db.rollback()
def create_job_applicant(**args):
args = frappe._dict(args)

View File

@@ -5,6 +5,7 @@ import frappe
from frappe.utils import add_days, add_months, getdate, nowdate
import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
@@ -14,16 +15,19 @@ class TestLeaveAllocation(unittest.TestCase):
def setUpClass(cls):
frappe.db.sql("delete from `tabLeave Period`")
def test_overlapping_allocation(self):
frappe.db.sql("delete from `tabLeave Allocation`")
emp_id = make_employee("test_emp_leave_allocation@salary.com")
cls.employee = frappe.get_doc("Employee", emp_id)
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
def tearDown(self):
frappe.db.rollback()
def test_overlapping_allocation(self):
leaves = [
{
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": employee.name,
"employee_name": employee.employee_name,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-10-01"),
"to_date": getdate("2015-10-31"),
@@ -33,8 +37,8 @@ class TestLeaveAllocation(unittest.TestCase):
{
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": employee.name,
"employee_name": employee.employee_name,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-01"),
"to_date": getdate("2015-11-30"),
@@ -46,40 +50,36 @@ class TestLeaveAllocation(unittest.TestCase):
self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save)
def test_invalid_period(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
doc = frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": employee.name,
"employee_name": employee.employee_name,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-30"),
"to_date": getdate("2015-09-1"),
"new_leaves_allocated": 5
})
#invalid period
# invalid period
self.assertRaises(frappe.ValidationError, doc.save)
def test_allocated_leave_days_over_period(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
doc = frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": employee.name,
"employee_name": employee.employee_name,
"employee": self.employee.name,
"employee_name": self.employee.employee_name,
"leave_type": "_Test Leave Type",
"from_date": getdate("2015-09-1"),
"to_date": getdate("2015-09-30"),
"new_leaves_allocated": 35
})
#allocated leave more than period
# allocated leave more than period
self.assertRaises(frappe.ValidationError, doc.save)
def test_carry_forward_calculation(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
leave_type.maximum_carry_forwarded_leaves = 10
leave_type.max_leaves_allowed = 30
@@ -87,6 +87,8 @@ class TestLeaveAllocation(unittest.TestCase):
# initial leave allocation = 15
leave_allocation = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave",
from_date=add_months(nowdate(), -12),
to_date=add_months(nowdate(), -1),
@@ -96,6 +98,8 @@ class TestLeaveAllocation(unittest.TestCase):
# carry forwarded leaves considering maximum_carry_forwarded_leaves
# new_leaves = 15, carry_forwarded = 10
leave_allocation_1 = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave",
carry_forward=1)
leave_allocation_1.submit()
@@ -107,6 +111,8 @@ class TestLeaveAllocation(unittest.TestCase):
# carry forwarded leaves considering max_leave_allowed
# max_leave_allowed = 30, new_leaves = 25, carry_forwarded = 5
leave_allocation_2 = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave",
carry_forward=1,
new_leaves_allocated=25)
@@ -115,8 +121,6 @@ class TestLeaveAllocation(unittest.TestCase):
self.assertEqual(leave_allocation_2.unused_leaves, 5)
def test_carry_forward_leaves_expiry(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
@@ -125,6 +129,8 @@ class TestLeaveAllocation(unittest.TestCase):
# initial leave allocation
leave_allocation = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave_expiry",
from_date=add_months(nowdate(), -24),
to_date=add_months(nowdate(), -12),
@@ -132,6 +138,8 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.submit()
leave_allocation = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave_expiry",
from_date=add_days(nowdate(), -90),
to_date=add_days(nowdate(), 100),
@@ -143,6 +151,8 @@ class TestLeaveAllocation(unittest.TestCase):
# leave allocation with carry forward of only new leaves allocated
leave_allocation_1 = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name,
leave_type="_Test_CF_leave_expiry",
carry_forward=1,
from_date=add_months(nowdate(), 6),
@@ -152,9 +162,10 @@ class TestLeaveAllocation(unittest.TestCase):
self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated)
def test_creation_of_leave_ledger_entry_on_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
leave_allocation = create_leave_allocation()
leave_allocation = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name
)
leave_allocation.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name))
@@ -169,10 +180,10 @@ class TestLeaveAllocation(unittest.TestCase):
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
def test_leave_addition_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name
)
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 40
@@ -180,44 +191,55 @@ class TestLeaveAllocation(unittest.TestCase):
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
def test_leave_subtraction_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name
)
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 10
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
def test_against_leave_application_validation_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
def test_validation_against_leave_application_after_submit(self):
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
leave_allocation = create_leave_allocation()
make_holiday_list()
frappe.db.set_value("Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List")
leave_allocation = create_leave_allocation(
employee=self.employee.name,
employee_name=self.employee.employee_name
)
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
leave_application = frappe.get_doc({
"doctype": 'Leave Application',
"employee": employee.name,
"employee": self.employee.name,
"leave_type": "_Test Leave Type",
"from_date": add_months(nowdate(), 2),
"to_date": add_months(add_days(nowdate(), 10), 2),
"company": erpnext.get_default_company() or "_Test Company",
"company": self.employee.company,
"docstatus": 1,
"status": "Approved",
"leave_approver": 'test@example.com'
})
leave_application.submit()
leave_allocation.new_leaves_allocated = 8
leave_allocation.total_leaves_allocated = 8
leave_application.reload()
# allocate less leaves than the ones which are already approved
leave_allocation.new_leaves_allocated = leave_application.total_leave_days - 1
leave_allocation.total_leaves_allocated = leave_application.total_leave_days - 1
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
def create_leave_allocation(**args):
args = frappe._dict(args)
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
leave_allocation = frappe.get_doc({
emp_id = make_employee("test_emp_leave_allocation@salary.com")
employee = frappe.get_doc("Employee", emp_id)
return frappe.get_doc({
"doctype": "Leave Allocation",
"__islocal": 1,
"employee": args.employee or employee.name,
@@ -228,6 +250,5 @@ def create_leave_allocation(**args):
"carry_forward": args.carry_forward or 0,
"to_date": args.to_date or add_months(nowdate(), 12)
})
return leave_allocation
test_dependencies = ["Employee", "Leave Type"]

View File

@@ -22,6 +22,7 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.utils import (
get_holiday_dates_for_employee,
get_leave_period,
set_employee_name,
share_doc_with_approver,
@@ -159,33 +160,57 @@ class LeaveApplication(Document):
.format(formatdate(future_allocation[0].from_date), future_allocation[0].name))
def update_attendance(self):
if self.status == "Approved":
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime("%Y-%m-%d")
status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
attendance_date = date, docstatus = ('!=', 2)))
if self.status != "Approved":
return
holiday_dates = []
if not frappe.db.get_value("Leave Type", self.leave_type, "include_holiday"):
holiday_dates = get_holiday_dates_for_employee(self.employee, self.from_date, self.to_date)
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime("%Y-%m-%d")
attendance_name = frappe.db.exists("Attendance", dict(employee = self.employee,
attendance_date = date, docstatus = ('!=', 2)))
# don't mark attendance for holidays
# if leave type does not include holidays within leaves as leaves
if date in holiday_dates:
if attendance_name:
# update existing attendance, change absent to on leave
doc = frappe.get_doc('Attendance', attendance_name)
if doc.status != status:
doc.db_set('status', status)
doc.db_set('leave_type', self.leave_type)
doc.db_set('leave_application', self.name)
else:
# make new attendance and submit it
doc = frappe.new_doc("Attendance")
doc.employee = self.employee
doc.employee_name = self.employee_name
doc.attendance_date = date
doc.company = self.company
doc.leave_type = self.leave_type
doc.leave_application = self.name
doc.status = status
doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True)
doc.submit()
# cancel and delete existing attendance for holidays
attendance = frappe.get_doc("Attendance", attendance_name)
attendance.flags.ignore_permissions = True
if attendance.docstatus == 1:
attendance.cancel()
frappe.delete_doc("Attendance", attendance_name, force=1)
continue
self.create_or_update_attendance(attendance_name, date)
def create_or_update_attendance(self, attendance_name, date):
status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
if attendance_name:
# update existing attendance, change absent to on leave
doc = frappe.get_doc('Attendance', attendance_name)
if doc.status != status:
doc.db_set({
'status': status,
'leave_type': self.leave_type,
'leave_application': self.name
})
else:
# make new attendance and submit it
doc = frappe.new_doc("Attendance")
doc.employee = self.employee
doc.employee_name = self.employee_name
doc.attendance_date = date
doc.company = self.company
doc.leave_type = self.leave_type
doc.leave_application = self.name
doc.status = status
doc.flags.ignore_validate = True
doc.insert(ignore_permissions=True)
doc.submit()
def cancel_attendance(self):
if self.docstatus == 2:

View File

@@ -5,7 +5,16 @@ import unittest
import frappe
from frappe.permissions import clear_user_permissions_for_doctype
from frappe.utils import add_days, add_months, getdate, nowdate
from frappe.utils import (
add_days,
add_months,
get_first_day,
get_last_day,
get_year_ending,
get_year_start,
getdate,
nowdate,
)
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
@@ -19,6 +28,10 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
)
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
make_holiday_list,
make_leave_application,
)
test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
@@ -61,13 +74,15 @@ class TestLeaveApplication(unittest.TestCase):
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
frappe.set_user("Administrator")
@classmethod
def setUpClass(cls):
set_leave_approver()
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
def tearDown(self):
frappe.set_user("Administrator")
frappe.db.rollback()
def _clear_roles(self):
frappe.db.sql("""delete from `tabHas Role` where parent in
@@ -106,6 +121,72 @@ class TestLeaveApplication(unittest.TestCase):
for d in ('2018-01-01', '2018-01-02', '2018-01-03'):
self.assertTrue(getdate(d) in dates)
def test_attendance_for_include_holidays(self):
# Case 1: leave type with 'Include holidays within leaves as leaves' enabled
frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1)
leave_type = frappe.get_doc(dict(
leave_type_name="Test Include Holidays",
doctype="Leave Type",
include_holiday=True
)).insert()
date = getdate()
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
holiday_list = make_holiday_list()
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list)
leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
leave_application.reload()
self.assertEqual(leave_application.total_leave_days, 4)
self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
leave_application.cancel()
def test_attendance_update_for_exclude_holidays(self):
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
leave_type = frappe.get_doc(dict(
leave_type_name="Test Do Not Include Holidays",
doctype="Leave Type",
include_holiday=False
)).insert()
date = getdate()
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
holiday_list = make_holiday_list()
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list)
# already marked attendance on a holiday should be deleted in this case
config = {
"doctype": "Attendance",
"employee": "_T-Employee-00001",
"status": "Present"
}
attendance_on_holiday = frappe.get_doc(config)
attendance_on_holiday.attendance_date = first_sunday
attendance_on_holiday.save()
# already marked attendance on a non-holiday should be updated
attendance = frappe.get_doc(config)
attendance.attendance_date = add_days(first_sunday, 3)
attendance.save()
leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name)
leave_application.reload()
# holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3)
self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3)
# attendance on holiday deleted
self.assertFalse(frappe.db.exists("Attendance", attendance_on_holiday.name))
# attendance on non-holiday updated
self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
def test_block_list(self):
self._clear_roles()
@@ -241,7 +322,13 @@ class TestLeaveApplication(unittest.TestCase):
leave_period = get_leave_period()
today = nowdate()
holiday_list = 'Test Holiday List for Optional Holiday'
optional_leave_date = add_days(today, 7)
employee = get_employee()
default_holiday_list = make_holiday_list()
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list)
first_sunday = get_first_sunday(default_holiday_list)
optional_leave_date = add_days(first_sunday, 1)
if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict(
@@ -253,7 +340,6 @@ class TestLeaveApplication(unittest.TestCase):
dict(holiday_date = optional_leave_date, description = 'Test')
]
)).insert()
employee = get_employee()
frappe.db.set_value('Leave Period', leave_period.name, 'optional_holiday_list', holiday_list)
leave_type = 'Test Optional Type'
@@ -266,7 +352,7 @@ class TestLeaveApplication(unittest.TestCase):
allocate_leaves(employee, leave_period, leave_type, 10)
date = add_days(today, 6)
date = add_days(first_sunday, 2)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
@@ -636,13 +722,13 @@ def create_carry_forwarded_allocation(employee, leave_type):
carry_forward=1)
leave_allocation.submit()
def make_allocation_record(employee=None, leave_type=None):
def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None):
allocation = frappe.get_doc({
"doctype": "Leave Allocation",
"employee": employee or "_T-Employee-00001",
"leave_type": leave_type or "_Test Leave Type",
"from_date": "2013-01-01",
"to_date": "2019-12-31",
"from_date": from_date or "2013-01-01",
"to_date": to_date or "2019-12-31",
"new_leaves_allocated": 30
})
@@ -691,3 +777,16 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
}).insert()
allocate_leave.submit()
def get_first_sunday(holiday_list):
month_start_date = get_first_day(nowdate())
month_end_date = get_last_day(nowdate())
first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday`
where parent = %s
and holiday_date between %s and %s
order by holiday_date
""", (holiday_list, month_start_date, month_end_date))[0][0]
return first_sunday

View File

@@ -1,294 +1,108 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "HR-LPR-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 15:20:52.864288",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"from_date",
"to_date",
"is_active",
"column_break_3",
"company",
"optional_holiday_list"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "From Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "To Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_active",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Active",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Is Active"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "optional_holiday_list",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holiday List for Optional Leave",
"length": 0,
"no_copy": 0,
"options": "Holiday List",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Holiday List"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-05-30 16:15:43.305502",
"links": [],
"modified": "2022-01-13 13:28:12.951025",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Period",
"name_case": "",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"search_fields": "from_date, to_date, company",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
"states": [],
"track_changes": 1
}

View File

@@ -113,10 +113,11 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-01 17:54:01.014509",
"modified": "2022-01-13 13:37:11.218882",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -164,5 +165,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "employee_name",
"track_changes": 1
}

View File

@@ -57,9 +57,7 @@ class LeavePolicyAssignment(Document):
leave_policy_detail.leave_type, leave_policy_detail.annual_allocation,
leave_type_details, date_of_joining
)
leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated}
self.db_set("leaves_allocated", 1)
return leave_allocations
@@ -131,6 +129,8 @@ class LeavePolicyAssignment(Document):
monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
new_leaves_allocated = monthly_earned_leave * months_passed
else:
new_leaves_allocated = 0
return new_leaves_allocated

View File

@@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
if (cur_dialog.fields_dict.leave_period.value) {
me.set_effective_date();
}
}
},
get_query() {
let filters = {"is_active": 1};
if (cur_dialog.fields_dict.company.value)
filters["company"] = cur_dialog.fields_dict.company.value;
return {
filters: filters
};
},
},
{
fieldtype: "Column Break"

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.utils import add_months, get_first_day, getdate
from erpnext.hr.doctype.leave_application.test_leave_application import (
get_employee,
@@ -17,9 +18,8 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
test_dependencies = ["Employee"]
class TestLeavePolicyAssignment(unittest.TestCase):
def setUp(self):
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
def test_grant_leaves(self):
@@ -54,8 +54,8 @@ class TestLeavePolicyAssignment(unittest.TestCase):
self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10)
self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type")
self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date)
self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date)
self.assertEqual(getdate(leave_alloc_doc.from_date), getdate(leave_period.from_date))
self.assertEqual(getdate(leave_alloc_doc.to_date), getdate(leave_period.to_date))
self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name)
self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0])
@@ -101,6 +101,55 @@ class TestLeavePolicyAssignment(unittest.TestCase):
# User are now allowed to grant leave
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0)
def test_earned_leave_allocation(self):
leave_period = create_leave_period("Test Earned Leave Period")
employee = get_employee()
leave_type = create_earned_leave_type("Test Earned Leave")
leave_policy = frappe.get_doc({
"doctype": "Leave Policy",
"leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}]
}).insert()
data = {
"assignment_based_on": "Leave Period",
"leave_policy": leave_policy.name,
"leave_period": leave_period.name
}
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
# leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency
leaves_allocated = frappe.db.get_value("Leave Allocation", {
"leave_policy_assignment": leave_policy_assignments[0]
}, "total_leaves_allocated")
self.assertEqual(leaves_allocated, 0)
def tearDown(self):
for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]:
frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec
frappe.db.rollback()
def create_earned_leave_type(leave_type):
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
return frappe.get_doc(dict(
leave_type_name=leave_type,
doctype="Leave Type",
is_earned_leave=1,
earned_leave_frequency="Monthly",
rounding=0.5,
max_leaves_allowed=6
)).insert()
def create_leave_period(name):
frappe.delete_doc_if_exists("Leave Period", name, force=1)
start_date = get_first_day(getdate())
return frappe.get_doc(dict(
name=name,
doctype="Leave Period",
from_date=start_date,
to_date=add_months(start_date, 12),
company="_Test Company",
is_active=1
)).insert()

View File

@@ -4,15 +4,32 @@
frappe.ui.form.on('Shift Type', {
refresh: function(frm) {
frm.add_custom_button(
'Mark Attendance',
() => frm.call({
doc: frm.doc,
method: 'process_auto_attendance',
freeze: true,
callback: () => {
frappe.msgprint(__("Attendance has been marked as per employee check-ins"));
__('Mark Attendance'),
() => {
if (!frm.doc.enable_auto_attendance) {
frm.scroll_to_field('enable_auto_attendance');
frappe.throw(__('Please Enable Auto Attendance and complete the setup first.'));
}
})
if (!frm.doc.process_attendance_after) {
frm.scroll_to_field('process_attendance_after');
frappe.throw(__('Please set {0}.', [__('Process Attendance After').bold()]));
}
if (!frm.doc.last_sync_of_checkin) {
frm.scroll_to_field('last_sync_of_checkin');
frappe.throw(__('Please set {0}.', [__('Last Sync of Checkin').bold()]));
}
frm.call({
doc: frm.doc,
method: 'process_auto_attendance',
freeze: true,
callback: () => {
frappe.msgprint(__('Attendance has been marked as per employee check-ins'));
}
});
}
);
}
});

View File

@@ -46,7 +46,7 @@ frappe.ui.form.on('Loan', {
});
});
$.each(["payment_account", "loan_account"], function (i, field) {
$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
@@ -88,6 +88,10 @@ frappe.ui.form.on('Loan', {
frm.add_custom_button(__('Loan Write Off'), function() {
frm.trigger("make_loan_write_off_entry");
},__('Create'));
frm.add_custom_button(__('Loan Refund'), function() {
frm.trigger("make_loan_refund");
},__('Create'));
}
}
frm.trigger("toggle_fields");
@@ -155,6 +159,21 @@ frappe.ui.form.on('Loan', {
})
},
make_loan_refund: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name
},
method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv",
callback: function (r) {
if (r.message) {
let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
})
},
request_loan_closure: function(frm) {
frappe.confirm(__("Do you really want to close this loan"),
function() {

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