Compare commits

...

291 Commits

Author SHA1 Message Date
Frappe PR Bot
f17b2de420 chore(release): Bumped to Version 13.49.6
## [13.49.6](https://github.com/frappe/erpnext/compare/v13.49.5...v13.49.6) (2023-03-22)

### Bug Fixes

* `Blanket Order` (backport [#34279](https://github.com/frappe/erpnext/issues/34279)) (backport [#34548](https://github.com/frappe/erpnext/issues/34548)) ([#34553](https://github.com/frappe/erpnext/issues/34553)) ([d5efeec](d5efeec0a4))
2023-03-22 09:29:45 +00:00
mergify[bot]
d5efeec0a4 fix: Blanket Order (backport #34279) (backport #34548) (#34553)
fix: `Blanket Order` (backport #34279) (#34548)

* fix: hide `+` button based on `Blanket Order Type`

(cherry picked from commit abf9a28d6a)

* feat: add field `Over Order Allowance (%)` in `Buying Settings`

(cherry picked from commit f5937f46cb)

# Conflicts:
#	erpnext/buying/doctype/buying_settings/buying_settings.json

* refactor: rewrite `blanket_order.py` queries in `QB`

(cherry picked from commit f3993783a3)

* fix: don't map item row having `0` qty

(cherry picked from commit fc1088d9c4)

* feat: consider `over_order_allowance` while validating order qty

(cherry picked from commit 8bcbc45add)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py

* feat: add field `Over Order Allowance (%)` in `Selling Settings`

(cherry picked from commit d7da8928ac)

# Conflicts:
#	erpnext/selling/doctype/selling_settings/selling_settings.json

* feat: consider `over_order_allowance` while validating sales order qty

(cherry picked from commit 53701c37b1)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py

* test: add test cases for `Over Order Allowance` against `Blanket Order`

(cherry picked from commit 66f650061d)

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
(cherry picked from commit 8ddbac5158)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-03-22 14:58:10 +05:30
Frappe PR Bot
6e492ec514 chore: release v13 (#34531)
* chore: Update user manual link (#34478)

* chore: Update user manual link (#34478)

(cherry picked from commit be723bb9d4)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflicts

* chore: Update version

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>

* chore: Update version

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>

* fix: Overallocation of 'qty' from Cr Notes to Parent Invoice

Cr Notes 'qty' are overallocated to parent invoice, when there are
mulitple instances of same item in Invoice.

(cherry picked from commit e2f19c6a14)

* refactor: Ignore linked Cr Notes in Report output

(cherry picked from commit d0715a82eb)

* test: Gross Profit report output for Cr notes

2 New test cases added.
1. Standalone Cr notes will be reported as normal Invoices
2. Cr notes against an Invoice will not overallocate qty if there are
multiple instances of same item

(cherry picked from commit cc61daeec4)

* fix: incorrect depr schedules after asset repair [v13] (#34520)

* fix: incorrect schedule after repair for WDV and DD

* chore: only fix schedules for assets with calc_depr true

* fix: incorrect schedule after repair for straight line and manual

* refactor: calc depr in asset repair and if statement (#34526)

refactor: minor asset repair of calc depr and if statement

* fix(client): Amount calculation for 0 qty debit notes (#34455)

fix(client): Amount calculation for 0 qty debit notes (#34455)

fix(client): Amount calculaton for 0 qty debit notes

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
(cherry picked from commit ee6c107d58)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>

* fix: german translations (#34312)

fix: german translations (#34312)

fix: some german translations
(cherry picked from commit 59c2e7ec3e)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>

* fix: exchange gain/loss GL's should be removed if advance is cancelled (#34529)

* fix: report GL for invoice when advance has different exchange rate

If deferred revenue/expense is enabled for any item, don't repost.

* test: cancelling advance should remove exchange gain/loss

If there are no deferred revenue/expense cancelling advance should
cancel the exchange gain/loss booked due to different exchange rates
of payment and its linked invoice

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-03-21 18:20:16 +05:30
Frappe PR Bot
cfcbdfcaec chore(release): Bumped to Version 13.49.5
## [13.49.5](https://github.com/frappe/erpnext/compare/v13.49.4...v13.49.5) (2023-03-14)

### Bug Fixes

* `BOM Stock Report` ([eb1f8f9](eb1f8f932d))
* `required_qty` get reset to `1` for Alternative Item in WO ([d117101](d1171016b3))
* adjust excess cf leaves in new leaves taken if number exceeds cf leaves allocation ([bc12269](bc12269ef4))
* consider leaves taken while calculating expired carry-forwarded leaves ([e74e02b](e74e02b765))
* consider leaves taken within carry-forwarded period separately while calculating balance ([52108d5](52108d52e2))
* consider relieving date while calculating payment days based on lwp ([563f83f](563f83f0f5))
* Don't use get_list & get_all interchangeably ([b70a37f](b70a37f6fa))
* Error in consolidated financial statement ([#34330](https://github.com/frappe/erpnext/issues/34330)) ([470dc10](470dc10b15))
* exclude cancelled leave ledger entries ([91cad9e](91cad9e985))
* exclude carry forwarding leaves while updating leaves after submission ([88c5de5](88c5de533a))
* leave allocation tests ([2f62a96](2f62a9641e))
* linter ([341eab2](341eab2b2a))
* operation time for multi-level BOM in WO ([f4d07cc](f4d07cc84e))
* precision for newly allocated leaves ([238769e](238769e6b5))
* **test:** `get_leave_allocation_records` ([c01bed9](c01bed9862))
* **test:** `TestBomStockReport` ([4824302](4824302811))
* Total row in trail balance report (backport [#34395](https://github.com/frappe/erpnext/issues/34395)) ([#34431](https://github.com/frappe/erpnext/issues/34431)) ([809d6d6](809d6d638e))
* Use customer name instead of name(id) in PSOA (backport [#34412](https://github.com/frappe/erpnext/issues/34412)) ([#34426](https://github.com/frappe/erpnext/issues/34426)) ([a24f050](a24f0507e1))

### Performance Improvements

* `update_completed_qty()` in `material_request.py` ([5bc2b8f](5bc2b8f685))
* Stock Entry (Material Transfer) ([7a159a7](7a159a7187))

### Reverts

* Revert "fix: Default sales team not getting set" (#34376) ([42bda6e](42bda6e37b)), closes [#34376](https://github.com/frappe/erpnext/issues/34376) [#34376](https://github.com/frappe/erpnext/issues/34376) [#34284](https://github.com/frappe/erpnext/issues/34284)
2023-03-14 16:38:26 +00:00
Deepesh Garg
499987040b Merge pull request #34441 from frappe/version-13-hotfix
chore: release v13
2023-03-14 22:06:33 +05:30
Sagar Sharma
28027a9f94 Merge pull request #34449 from frappe/mergify/bp/version-13-hotfix/pr-34415
fix: operation time for multi-level BOM in WO (backport #34415)
2023-03-14 19:25:44 +05:30
Sagar Sharma
b4aabf3f35 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-34415 2023-03-14 18:55:49 +05:30
Sagar Sharma
cf49b0effb Merge pull request #34451 from frappe/mergify/bp/version-13-hotfix/pr-34381
chore: fix french translation (backport #34381)
2023-03-14 18:55:19 +05:30
HENRY Florian
0bb43a1be5 chore: fix french translation (#34381)
chore: update french translation
(cherry picked from commit d267111e13)
2023-03-14 13:20:05 +00:00
s-aga-r
f4d07cc84e fix: operation time for multi-level BOM in WO
(cherry picked from commit 442ee3adba)
2023-03-14 13:18:29 +00:00
mergify[bot]
809d6d638e fix: Total row in trail balance report (backport #34395) (#34431)
fix: Total row in trail balance report (#34395)

* fix: Total row in trail balance report

* fix: Calculate total after preparing opening and closing

(cherry picked from commit c6999fc687)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-14 15:07:30 +05:30
Rucha Mahabal
0834cb1bb6 Merge pull request #34436 from ruchamahabal/fix-leave-balance-editing-v13 2023-03-14 12:57:52 +05:30
Rucha Mahabal
2f62a9641e fix: leave allocation tests 2023-03-14 12:24:56 +05:30
Rucha Mahabal
cdf73bb781 test: update for total leaves allocated post submission 2023-03-14 11:49:32 +05:30
Rucha Mahabal
238769e6b5 fix: precision for newly allocated leaves 2023-03-14 11:49:24 +05:30
Rucha Mahabal
91cad9e985 fix: exclude cancelled leave ledger entries 2023-03-14 11:49:14 +05:30
Rucha Mahabal
7b9784ce10 refactor(tests): create_leave_type usage 2023-03-14 11:49:02 +05:30
Rucha Mahabal
fc10c8e44e test: leaves updated after submission with carry forwarding 2023-03-14 11:48:53 +05:30
Rucha Mahabal
88c5de533a fix: exclude carry forwarding leaves while updating leaves after submission 2023-03-14 11:48:42 +05:30
mergify[bot]
a24f0507e1 fix: Use customer name instead of name(id) in PSOA (backport #34412) (#34426)
fix: Use customer name instead of name(id) in PSOA (#34412)

(cherry picked from commit fa776d2987)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-13 20:29:17 +05:30
mergify[bot]
470dc10b15 fix: Error in consolidated financial statement (#34330)
fix: Error in consolidated financial statement (#34330)

(cherry picked from commit aae53bb910)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-13 14:02:28 +05:30
Saurabh
4d25091196 Merge pull request #34359 from saurabh6790/consider-relieving-date-in-lwp-calculation
fix: consider relieving date while calculating payment days based on lwp
2023-03-13 13:11:28 +05:30
Saurabh
44df522655 chore: fix linter #nosemgrep 2023-03-13 12:43:20 +05:30
Rucha Mahabal
8694d22b7a Merge pull request #34388 from ruchamahabal/fix-balance-edge-cases-v13 2023-03-10 14:39:03 +05:30
Rucha Mahabal
072c7e913d chore: fix linter 2023-03-10 11:44:33 +05:30
Rucha Mahabal
bc12269ef4 fix: adjust excess cf leaves in new leaves taken if number exceeds cf leaves allocation 2023-03-10 11:41:36 +05:30
Rucha Mahabal
7717a8a5e3 test: leave details with application across and after cf leave expiry 2023-03-10 11:41:17 +05:30
Rucha Mahabal
fd5d2ed87f refactor: consider cases for partially consumed cf and new leaves
- the above two cases weren't considering the split between cf leaves taken and new leaves taken and substracting all consumed leaves from cf leaves
2023-03-10 11:29:18 +05:30
Rucha Mahabal
c01bed9862 fix(test): get_leave_allocation_records 2023-03-10 11:29:12 +05:30
Rucha Mahabal
52108d52e2 fix: consider leaves taken within carry-forwarded period separately while calculating balance 2023-03-10 11:29:04 +05:30
Rucha Mahabal
e74e02b765 fix: consider leaves taken while calculating expired carry-forwarded leaves 2023-03-10 11:28:56 +05:30
gavin
b70a37f6fa fix: Don't use get_list & get_all interchangeably
* fix: Fetch all fields via get_returned_qty_map_for_row

* fix(update_billing_percentage): Remove permlevel checks on aggregated
value
2023-03-09 16:05:54 +05:30
Frappe PR Bot
5157f5dd0e chore(release): Bumped to Version 13.49.4
## [13.49.4](https://github.com/frappe/erpnext/compare/v13.49.3...v13.49.4) (2023-03-09)

### Reverts

* Revert "fix: Default sales team not getting set" (#34376) ([b712aea](b712aea3a4)), closes [#34376](https://github.com/frappe/erpnext/issues/34376) [#34376](https://github.com/frappe/erpnext/issues/34376) [#34284](https://github.com/frappe/erpnext/issues/34284)
2023-03-09 10:26:41 +00:00
Deepesh Garg
9ce5d84951 Merge pull request #34380 from frappe/mergify/bp/version-13/pr-34378
Revert "fix: Default sales team not getting set" (backport #34376) (backport #34378)
2023-03-09 15:43:02 +05:30
mergify[bot]
b712aea3a4 Revert "fix: Default sales team not getting set" (#34376)
Revert "fix: Default sales team not getting set" (#34376)

Revert "fix: Default sales team not getting set (#34284)"

This reverts commit 7d0199d743.

(cherry picked from commit 9a8f8e8b7d)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit 42bda6e37b)
2023-03-09 10:12:06 +00:00
mergify[bot]
42bda6e37b Revert "fix: Default sales team not getting set" (#34376)
Revert "fix: Default sales team not getting set" (#34376)

Revert "fix: Default sales team not getting set (#34284)"

This reverts commit 7d0199d743.

(cherry picked from commit 9a8f8e8b7d)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-09 15:40:32 +05:30
Sagar Sharma
eb24f91341 Merge pull request #34369 from frappe/mergify/bp/version-13-hotfix/pr-34360
chore: `Alternative Item Code` error msg (backport #34360)
2023-03-09 13:54:59 +05:30
Saurabh
341eab2b2a fix: linter 2023-03-09 13:24:38 +05:30
Sagar Sharma
0857632359 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-34360 2023-03-09 12:07:45 +05:30
Sagar Sharma
e23f1555bb Merge pull request #34367 from frappe/mergify/bp/version-13-hotfix/pr-34362
fix: `required_qty` get reset to `1` for Alternative Item in WO (backport #34362)
2023-03-09 11:09:06 +05:30
s-aga-r
75c844a15a chore: Alternative Item Code error msg
(cherry picked from commit baef5ae1ef)
2023-03-09 04:38:21 +00:00
s-aga-r
d1171016b3 fix: required_qty get reset to 1 for Alternative Item in WO
(cherry picked from commit 046834a97a)
2023-03-09 04:35:58 +00:00
Saurabh
563f83f0f5 fix: consider relieving date while calculating payment days based on lwp 2023-03-08 19:02:46 +05:30
Sagar Sharma
9844508066 Merge pull request #34357 from frappe/mergify/bp/version-13-hotfix/pr-34352
fix: `BOM Stock Report` (backport #34352)
2023-03-08 18:14:42 +05:30
s-aga-r
4824302811 fix(test): TestBomStockReport 2023-03-08 17:44:51 +05:30
s-aga-r
08b9aaff26 test: add test cases for BOM Stock Report
(cherry picked from commit b53dcb04ed)
2023-03-08 10:56:43 +00:00
s-aga-r
eb1f8f932d fix: BOM Stock Report
(cherry picked from commit a65b80911b)
2023-03-08 10:56:43 +00:00
Frappe PR Bot
07ff956fd8 chore(release): Bumped to Version 13.49.3
## [13.49.3](https://github.com/frappe/erpnext/compare/v13.49.2...v13.49.3) (2023-03-07)

### Performance Improvements

* `update_completed_qty()` in `material_request.py` ([6841e22](6841e22ffe))
* Stock Entry (Material Transfer) ([56a422d](56a422deed))
2023-03-07 17:01:01 +00:00
Sagar Sharma
c575942acf Merge pull request #34341 from frappe/mergify/bp/version-13/pr-34336
perf: Stock Entry (Material Transfer) (backport #34313) (backport #34336)
2023-03-07 22:29:01 +05:30
s-aga-r
6841e22ffe perf: update_completed_qty() in material_request.py
(cherry picked from commit 8ad9e99cea)
(cherry picked from commit 5bc2b8f685)
2023-03-07 16:07:23 +00:00
s-aga-r
56a422deed perf: Stock Entry (Material Transfer)
(cherry picked from commit de18f98c5c)
(cherry picked from commit 7a159a7187)
2023-03-07 16:07:22 +00:00
Sagar Sharma
99b201d5d7 Merge pull request #34336 from frappe/mergify/bp/version-13-hotfix/pr-34313
perf: Stock Entry (Material Transfer) (backport #34313)
2023-03-07 21:34:09 +05:30
s-aga-r
5bc2b8f685 perf: update_completed_qty() in material_request.py
(cherry picked from commit 8ad9e99cea)
2023-03-07 12:33:46 +00:00
s-aga-r
7a159a7187 perf: Stock Entry (Material Transfer)
(cherry picked from commit de18f98c5c)
2023-03-07 12:33:46 +00:00
Frappe PR Bot
0ec34e5880 chore(release): Bumped to Version 13.49.2
## [13.49.2](https://github.com/frappe/erpnext/compare/v13.49.1...v13.49.2) (2023-03-07)

### Bug Fixes

* `rejected_serial_no` not getting copied from PR to PR(Return) ([bb55210](bb55210f49))
* `Serial No is mandatory` even if the `qty` is `0` ([9bea2fc](9bea2fcdfc))
* Default sales team not getting set ([#34284](https://github.com/frappe/erpnext/issues/34284)) ([65c0189](65c0189c4d))
* **minor:** Dirty the form after clicking on Get advances button in Invoices ([#34323](https://github.com/frappe/erpnext/issues/34323)) ([3a1475a](3a1475a90b))
* **test:** check for batch_no in returned dict ([8c5322c](8c5322c1cb))
* UI freeze while selecting batched items in sales invoice ([82a8f2b](82a8f2b1b2))
* Wrap unexpectedly long text in remark ([ba66a67](ba66a6714c))
2023-03-07 10:43:39 +00:00
Deepesh Garg
ba58c7ed59 Merge pull request #34326 from frappe/version-13-hotfix
chore: release v13
2023-03-07 16:11:57 +05:30
mergify[bot]
3a1475a90b fix(minor): Dirty the form after clicking on Get advances button in Invoices (#34323)
fix(minor): Dirty the form after clicking on Get advances button in Invoices (#34323)

fix(minor): Dirty form after clicking on Get advances button

(cherry picked from commit 2feb27e399)

Co-authored-by: Marica <maricadsouza221197@gmail.com>
2023-03-07 15:47:37 +05:30
mergify[bot]
65c0189c4d fix: Default sales team not getting set (#34284)
fix: Default sales team not getting set (#34284)

(cherry picked from commit 7d0199d743)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-07 13:33:55 +05:30
mergify[bot]
1b2c4bf868 chore: add german translations (#34167)
* chore: add german translations (#34167)

* chore: add german translations

* Apply suggestions from code review

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
(cherry picked from commit bbb6a62a7d)

# Conflicts:
#	erpnext/translations/de.csv

* chore: resolve conflicts

---------

Co-authored-by: Patrick Eissler <77415730+PatrickDenis-stack@users.noreply.github.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-03-07 11:46:30 +05:30
ruthra kumar
88ed6e6cb4 Merge pull request #34304 from ruthra-kumar/ui_freeze_on_item_selection
fix: UI freeze while selecting batched items in sales invoice
2023-03-06 11:24:01 +05:30
ruthra kumar
8c5322c1cb fix(test): check for batch_no in returned dict 2023-03-05 20:47:31 +05:30
ruthra kumar
82a8f2b1b2 fix: UI freeze while selecting batched items in sales invoice 2023-03-05 20:47:29 +05:30
Sagar Sharma
3908b510bd Merge pull request #34276 from frappe/mergify/bp/version-13-hotfix/pr-34273
fix: `rejected_serial_no` not getting copied from PR to PR(Return) (backport #34273)
2023-03-04 15:35:00 +05:30
s-aga-r
14547d94b3 chore: conflicts 2023-03-04 15:04:50 +05:30
s-aga-r
9bea2fcdfc fix: Serial No is mandatory even if the qty is 0
(cherry picked from commit cb0b6de4b9)
2023-03-02 07:08:14 +00:00
s-aga-r
bb55210f49 fix: rejected_serial_no not getting copied from PR to PR(Return)
(cherry picked from commit a9f0a11ce6)

# Conflicts:
#	erpnext/controllers/sales_and_purchase_return.py
2023-03-02 07:08:14 +00:00
Frappe PR Bot
178be42369 chore(release): Bumped to Version 13.49.1
## [13.49.1](https://github.com/frappe/erpnext/compare/v13.49.0...v13.49.1) (2023-03-01)

### Bug Fixes

* Wrap unexpectedly long text in remark ([e694550](e6945508f1))
2023-03-01 10:55:31 +00:00
Suraj Shetty
b4e775b264 Merge pull request #34264 from frappe/mergify/bp/version-13/pr-34262
fix(General Ledger): Wrap unexpectedly long word  (backport #34262)
2023-03-01 16:23:56 +05:30
Suraj Shetty
e6945508f1 fix: Wrap unexpectedly long text in remark
(cherry picked from commit ba66a6714c)
2023-03-01 10:53:42 +00:00
Suraj Shetty
5354169f31 Merge pull request #34262 from frappe/fix-general-ledger-report 2023-03-01 16:22:28 +05:30
Suraj Shetty
ba66a6714c fix: Wrap unexpectedly long text in remark 2023-03-01 16:16:58 +05:30
Frappe PR Bot
573cd3c33b chore(release): Bumped to Version 13.49.0
# [13.49.0](https://github.com/frappe/erpnext/compare/v13.48.1...v13.49.0) (2023-02-28)

### Bug Fixes

* conversion factor not set ([59d5797](59d579764d))
* german translations ([#31732](https://github.com/frappe/erpnext/issues/31732)) ([d44da6c](d44da6c820))
* ignore remaining leaves calculation for cf leaves after expiry ([d82ba4e](d82ba4e86f))
* incorrect acc depr amount if multiple FBs with straight line or manual method ([304e6bb](304e6bb996))
* incorrect color in the BOM Stock Report ([e98b346](e98b34617f))
* incorrect leave balance after carry-forwarded leave expiry ([a3a9cd5](a3a9cd5174))
* manual depr schedule ([7176799](71767994a7))
* multiple pos conversion issue resolved ([de631e6](de631e65cc))
* not able to repost gl entries ([2039bd0](2039bd066d))
* **patch:** create only 80G custom fields instead of running the whole setup ([#34183](https://github.com/frappe/erpnext/issues/34183)) ([806f7e5](806f7e5eef))
* permission error while calling get_work_order_items ([3d7b2b1](3d7b2b1a6d))
* pos return throwing amount greater than grand total ([f6607a6](f6607a6050))
* Remove missing DocField in fetch_from ([45645c1](45645c1064))
* set `from_warehouse` and `to_warehouse` while mapping SE ([b1ecca3](b1ecca3a16))
* **test:** use standalone method to fetch work orders from SO ([7971c14](7971c149ed))
* ui freeze on item selection in sales invoice ([d1b611d](d1b611d37f))
* user shouldn't able to make item price for item template ([69f1247](69f1247fab))
* zero division error while making LCV ([91a95ad](91a95adcb6))

### Features

* provision to convert transaction based reposting to item warehouse based reposting ([59c6eb5](59c6eb591b))

### Performance Improvements

* fetch SLE's on demand and memoize ([642692a](642692a040))
2023-02-28 13:29:25 +00:00
ruthra kumar
b6edadb3cb Merge pull request #34239 from frappe/version-13-hotfix
chore: release v13
2023-02-28 18:57:46 +05:30
ruthra kumar
d65df443fc Merge pull request #34246 from frappe/mergify/bp/version-13-hotfix/pr-34241
fix: pos return throwing amount greater than grand total (backport #34241)
2023-02-28 18:33:47 +05:30
ruthra kumar
f6607a6050 fix: pos return throwing amount greater than grand total
(cherry picked from commit 35c70f39fa)
2023-02-28 12:53:30 +00:00
Sagar Sharma
75d98ef205 Merge pull request #34237 from frappe/mergify/bp/version-13-hotfix/pr-34060
fix: multiple Point of Sale conversion issue resolved (backport #34060)
2023-02-28 16:49:46 +05:30
Vishal
fd1d2cd203 chore: minor changes in pos_controller
(cherry picked from commit f18ae5856f)
2023-02-28 09:26:37 +00:00
Vishal
c66dc5658f chore: minor change
(cherry picked from commit a51bec0269)
2023-02-28 09:26:36 +00:00
Vishal
1ebf2dd2bf chore: minor changes added to code
(cherry picked from commit 3ebe7d861d)
2023-02-28 09:26:36 +00:00
Vishal
de631e65cc fix: multiple pos conversion issue resolved
(cherry picked from commit 1de531e56e)
2023-02-28 09:26:36 +00:00
ruthra kumar
02f2844db2 Merge pull request #34218 from frappe/mergify/bp/version-13-hotfix/pr-34207
fix: permission error while calling get_work_order_items (backport #34207)
2023-02-28 11:04:52 +05:30
Frappe PR Bot
c4d9576f9f chore(release): Bumped to Version 13.48.1
## [13.48.1](https://github.com/frappe/erpnext/compare/v13.48.0...v13.48.1) (2023-02-27)

### Bug Fixes

* not able to repost gl entries ([a34aff6](a34aff6f49))
2023-02-27 14:34:43 +00:00
rohitwaghchaure
74303b65cf Merge pull request #34228 from frappe/mergify/bp/version-13/pr-34209
fix: not able to repost gl entries (backport #34206) (backport #34209)
2023-02-27 20:03:15 +05:30
Rohit Waghchaure
a34aff6f49 fix: not able to repost gl entries
(cherry picked from commit 7d10dd9ea8)
(cherry picked from commit 2039bd066d)
2023-02-27 14:08:29 +00:00
Sagar Sharma
f105c1bd5e Merge pull request #34227 from frappe/mergify/bp/version-13-hotfix/pr-34225
fix: set `from_warehouse` and `to_warehouse` while mapping SE (backport #34225)
2023-02-27 17:56:08 +05:30
Sagar Sharma
264c314416 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-34225 2023-02-27 17:32:34 +05:30
Sagar Sharma
a71a336e59 chore: conflicts 2023-02-27 17:32:05 +05:30
Anand Baburajan
7db3645298 Merge pull request #34215 from frappe/mergify/bp/version-13-hotfix/pr-34205
fix: asset manual depr schedule (backport #34205)
2023-02-27 13:31:44 +05:30
s-aga-r
b1ecca3a16 fix: set from_warehouse and to_warehouse while mapping SE
(cherry picked from commit c09a61f360)

# Conflicts:
#	erpnext/stock/doctype/material_request/material_request.py
2023-02-27 07:23:20 +00:00
Anand Baburajan
cbfa188d3d Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-34205 2023-02-27 12:41:45 +05:30
Sagar Sharma
4f7344c278 Merge pull request #34224 from frappe/mergify/bp/version-13-hotfix/pr-34212
fix: Remove missing DocField in fetch_from (backport #34212)
2023-02-27 12:28:55 +05:30
Brian Pond
45645c1064 fix: Remove missing DocField in fetch_from
(cherry picked from commit 83f3e317e1)
2023-02-27 06:24:23 +00:00
mergify[bot]
d44da6c820 fix: german translations (#31732)
fix: german translations (#31732)

(cherry picked from commit 6b510546ae)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-02-27 11:50:12 +05:30
Sagar Sharma
95ea28f14d Merge pull request #34209 from frappe/mergify/bp/version-13-hotfix/pr-34206
fix: not able to repost gl entries (backport #34206)
2023-02-27 10:26:38 +05:30
ruthra kumar
7971c149ed fix(test): use standalone method to fetch work orders from SO
(cherry picked from commit a11d3327df)
2023-02-27 10:14:36 +05:30
ruthra kumar
3d7b2b1a6d fix: permission error while calling get_work_order_items
(cherry picked from commit b6bad728cd)
2023-02-27 10:14:31 +05:30
anandbaburajan
9942a9d40a chore: refactor long if conditions
(cherry picked from commit d56ca011fe)
2023-02-26 14:38:43 +00:00
anandbaburajan
9a607b9bd0 chore: should prepare schedule if not draft
(cherry picked from commit 75386e3653)
2023-02-26 14:38:42 +00:00
anandbaburajan
304e6bb996 fix: incorrect acc depr amount if multiple FBs with straight line or manual method
(cherry picked from commit dda6baea3e)
2023-02-26 14:38:42 +00:00
anandbaburajan
4a557b47d7 chore: handle change in opening_accumulated_depreciation properly
(cherry picked from commit b0d670a51d)
2023-02-26 14:38:42 +00:00
anandbaburajan
71767994a7 fix: manual depr schedule
(cherry picked from commit 971c0720e5)
2023-02-26 14:38:41 +00:00
Rohit Waghchaure
2039bd066d fix: not able to repost gl entries
(cherry picked from commit 7d10dd9ea8)
2023-02-24 15:41:22 +00:00
rohitwaghchaure
b599b93ae8 Merge pull request #34201 from frappe/mergify/bp/version-13-hotfix/pr-34199
fix: conversion factor not set (backport #34199)
2023-02-24 17:52:40 +05:30
Rohit Waghchaure
59d579764d fix: conversion factor not set
(cherry picked from commit 8e46aebc50)
2023-02-24 09:28:37 +00:00
Frappe PR Bot
5f25cea322 chore(release): Bumped to Version 13.48.0
# [13.48.0](https://github.com/frappe/erpnext/compare/v13.47.0...v13.48.0) (2023-02-24)

### Bug Fixes

* incorrect color in the BOM Stock Report ([0490e3b](0490e3bfe6))

### Features

* provision to convert transaction based reposting to item warehouse based reposting ([c8ec365](c8ec365594))
2023-02-24 09:08:54 +00:00
rohitwaghchaure
6a0c24e7b3 Merge pull request #34196 from frappe/mergify/bp/version-13/pr-34178
fix: incorrect color in the BOM Stock Report (backport #34173) (backport #34178)
2023-02-24 14:37:28 +05:30
rohitwaghchaure
8eb6053c97 Merge pull request #34198 from frappe/mergify/bp/version-13/pr-34197
feat: provision to convert transaction based reposting to item wareho… (backport #34115) (backport #34197)
2023-02-24 14:37:14 +05:30
Rohit Waghchaure
c8ec365594 feat: provision to convert transaction based reposting to item warehouse based reposting
(cherry picked from commit f1383b5ef9)
(cherry picked from commit 59c6eb591b)
2023-02-24 06:27:41 +00:00
rohitwaghchaure
0fbd29b16d Merge pull request #34197 from frappe/mergify/bp/version-13-hotfix/pr-34115
feat: provision to convert transaction based reposting to item wareho… (backport #34115)
2023-02-24 11:54:56 +05:30
Rucha Mahabal
806f7e5eef fix(patch): create only 80G custom fields instead of running the whole setup (#34183) 2023-02-24 11:32:46 +05:30
Rohit Waghchaure
59c6eb591b feat: provision to convert transaction based reposting to item warehouse based reposting
(cherry picked from commit f1383b5ef9)
2023-02-24 05:51:56 +00:00
Rohit Waghchaure
0490e3bfe6 fix: incorrect color in the BOM Stock Report
(cherry picked from commit a8f03ebf7f)
(cherry picked from commit e98b34617f)
2023-02-24 05:40:48 +00:00
rohitwaghchaure
0aeef34944 Merge pull request #34191 from frappe/mergify/bp/version-13-hotfix/pr-34189
fix: user shouldn't able to make item price for item template (backport #34189)
2023-02-24 09:23:41 +05:30
Rohit Waghchaure
69f1247fab fix: user shouldn't able to make item price for item template
(cherry picked from commit 6417ae0ee8)
2023-02-23 15:18:42 +00:00
rohitwaghchaure
2d01b72b04 Merge pull request #34178 from frappe/mergify/bp/version-13-hotfix/pr-34173
fix: incorrect color in the BOM Stock Report (backport #34173)
2023-02-23 20:47:07 +05:30
ruthra kumar
bb4c968d95 Merge pull request #34185 from frappe/mergify/bp/version-13-hotfix/pr-34022
perf: Gross Profit report will fetch SLE's on demand and memoize (backport #34022)
2023-02-23 12:54:47 +05:30
ruthra kumar
c40aa580c5 refactor: use docstatus from Delivery Note Item
(cherry picked from commit 88d888d9d0)
2023-02-23 06:26:15 +00:00
ruthra kumar
642692a040 perf: fetch SLE's on demand and memoize
(cherry picked from commit 3e5691072a)
2023-02-23 06:26:15 +00:00
rohitwaghchaure
fd24d52d86 Merge pull request #34182 from frappe/mergify/bp/version-13-hotfix/pr-34172
fix: zero division error while making LCV (backport #34172)
2023-02-23 11:25:24 +05:30
Rohit Waghchaure
91a95adcb6 fix: zero division error while making LCV
(cherry picked from commit 80e94a08cf)
2023-02-23 05:24:56 +00:00
ruthra kumar
fe04b5a2b9 Merge pull request #34179 from frappe/mergify/bp/version-13-hotfix/pr-34176
fix: ui freeze upon item selection in sales invoice (backport #34176)
2023-02-23 10:52:12 +05:30
ruthra kumar
d1b611d37f fix: ui freeze on item selection in sales invoice
(cherry picked from commit 6412583e98)
2023-02-23 05:06:50 +00:00
Rohit Waghchaure
e98b34617f fix: incorrect color in the BOM Stock Report
(cherry picked from commit a8f03ebf7f)
2023-02-23 04:25:45 +00:00
Rucha Mahabal
6391ccd56a Merge pull request #34175 from ruchamahabal/fix-leave-balances 2023-02-22 20:17:13 +05:30
Rucha Mahabal
b848b77815 test: leave details with expired cf leaves 2023-02-22 19:46:08 +05:30
Rucha Mahabal
d82ba4e86f fix: ignore remaining leaves calculation for cf leaves after expiry
- calculate correct cf expiry in the entire allocation period
2023-02-22 19:44:23 +05:30
Rucha Mahabal
aea9d82672 test: leaves allocated before and after cf leave expiry is same 2023-02-22 19:44:06 +05:30
Rucha Mahabal
a3a9cd5174 fix: incorrect leave balance after carry-forwarded leave expiry 2023-02-22 19:43:56 +05:30
Frappe PR Bot
9766827a08 chore(release): Bumped to Version 13.47.0
# [13.47.0](https://github.com/frappe/erpnext/compare/v13.46.1...v13.47.0) (2023-02-21)

### Bug Fixes

* add missing import ([8add12d](8add12d568))
* Amount for debit and credit notes with 0 qty line items (backport [#33902](https://github.com/frappe/erpnext/issues/33902)) ([#34123](https://github.com/frappe/erpnext/issues/34123)) ([2408966](2408966090))
* asset repair status after deletion and asset status after manual depr entry ([922b30a](922b30a566))
* asset_depreciation_and_balances report doesn't reflect manual depr entries ([6227c16](6227c16374))
* check for duplicate in pos closing and pos merge log entry ([92da1ed](92da1ed3c2))
* don't get chart data if data is empty ([acdf7fd](acdf7fd8df))
* **ecommerce:** throw invalid doctype error in shop by category ([#33901](https://github.com/frappe/erpnext/issues/33901)) ([de87786](de87786db4))
* Filters in item-wise sales history report ([#34145](https://github.com/frappe/erpnext/issues/34145)) ([9826245](9826245d8a))
* fiscal year error for existing assets in fixed asset register ([1fb3a28](1fb3a28128))
* opening_accumulated_depreciation and precision in charts ([4f10f48](4f10f48f7c))
* should never get cutomer price on purchase document ([#34002](https://github.com/frappe/erpnext/issues/34002)) ([117dbe3](117dbe38c4)), closes [#33998](https://github.com/frappe/erpnext/issues/33998)
* update `reserved_qty` when `Sales Order` marked as `Hold` ([2391c37](2391c37238))
* Use normal rounding for Tax Withholding Category ([#34114](https://github.com/frappe/erpnext/issues/34114)) ([26ed460](26ed460a4f))
* **ux:** `ReferenceError: me is not defined` Delivery Note ([495d1b2](495d1b2548))

### Features

* **UX:** Add option to disable consolidating leave types in balance reports ([ccd2568](ccd25684f9))
2023-02-21 17:21:05 +00:00
Deepesh Garg
eeaa8b2479 Merge pull request #34160 from frappe/version-13-hotfix
chore: release v13
2023-02-21 22:49:22 +05:30
ruthra kumar
c7093b6e96 Merge pull request #34165 from frappe/mergify/bp/version-13-hotfix/pr-34102
fix: check for duplicate pos invoices in closing entry (backport #34102)
2023-02-21 20:04:02 +05:30
ruthra kumar
92da1ed3c2 fix: check for duplicate in pos closing and pos merge log entry
(cherry picked from commit 47add0b751)
2023-02-21 13:16:32 +00:00
mergify[bot]
9826245d8a fix: Filters in item-wise sales history report (#34145)
fix: Filters in item-wise sales history report (#34145)

(cherry picked from commit c88444a6c4)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-02-21 15:06:21 +05:30
mergify[bot]
26ed460a4f fix: Use normal rounding for Tax Withholding Category (#34114)
fix: Use normal rounding for Tax Withholding Category (#34114)

(cherry picked from commit 35cdd996a9)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-02-21 14:23:16 +05:30
Anand Baburajan
7bed6cddc7 Merge pull request #34156 from frappe/mergify/bp/version-13-hotfix/pr-34153
fix: fiscal year error for existing assets in fixed asset register (backport #34153)
2023-02-21 14:19:45 +05:30
anandbaburajan
1fb3a28128 fix: fiscal year error for existing assets in fixed asset register
(cherry picked from commit 76861eb332)
2023-02-21 08:25:47 +00:00
Rucha Mahabal
c49c621e43 Merge pull request #34150 from ruchamahabal/consolidate-balance-entries 2023-02-21 13:01:32 +05:30
Rucha Mahabal
8add12d568 fix: add missing import 2023-02-21 12:33:47 +05:30
Rucha Mahabal
acdf7fd8df fix: don't get chart data if data is empty 2023-02-21 12:30:52 +05:30
Rucha Mahabal
ccd25684f9 feat(UX): Add option to disable consolidating leave types in balance reports 2023-02-21 12:30:24 +05:30
Sagar Sharma
e34f5c9cf7 Merge pull request #34147 from frappe/mergify/bp/version-13-hotfix/pr-34138
fix(ux): `ReferenceError: me is not defined` Delivery Note (backport #34138)
2023-02-21 10:31:27 +05:30
s-aga-r
bdefd700af chore: Linters
(cherry picked from commit 44ee9f0f19)
2023-02-21 04:57:40 +00:00
s-aga-r
495d1b2548 fix(ux): ReferenceError: me is not defined Delivery Note
(cherry picked from commit 1b010add26)
2023-02-21 04:57:40 +00:00
mergify[bot]
2408966090 fix: Amount for debit and credit notes with 0 qty line items (backport #33902) (#34123)
fix: Amount for debit and credit notes with 0 qty line items (#33902)

(cherry picked from commit 47c91324b1)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-02-19 11:37:44 +05:30
Anand Baburajan
58a006ff64 Merge pull request #34113 from frappe/mergify/bp/version-13-hotfix/pr-34112
fix: repair status after deletion, asset status after manual depr entry and other misc bugs [v14] (backport #34112)
2023-02-17 16:52:28 +05:30
anandbaburajan
3585b90ce5 chore: fixing conflict 2023-02-17 16:29:09 +05:30
anandbaburajan
922b30a566 fix: asset repair status after deletion and asset status after manual depr entry
(cherry picked from commit 03f07a20e7)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.py
2023-02-17 10:44:04 +00:00
Deepesh Garg
5fc68a3dfe Merge pull request #34017 from frappe/mergify/bp/version-13-hotfix/pr-33901
fix(ecommerce): throw invalid doctype error in shop by category (backport #33901)
2023-02-15 16:59:18 +05:30
Frappe PR Bot
4a95c9d642 chore(release): Bumped to Version 13.46.1
## [13.46.1](https://github.com/frappe/erpnext/compare/v13.46.0...v13.46.1) (2023-02-15)

### Bug Fixes

* asset_depreciation_and_balances report doesn't reflect manual depr entries ([62dc68b](62dc68bb57))
* opening_accumulated_depreciation and precision in charts ([6308fca](6308fca587))
2023-02-15 10:33:52 +00:00
Anand Baburajan
f6707b2b92 Merge pull request #34075 from frappe/mergify/bp/version-13/pr-34073
fix: manual depr entries in asset_depreciations_and_balances report and some misc bugs [v14] (backport #34058) (backport #34073)
2023-02-15 16:02:25 +05:30
Sagar Sharma
f6d8adc921 Merge pull request #34068 from frappe/mergify/bp/version-13-hotfix/pr-34002
fix: should never get cutomer price on purchase document (backport #34002)
2023-02-15 16:01:17 +05:30
Sagar Sharma
48f2bd9add Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-34002 2023-02-15 15:22:02 +05:30
anandbaburajan
62dc68bb57 fix: asset_depreciation_and_balances report doesn't reflect manual depr entries
(cherry picked from commit 1535c3d856)
(cherry picked from commit 6227c16374)
2023-02-15 08:36:50 +00:00
anandbaburajan
f80fb97c71 chore: break look if je processed
(cherry picked from commit a220dc0c9c)
(cherry picked from commit ff2e617c0c)
2023-02-15 08:36:50 +00:00
anandbaburajan
6308fca587 fix: opening_accumulated_depreciation and precision in charts
(cherry picked from commit 47cc8ab6c6)
(cherry picked from commit 4f10f48f7c)
2023-02-15 08:36:50 +00:00
Anand Baburajan
fb3a411d1f Merge pull request #34073 from frappe/mergify/bp/version-13-hotfix/pr-34058
fix: manual depr entries in asset_depreciations_and_balances report and some misc bugs [v14] (backport #34058)
2023-02-15 13:43:36 +05:30
anandbaburajan
6227c16374 fix: asset_depreciation_and_balances report doesn't reflect manual depr entries
(cherry picked from commit 1535c3d856)
2023-02-15 07:04:18 +00:00
anandbaburajan
ff2e617c0c chore: break look if je processed
(cherry picked from commit a220dc0c9c)
2023-02-15 07:04:18 +00:00
anandbaburajan
4f10f48f7c fix: opening_accumulated_depreciation and precision in charts
(cherry picked from commit 47cc8ab6c6)
2023-02-15 07:04:17 +00:00
Anand Baburajan
612ceb59c7 Merge pull request #34063 from frappe/mergify/bp/version-13-hotfix/pr-34059
chore: add anand to asset's codeowner (backport #34059)
2023-02-15 11:44:41 +05:30
HENRY Florian
117dbe38c4 fix: should never get cutomer price on purchase document (#34002)
* fix: never get cutomer price on purchase document

chores: syntax

chore: typo in stock_entry get_uom_details (#33998)

fix: typo in stock_entry get_uom_details

chores: syntax

* feat: add test for get_item_detail price list oriented

* feat: add test for get_item_detail price price oriented

* feat: add test for get_item_detail price price oriented

* chore: clean test code

(cherry picked from commit 231fe4156f)
2023-02-15 02:38:50 +00:00
Anand Baburajan
1223e31e7d Update CODEOWNERS 2023-02-14 20:33:40 +05:30
anandbaburajan
bc59ea0d55 chore: add anand to asset's codeowner
(cherry picked from commit d003370f61)

# Conflicts:
#	CODEOWNERS
2023-02-14 14:22:03 +00:00
Sabu Siyad
6ae1cc020a resolve conflicts 2023-02-14 19:34:39 +05:30
Sabu Siyad
4278bfe7b3 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-33901 2023-02-14 19:05:14 +05:30
Sagar Sharma
574791f2c9 Merge pull request #34056 from frappe/mergify/bp/version-13-hotfix/pr-34018
fix: update `reserved_qty` when `Sales Order` marked as `Hold` (backport #34018)
2023-02-14 17:40:40 +05:30
s-aga-r
2391c37238 fix: update reserved_qty when Sales Order marked as Hold
(cherry picked from commit d76759e066)
2023-02-14 11:23:16 +00:00
Frappe PR Bot
ab71a7bba8 chore(release): Bumped to Version 13.46.0
# [13.46.0](https://github.com/frappe/erpnext/compare/v13.45.1...v13.46.0) (2023-02-14)

### Bug Fixes

* `amount` in `Material Request` ([813e8bb](813e8bb664))
* allow PI cancel if linked asset is cancelled ([fbeaabf](fbeaabffc9))
* conflicts ([a9f5be3](a9f5be3f98))
* currency formatting in item-wise sales history ([#33903](https://github.com/frappe/erpnext/issues/33903)) ([f641039](f6410393ce))
* Debit and Credit not equal while submitting PI containing asset item ([#33092](https://github.com/frappe/erpnext/issues/33092)) ([5be4c6f](5be4c6ffbc))
* german chart of accounts "SKR03" ([#33909](https://github.com/frappe/erpnext/issues/33909)) ([b2a3e01](b2a3e014e9))
* incorrect actual qty in Bin ([8f42833](8f42833fba))
* LWP calculation ([c1de4e4](c1de4e4420))
* negative stock error ([2f4ffe1](2f4ffe137e))
* stock entry from item dashboard (stock levels) ([8106c64](8106c64c91))
* **UX:** make Item attachments public by default (backport [#32196](https://github.com/frappe/erpnext/issues/32196)) ([#33949](https://github.com/frappe/erpnext/issues/33949)) ([124d7de](124d7dea1b))
* validate working day list against holidays ([a8ea3ef](a8ea3efae2))

### Features

* Setting to allow Sales Order creation against expired quotation ([#33952](https://github.com/frappe/erpnext/issues/33952)) ([f04542e](f04542eac9))
2023-02-14 10:40:00 +00:00
Deepesh Garg
958a3320e8 Merge pull request #34052 from frappe/version-13-hotfix
chore: release v13
2023-02-14 16:07:17 +05:30
Saurabh
60d2bf939b Merge pull request #34041 from saurabh6790/fix-lwp-calculations
fix: LWP calculation
2023-02-14 15:26:01 +05:30
Saurabh
a8ea3efae2 fix: validate working day list against holidays 2023-02-14 13:14:58 +05:30
Saurabh
c1de4e4420 fix: LWP calculation 2023-02-14 10:52:10 +05:30
mergify[bot]
f04542eac9 feat: Setting to allow Sales Order creation against expired quotation (#33952)
* feat: Setting to allow Sales Order creation against expired quotation (#33952)

* feat: Setting to allow Sales Order creation against expired quotation

* chore: linting issues

(cherry picked from commit 148703bfc2)

# Conflicts:
#	erpnext/selling/doctype/selling_settings/selling_settings.json

* chore: Resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-02-13 11:39:30 +05:30
Sabu Siyad
de87786db4 fix(ecommerce): throw invalid doctype error in shop by category (#33901)
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit 0df28c7174)

# Conflicts:
#	erpnext/www/shop-by-category/index.py
2023-02-12 06:58:27 +00:00
mergify[bot]
f6410393ce fix: currency formatting in item-wise sales history (#33903)
fix: currency formatting in item-wise sales history (#33903)

* fix(item-sales-history): currency formatting

* chore: linting issues

* fix: convert raw sql to qb

(cherry picked from commit 2cc7239dd5)

Co-authored-by: Dany Robert <danyrt@wahni.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-02-11 11:10:58 +05:30
mergify[bot]
5be4c6ffbc fix: Debit and Credit not equal while submitting PI containing asset item (#33092)
fix: Debit and Credit not equal while submitting PI containing asset item

(cherry picked from commit dc8d635120)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-02-11 09:02:06 +05:30
mergify[bot]
124d7dea1b fix(UX): make Item attachments public by default (backport #32196) (#33949)
* fix(UX): make Item attachments public by default (#32196)

(cherry picked from commit fffc245922)

# Conflicts:
#	erpnext/stock/doctype/item/item.json

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-02-10 21:39:33 +05:30
mergify[bot]
bbcdd1e2e2 chore: typo in stock_entry get_uom_details (backport #33998) (#34004)
chore: typo in stock_entry get_uom_details (#33998)

fix: typo in stock_entry get_uom_details
(cherry picked from commit 185c543b73)

Co-authored-by: Akshay <60477442+akshayitzme@users.noreply.github.com>
2023-02-10 20:55:52 +05:30
Anand Baburajan
b6bc29ac92 Merge pull request #33963 from frappe/mergify/bp/version-13-hotfix/pr-33946
fix: allow cancelling purchase invoice if linked asset is already cancelled (backport #33946)
2023-02-10 14:15:44 +05:30
Anand Baburajan
e9a453c430 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-33946 2023-02-08 23:23:31 +05:30
rohitwaghchaure
7e1d5e3595 Merge pull request #33956 from frappe/mergify/bp/version-13-hotfix/pr-33936
fix: negative stock error (backport #33936)
2023-02-07 19:13:35 +05:30
rohitwaghchaure
a9f5be3f98 fix: conflicts 2023-02-07 13:46:40 +05:30
anandbaburajan
d6b0e622ea chore: use continue, not break
(cherry picked from commit 3380dc5dea)
2023-02-05 10:38:00 +00:00
anandbaburajan
fbeaabffc9 fix: allow PI cancel if linked asset is cancelled
(cherry picked from commit b961321de5)
2023-02-05 10:38:00 +00:00
Sagar Sharma
c7c611d929 Merge pull request #33959 from frappe/mergify/bp/version-13-hotfix/pr-33942
fix: stock entry from item dashboard (stock levels) (backport #33942)
2023-02-05 10:06:14 +05:30
s-aga-r
8106c64c91 fix: stock entry from item dashboard (stock levels)
(cherry picked from commit dc0ddf8d7e)
2023-02-05 04:17:35 +00:00
Rohit Waghchaure
bd1191783b test: test case
(cherry picked from commit 9ae7578b07)
2023-02-04 18:06:07 +00:00
Rohit Waghchaure
2f4ffe137e fix: negative stock error
(cherry picked from commit 6d513e2519)

# Conflicts:
#	erpnext/stock/stock_ledger.py
2023-02-04 18:06:07 +00:00
Sagar Sharma
6735b09dd9 Merge pull request #33951 from frappe/mergify/bp/version-13-hotfix/pr-33940
chore: report `Warehouse wise Item Balance Age and Value` (backport #33940)
2023-02-04 18:58:46 +05:30
s-aga-r
745bef8ebc chore: conflicts 2023-02-04 13:51:17 +05:30
s-aga-r
5a9673ae1f chore: add Item Name column in Warehouse wise Item Balance Age and Value report
(cherry picked from commit 56356ffbb9)
2023-02-04 07:59:04 +00:00
s-aga-r
8d0b45b835 chore: column width in Warehouse wise Item Balance Age and Value report
(cherry picked from commit d7a665cb84)

# Conflicts:
#	erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
2023-02-04 07:59:04 +00:00
Frappe PR Bot
6a9660de65 chore(release): Bumped to Version 13.45.1
## [13.45.1](https://github.com/frappe/erpnext/compare/v13.45.0...v13.45.1) (2023-02-01)

### Bug Fixes

* incorrect actual qty in Bin ([e3ad0b1](e3ad0b1655))
2023-02-01 17:46:11 +00:00
rohitwaghchaure
edbbb2469f Merge pull request #33928 from frappe/mergify/bp/version-13/pr-33922
fix: incorrect actual qty in Bin (backport #33918) (backport #33922)
2023-02-01 23:14:23 +05:30
Rohit Waghchaure
e3ad0b1655 fix: incorrect actual qty in Bin
(cherry picked from commit f8c852c54c)
(cherry picked from commit 8f42833fba)
2023-02-01 16:51:50 +00:00
rohitwaghchaure
7738ca1ce0 Merge pull request #33922 from frappe/mergify/bp/version-13-hotfix/pr-33918
fix: incorrect actual qty in Bin (backport #33918)
2023-02-01 22:20:31 +05:30
Rohit Waghchaure
8f42833fba fix: incorrect actual qty in Bin
(cherry picked from commit f8c852c54c)
2023-02-01 12:54:28 +00:00
mergify[bot]
b2a3e014e9 fix: german chart of accounts "SKR03" (#33909)
fix: german chart of accounts "SKR03" (#33909)

* fix: german chart of accounts "SKR03"

- Added some missing account types and tax rates
- Added some missing accounts

* style: convert indentation to tabs

* fix: space before percentage sign

* feat: add some expense accounts

* refactor: replace unicode characters with utf-8

for better readability

* revert: add back groups for Bank and Cash accounts

Removed in 7d0d9c6900

(cherry picked from commit 3c7b460fd8)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-02-01 14:17:59 +05:30
Sagar Sharma
64018c29f3 Merge pull request #33898 from frappe/mergify/bp/version-13-hotfix/pr-33869
fix: `amount` in `Material Request` (backport #33869)
2023-01-31 16:01:49 +05:30
s-aga-r
813e8bb664 fix: amount in Material Request
(cherry picked from commit 6b781d78e0)
2023-01-31 09:15:48 +00:00
Frappe PR Bot
81e4be37ff chore(release): Bumped to Version 13.45.0
# [13.45.0](https://github.com/frappe/erpnext/compare/v13.44.0...v13.45.0) (2023-01-31)

### Bug Fixes

* disposal_was_made_on_original_schedule_date ([939a312](939a3121b7))
* enable customs in Selling Workpace by default ([#33853](https://github.com/frappe/erpnext/issues/33853)) ([54c1642](54c1642e3b))
* Fetch commission rate from sales partner ([#33851](https://github.com/frappe/erpnext/issues/33851)) ([3425a3b](3425a3bef9))
* **gp:** fetch buying amount from dn related to so ([be5edd3](be5edd329f))
* item rate not fetching ([bb56415](bb5641535b))
* manual depr entry not updating asset value [v13] ([#33890](https://github.com/frappe/erpnext/issues/33890)) ([f5efb20](f5efb2057c))
* use correct filter name in `item_query` (backport [#33814](https://github.com/frappe/erpnext/issues/33814)) ([#33817](https://github.com/frappe/erpnext/issues/33817)) ([b38ad66](b38ad66012))

### Features

* **gp:** test for inv and dn related via so ([b72a35a](b72a35a622))

### Performance Improvements

* show update items dialog ([0ff5099](0ff5099cbc))
* Timeout while doing payment reconciliation (v13) ([#33818](https://github.com/frappe/erpnext/issues/33818)) ([4bf3e31](4bf3e310e1))
2023-01-31 06:19:21 +00:00
Deepesh Garg
62edb118eb Merge pull request #33872 from frappe/version-13-hotfix
chore: release v13
2023-01-31 11:47:48 +05:30
Anand Baburajan
f5efb2057c fix: manual depr entry not updating asset value [v13] (#33890)
fix: asset value for manual depr entries
2023-01-31 11:00:06 +05:30
mergify[bot]
3425a3bef9 fix: Fetch commission rate from sales partner (#33851)
* fix: Fetch commission rate from sales partner (#33851)
2023-01-31 10:17:05 +05:30
Deepesh Garg
4bf3e310e1 perf: Timeout while doing payment reconciliation (v13) (#33818)
perf: Timeout while doing payment reconciliation
2023-01-31 09:37:45 +05:30
ruthra kumar
abb466e2fb Merge pull request #33876 from frappe/mergify/bp/version-13-hotfix/pr-33736
fix(gp): fetch buying amount from dn related to so (backport #33736)
2023-01-31 09:29:56 +05:30
HENRY Florian
54c1642e3b fix: enable customs in Selling Workpace by default (#33853)
* fix: enable customs in Selling Workpace by default

* fix: enable customs in Selling Workpace by default
2023-01-31 08:50:34 +05:30
mergify[bot]
b6839d8f51 ci: bump isort to 5.12.0 (backport #33875) (#33880)
ci: bump isort to 5.12.0 (#33875)

[skip ci]

(cherry picked from commit 2bad86d8d8)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-01-31 08:49:28 +05:30
Anand Baburajan
af3ad155e5 Merge pull request #33885 from AnandBaburajan/fix_disposal_was_made_on_original_schedule_date_v13
fix: disposal_was_made_on_original_schedule_date [v13]
2023-01-30 22:55:34 +05:30
anandbaburajan
939a3121b7 fix: disposal_was_made_on_original_schedule_date 2023-01-30 22:10:51 +05:30
Dany Robert
ac6186e16f chore: linting issues
(cherry picked from commit d69c839369)
2023-01-30 13:35:51 +00:00
Dany Robert
b72a35a622 feat(gp): test for inv and dn related via so
(cherry picked from commit 1f6ab86a65)
2023-01-30 13:35:50 +00:00
Dany Robert
0d8a4bf936 chore: linting issue
(cherry picked from commit ef90e24931)
2023-01-30 13:35:50 +00:00
Dany Robert
be5edd329f fix(gp): fetch buying amount from dn related to so
(cherry picked from commit e8e20da78e)
2023-01-30 13:35:50 +00:00
rohitwaghchaure
0902a5c440 Merge pull request #33847 from frappe/mergify/bp/version-13-hotfix/pr-33845
fix: item rate not fetching (backport #33845)
2023-01-28 22:41:06 +05:30
Rohit Waghchaure
bb5641535b fix: item rate not fetching
(cherry picked from commit 0d7f98b496)
2023-01-28 09:41:20 +00:00
Sagar Vora
fc7aac9d41 Merge pull request #33833 from frappe/mergify/bp/version-13-hotfix/pr-33831
perf: show update items dialog (backport #33831)
2023-01-27 04:19:30 +00:00
Devin Slauenwhite
0ff5099cbc perf: show update items dialog
(cherry picked from commit a835c1a418)
2023-01-27 04:18:50 +00:00
mergify[bot]
b38ad66012 fix: use correct filter name in item_query (backport #33814) (#33817)
fix: use correct filter name in `item_query` (#33814)

(cherry picked from commit da323cbb40)

Co-authored-by: Daizy Modi <modidaizy5217@gmail.com>
2023-01-25 17:38:03 +05:30
Frappe PR Bot
71395b9a8e chore(release): Bumped to Version 13.44.0
# [13.44.0](https://github.com/frappe/erpnext/compare/v13.43.2...v13.44.0) (2023-01-25)

### Bug Fixes

* accumulated_depreciation in reverse_depreciation_entry_made_after_disposal ([b7e9e4a](b7e9e4a7c5))
* backport of [#32226](https://github.com/frappe/erpnext/issues/32226) ([d6913ff](d6913fffe6))
* calculate correct amount for qty == 0 (backport [#33739](https://github.com/frappe/erpnext/issues/33739)) ([#33752](https://github.com/frappe/erpnext/issues/33752)) ([d650432](d6504320b1))
* conflicts ([d717ca0](d717ca0325))
* conflicts ([055f853](055f8536c3))
* don't add template item in sales/purchase transaction ([f81d4a7](f81d4a79ea))
* e-Invoicing for SEZ Customer(v13) ([#33796](https://github.com/frappe/erpnext/issues/33796)) ([1b11566](1b11566485))
* **ecommerce:** breadcrumb: fallback to `/all-products` ([#33718](https://github.com/frappe/erpnext/issues/33718)) ([2da543e](2da543ebd4))
* fb issue in asset chart ([ae031ce](ae031cea63))
* incorrect actual qty for the packed item ([09e13d2](09e13d279c))
* incorrect row order and accumulated_depreciation when schedule with multiple FBs is scrapped ([7174a2c](7174a2cd93))
* linter issue ([593d7f3](593d7f3dd6))
* linting ([13906cb](13906cba9a))
* **minor:** Label updates in Statement of Accounts ([#33639](https://github.com/frappe/erpnext/issues/33639)) ([47e500c](47e500c2eb))
* missing constant definition ([fc4be1b](fc4be1b337))
* patch item_reposting_for_incorrect_sl_and_gl ([1c5c067](1c5c06716b))
* rewrite logic for duplicate check in Item Attribute ([4741ce1](4741ce13c6))
* Short closed order, receipt and delivery note status on cancellation ([#33743](https://github.com/frappe/erpnext/issues/33743)) ([3daaa02](3daaa021eb))

### Features

* provision to select date type based on filter ([5ed6a74](5ed6a74fc4))
2023-01-25 04:00:37 +00:00
Deepesh Garg
d3aa37aece Merge pull request #33801 from frappe/version-13-hotfix
chore: release v13
2023-01-25 09:29:03 +05:30
Deepesh Garg
1b11566485 fix: e-Invoicing for SEZ Customer(v13) (#33796) 2023-01-25 08:57:52 +05:30
mergify[bot]
28f5d28201 ci: documentation helper (backport #33757) (#33799)
ci: documentation helper (#33757)

refactor: documentation helper
(cherry picked from commit d155042edd)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-01-24 16:47:13 +05:30
Anand Baburajan
421814e9b3 Merge pull request #33773 from AnandBaburajan/asset_bug_fixes_13
fix: backport #32226 and fix some asset bugs related to finance books [v13]
2023-01-23 21:09:05 +05:30
Anand Baburajan
814333b0cc Merge branch 'version-13-hotfix' into asset_bug_fixes_13 2023-01-23 14:22:46 +05:30
rohitwaghchaure
0862f670ee Merge pull request #33787 from frappe/mergify/bp/version-13-hotfix/pr-33684
feat: [minor] date type based on filter in Work Order Summary report (backport #33684)
2023-01-23 14:16:43 +05:30
rohitwaghchaure
593d7f3dd6 fix: linter issue 2023-01-23 13:22:24 +05:30
Rohit Waghchaure
5ed6a74fc4 feat: provision to select date type based on filter
(cherry picked from commit 20c8873208)
2023-01-23 07:44:08 +00:00
Anand Baburajan
76b6833b61 Merge branch 'version-13-hotfix' into asset_bug_fixes_13 2023-01-22 13:57:31 +05:30
anandbaburajan
b7e9e4a7c5 fix: accumulated_depreciation in reverse_depreciation_entry_made_after_disposal 2023-01-22 13:56:56 +05:30
Sagar Sharma
54b2f78a99 Merge pull request #33780 from frappe/mergify/bp/version-13-hotfix/pr-33778
fix: missing constant definition (backport #33778)
2023-01-21 22:32:59 +05:30
barredterra
fc4be1b337 fix: missing constant definition
(cherry picked from commit 547d37b1db)
2023-01-21 16:59:37 +00:00
Anand Baburajan
b9b110674e Merge branch 'version-13-hotfix' into asset_bug_fixes_13 2023-01-21 20:04:20 +05:30
anandbaburajan
7959e41a81 chore: fix circular import issue and rename date_of_sale to date_of_disposal 2023-01-21 19:58:26 +05:30
anandbaburajan
d6913fffe6 fix: backport of #32226 2023-01-21 19:29:39 +05:30
anandbaburajan
7174a2cd93 fix: incorrect row order and accumulated_depreciation when schedule with multiple FBs is scrapped 2023-01-21 18:41:19 +05:30
mergify[bot]
47e500c2eb fix(minor): Label updates in Statement of Accounts (#33639)
fix(minor): Label updates in Statement of Accounts (#33639)
2023-01-21 15:44:27 +05:30
mergify[bot]
4511d41329 Removed an unnecessary check in code which always evaluates to true (#33710)
fix: removed an unnecessary check which always evaluates to true
2023-01-21 12:03:33 +05:30
rohitwaghchaure
7243f71d7d Merge pull request #33762 from frappe/mergify/bp/version-13-hotfix/pr-33759
fix: incorrect actual qty for the packed item (backport #33759)
2023-01-21 09:46:59 +05:30
mergify[bot]
3daaa021eb fix: Short closed order, receipt and delivery note status on cancellation (#33743)
fix: Short closed order, receipt, and delivery note status on cancellation (#33743)
2023-01-20 23:41:14 +05:30
Rohit Waghchaure
09e13d279c fix: incorrect actual qty for the packed item
(cherry picked from commit 02566a02a8)
2023-01-20 18:10:16 +00:00
mergify[bot]
d6504320b1 fix: calculate correct amount for qty == 0 (backport #33739) (#33752)
fix: calculate correct amount for qty == 0 (#33739)

(cherry picked from commit 327b6fdb32)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-01-20 23:38:37 +05:30
Raffael Meyer
af3a0e56f6 chore: bump python version for docs-checker (#33756) 2023-01-20 22:52:33 +05:30
mergify[bot]
2da543ebd4 fix(ecommerce): breadcrumb: fallback to /all-products (#33718)
fix(ecommerce): breadcrumb: fallback to `/all-products` (#33718)

* fix(ecommerce): breadcrumb: fallback to `/all-products`

* fix(item_group): use `==` instead of `is`

* test(ecommerce): breadcrumb

(cherry picked from commit a94aa7a79f)

Co-authored-by: Sabu Siyad <hello@ssiyad.com>
2023-01-20 19:26:05 +05:30
anandbaburajan
ae031cea63 fix: fb issue in asset chart 2023-01-20 15:39:01 +05:30
rohitwaghchaure
6135d2972e Merge pull request #33742 from frappe/mergify/bp/version-13-hotfix/pr-33695
fix: patch item_reposting_for_incorrect_sl_and_gl (backport #33695)
2023-01-20 14:35:10 +05:30
rohitwaghchaure
d717ca0325 fix: conflicts 2023-01-20 14:05:14 +05:30
Rohit Waghchaure
1c5c06716b fix: patch item_reposting_for_incorrect_sl_and_gl
(cherry picked from commit dbde3a3421)

# Conflicts:
#	erpnext/patches.txt
2023-01-20 06:14:02 +00:00
rohitwaghchaure
61d06dd702 Merge pull request #33732 from frappe/mergify/bp/version-13-hotfix/pr-33723
fix: don't add template item in sales/purchase transaction (backport #33723)
2023-01-20 08:30:41 +05:30
Sagar Sharma
b702a02f61 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-33723 2023-01-19 22:04:05 +05:30
rohitwaghchaure
055f8536c3 fix: conflicts 2023-01-19 16:57:17 +05:30
Sagar Sharma
40ab5b034c Merge pull request #33734 from frappe/mergify/bp/version-13-hotfix/pr-33619
fix: rewrite logic for duplicate check in Item Attribute (backport #33619)
2023-01-19 15:29:17 +05:30
unknown
13906cba9a fix: linting
(cherry picked from commit 2ca4d3fb71)
2023-01-19 07:59:03 +00:00
unknown
4741ce13c6 fix: rewrite logic for duplicate check in Item Attribute
Previously, Item Attribute values were not checked for case-insensitive duplicates, and Item tttribute abbreviations were forced to be uppercase. This commit fixes both problems.

(cherry picked from commit 974e12c837)
2023-01-19 07:59:02 +00:00
Rohit Waghchaure
f81d4a79ea fix: don't add template item in sales/purchase transaction
(cherry picked from commit 2c83fff1a1)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/test_purchase_order.py
2023-01-19 07:13:29 +00:00
Frappe PR Bot
550daf2108 chore(release): Bumped to Version 13.43.2
## [13.43.2](https://github.com/frappe/erpnext/compare/v13.43.1...v13.43.2) (2023-01-17)

### Bug Fixes

* allow to create sales order from expired quotation ([#33582](https://github.com/frappe/erpnext/issues/33582)) ([2f81f15](2f81f15f02))
* asset value in fixed asset register ([#33608](https://github.com/frappe/erpnext/issues/33608)) ([42fe63d](42fe63da2c))
* better comparision of difference value between stock and account ([a450c8d](a450c8dce9))
* don't check other warehouse ledgers to calculate valuation rate ([66bf107](66bf1071bb))
* handle post depr entries fail and fix asset repair link ([5f7dc8a](5f7dc8a5b9))
* only group similar items in print format if group_same_items is checked in pick list (backport [#33627](https://github.com/frappe/erpnext/issues/33627)) ([#33631](https://github.com/frappe/erpnext/issues/33631)) ([7dcf0f0](7dcf0f0866))
* Return against internal purchase invoice ([#33635](https://github.com/frappe/erpnext/issues/33635)) ([eef0f45](eef0f453d2))
* Sales ORder Connections on Material Request ([97488ae](97488aee88))
* Updating SO throws ordered_qty not allowed to change after submission ([a46aa80](a46aa808be))

### Reverts

* Reverting changes done on 33495 ([#33662](https://github.com/frappe/erpnext/issues/33662)) ([0f0a2b1](0f0a2b100c))
2023-01-17 15:36:54 +00:00
Deepesh Garg
959eae1b5c Merge pull request #33702 from frappe/version-13-hotfix
chore: release v13
2023-01-17 21:05:24 +05:30
Anand Baburajan
f2d83b1b21 Merge pull request #33689 from AnandBaburajan/misc_asset_fixes_v13
fix: handle asset depr entries posting failure and fix asset repair link [v13]
2023-01-17 15:14:10 +05:30
Anand Baburajan
f1670e922f Merge branch 'version-13-hotfix' into misc_asset_fixes_v13 2023-01-17 11:21:29 +05:30
Ankush Menat
ec780ac263 chore: ignore b028 2023-01-17 10:59:03 +05:30
Sagar Sharma
847171bd14 Merge pull request #33693 from frappe/mergify/bp/version-13-hotfix/pr-33690
fix: Sales Order Connections Tabs do not show linked Material Request or "+" button  (intoduce by #33304) (backport #33690)
2023-01-17 10:08:17 +05:30
Florian HENRY
97488aee88 fix: Sales ORder Connections on Material Request
(cherry picked from commit e19161a8ee)
2023-01-17 04:37:12 +00:00
Anand Baburajan
2b3a0ba9c4 Merge branch 'version-13-hotfix' into misc_asset_fixes_v13 2023-01-17 00:44:05 +05:30
anandbaburajan
5f7dc8a5b9 fix: handle post depr entries fail and fix asset repair link 2023-01-17 00:39:35 +05:30
ruthra kumar
edc20ae8b8 Merge pull request #33672 from frappe/mergify/bp/version-13-hotfix/pr-33646
Revert "fix: Updating SO throws ordered_qty not allowed to change after submission" (backport #33646)
2023-01-16 15:28:33 +05:30
ruthra kumar
6ebc9c5c82 Merge pull request #33669 from frappe/mergify/bp/version-13-hotfix/pr-33662
revert: Reverting changes done on 33495 (backport #33662)
2023-01-16 14:25:55 +05:30
Sagar Sharma
5a4d92b1bc Merge pull request #33666 from frappe/mergify/bp/version-13-hotfix/pr-33664
chore: `Sales Order` link in `Pick List` (backport #33664)
2023-01-16 13:53:28 +05:30
Sagar Sharma
ff48c44496 Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-33664 2023-01-16 10:30:13 +05:30
mergify[bot]
2f81f15f02 fix: allow to create sales order from expired quotation (#33582)
fix: allow to create sales order from expired quotation (#33582)

(cherry picked from commit dceef0397a)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-01-16 10:02:41 +05:30
ruthra kumar
f1bb8933c1 Revert "fix: Updating SO throws ordered_qty not allowed to change after submission" (#33646)
(cherry picked from commit 333907b7a5)
2023-01-16 04:09:26 +00:00
ruthra kumar
0f0a2b100c revert: Reverting changes done on 33495 (#33662)
'ordered_qty' will not be fetched from `tabBin`

(cherry picked from commit be382054e5)
2023-01-16 04:03:32 +00:00
mergify[bot]
eef0f453d2 fix: Return against internal purchase invoice (#33635)
fix: Return against internal purchase invoice (#33635)

(cherry picked from commit 906ad10d16)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-01-16 08:48:52 +05:30
s-aga-r
e4af69bc93 chore: Sales Order link in Pick List
(cherry picked from commit b3759890d7)
2023-01-15 17:35:52 +00:00
mergify[bot]
42fe63da2c fix: asset value in fixed asset register (#33608)
fix: asset value in fixed asset register

(cherry picked from commit aa1f2a7297)

Co-authored-by: anandbaburajan <anandbaburajan@gmail.com>
2023-01-15 17:33:17 +05:30
ruthra kumar
e23d7aa968 Merge pull request #33629 from frappe/mergify/bp/version-13-hotfix/pr-33622
fix: Updating SO throws ordered_qty not allowed to change after submission (backport #33622)
2023-01-13 10:15:14 +05:30
ruthra kumar
a46aa808be fix: Updating SO throws ordered_qty not allowed to change after submission
(cherry picked from commit 391f42db04)
2023-01-13 08:22:50 +05:30
mergify[bot]
f3b6b4609e chore: reuse doc object in test_pick_list_grouping_before_print (backport #33636) (#33638)
chore: reuse doc object in test_pick_list_grouping_before_print (#33636)

(cherry picked from commit e22d56484d)

Co-authored-by: Ritwik Puri <ritwikpuri5678@gmail.com>
2023-01-13 01:33:46 +05:30
mergify[bot]
7dcf0f0866 fix: only group similar items in print format if group_same_items is checked in pick list (backport #33627) (#33631)
fix: only group similar items in print format if group_same_items is checked in pick list (#33627)

* fix: only group similar items if group same items is checked in pick list

* test: non grouping of locations if group_same_items is false

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
(cherry picked from commit cfb0bb1eaa)

Co-authored-by: Ritwik Puri <ritwikpuri5678@gmail.com>
2023-01-12 20:32:37 +05:30
Sagar Sharma
29dcce53db Merge pull request #33615 from frappe/mergify/bp/version-13-hotfix/pr-33611
fix: better comparision of `difference_value` of Stock and Account (backport #33611)
2023-01-11 15:44:03 +05:30
Smit Vora
a450c8dce9 fix: better comparision of difference value between stock and account
(cherry picked from commit be05aea101)
2023-01-11 09:24:10 +00:00
Sagar Sharma
287411667a Merge pull request #33602 from frappe/mergify/bp/version-13-hotfix/pr-33597
fix: don't check other warehouse ledgers to calculate valuation rate (backport #33597)
2023-01-10 22:58:10 +05:30
Frappe PR Bot
ab30e2a9c7 chore(release): Bumped to Version 13.43.1
## [13.43.1](https://github.com/frappe/erpnext/compare/v13.43.0...v13.43.1) (2023-01-10)

### Bug Fixes

* better handling of duplicate bundle items ([0b952e8](0b952e8bba))
* remove hard-coded roles for populating leave balance reports ([#249](https://github.com/frappe/erpnext/issues/249)) ([#33557](https://github.com/frappe/erpnext/issues/33557)) ([c20d469](c20d469f31))
* remove unnecessary permissions from Appointment and Appointment Booking Settings ([#33468](https://github.com/frappe/erpnext/issues/33468)) ([a50ad1d](a50ad1d292))
2023-01-10 16:51:45 +00:00
Deepesh Garg
65dd72a0b0 Merge pull request #33600 from frappe/version-13-hotfix
chore: release v13
2023-01-10 22:20:15 +05:30
Rohit Waghchaure
66bf1071bb fix: don't check other warehouse ledgers to calculate valuation rate
(cherry picked from commit ef2bf3c223)
2023-01-10 10:05:12 +00:00
Sagar Sharma
410e617834 Merge pull request #33587 from frappe/mergify/bp/version-13-hotfix/pr-33562
fix: better handling of duplicate bundle items (backport #33562)
2023-01-09 22:55:56 +05:30
ruthra kumar
0b952e8bba fix: better handling of duplicate bundle items
(cherry picked from commit c717e87c9e)
2023-01-09 16:54:55 +00:00
mergify[bot]
e9d85a3ee4 ci: bump node in release workflow (backport #33574) (#33576)
ci: bump node in release workflow (#33574)

[skip ci]

(cherry picked from commit 1ad1fc4c7d)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-01-09 13:26:18 +05:30
Rucha Mahabal
c20d469f31 fix: remove hard-coded roles for populating leave balance reports (#249) (#33557) 2023-01-06 14:28:15 +05:30
Sagar Sharma
7d0a118eab Merge pull request #33546 from frappe/mergify/bp/version-13-hotfix/pr-33543
chore: enable `No Copy` attribute for `route` in Item Group (backport #33543)
2023-01-05 13:32:52 +05:30
s-aga-r
2394f64872 chore: enable No Copy attribute for route in Item Group
(cherry picked from commit 348dc32514)
2023-01-05 07:33:04 +00:00
Daizy Modi
a50ad1d292 fix: remove unnecessary permissions from Appointment and Appointment Booking Settings (#33468)
* fix: remove unnecessary permissions from Appointment and Appointment Booking Settings

* fix: remove line

* fix: use more intuitive import

Co-authored-by: Sagar Vora <sagar@resilient.tech>
2023-01-04 21:44:44 +05:30
150 changed files with 3707 additions and 1572 deletions

View File

@@ -66,7 +66,8 @@ ignore =
F841,
E713,
E712,
B023
B023,
B028
max-line-length = 200

View File

@@ -3,52 +3,71 @@ from urllib.parse import urlparse
import requests
docs_repos = [
"frappe_docs",
"erpnext_documentation",
WEBSITE_REPOS = [
"erpnext_com",
"frappe_io",
]
DOCUMENTATION_DOMAINS = [
"docs.erpnext.com",
"frappeframework.com",
]
def uri_validator(x):
result = urlparse(x)
return all([result.scheme, result.netloc, result.path])
def docs_link_exists(body):
for line in body.splitlines():
for word in line.split():
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
parts = parsed_url.path.split('/')
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True
elif parsed_url.netloc == "docs.erpnext.com":
return True
def is_valid_url(url: str) -> bool:
parts = urlparse(url)
return all((parts.scheme, parts.netloc, parts.path))
def is_documentation_link(word: str) -> bool:
if not word.startswith("http") or not is_valid_url(word):
return False
parsed_url = urlparse(word)
if parsed_url.netloc in DOCUMENTATION_DOMAINS:
return True
if parsed_url.netloc == "github.com":
parts = parsed_url.path.split("/")
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS:
return True
return False
def contains_documentation_link(body: str) -> bool:
return any(
is_documentation_link(word)
for line in body.splitlines()
for word in line.split()
)
def check_pull_request(number: str) -> "tuple[int, str]":
response = requests.get(f"https://api.github.com/repos/frappe/erpnext/pulls/{number}")
if not response.ok:
return 1, "Pull Request Not Found! ⚠️"
payload = response.json()
title = (payload.get("title") or "").lower().strip()
head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower()
if (
not title.startswith("feat")
or not head_sha
or "no-docs" in body
or "backport" in body
):
return 0, "Skipping documentation checks... 🏃"
if contains_documentation_link(body):
return 0, "Documentation Link Found. You're Awesome! 🎉"
return 1, "Documentation Link Not Found! ⚠️"
if __name__ == "__main__":
pr = sys.argv[1]
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
if response.ok:
payload = response.json()
title = (payload.get("title") or "").lower().strip()
head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower()
if (title.startswith("feat")
and head_sha
and "no-docs" not in body
and "backport" not in body
):
if docs_link_exists(body):
print("Documentation Link Found. You're Awesome! 🎉")
else:
print("Documentation Link Not Found! ⚠️")
sys.exit(1)
else:
print("Skipping documentation checks... 🏃")
exit_code, message = check_pull_request(sys.argv[1])
print(message)
sys.exit(exit_code)

View File

@@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
python-version: 3.6
python-version: '3.10'
- name: 'Clone repo'
uses: actions/checkout@v2

View File

@@ -13,10 +13,10 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js v14
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
node-version: 18
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
@@ -28,4 +28,4 @@ jobs:
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release
run: npx semantic-release

View File

@@ -32,8 +32,8 @@ repos:
- id: black
additional_dependencies: ['click==8.0.4']
- repo: https://github.com/timothycrosley/isort
rev: 5.9.1
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
exclude: ".*setup.py$"

View File

@@ -4,7 +4,7 @@
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/erpnext_integrations/ @nextchamp-saqib
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar

View File

@@ -4,7 +4,7 @@ import frappe
from erpnext.hooks import regional_overrides
__version__ = "13.43.0"
__version__ = "13.49.6"
def get_default_company(user=None):

View File

@@ -1,38 +1,38 @@
{
"country_code": "de",
"name": "SKR03 mit Kontonummern",
"tree": {
"Aktiva": {
"is_group": 1,
"country_code": "de",
"name": "SKR03 mit Kontonummern",
"tree": {
"Aktiva": {
"is_group": 1,
"root_type": "Asset",
"A - Anlagevermögen": {
"is_group": 1,
"EDV-Software": {
"account_number": "0027",
"account_type": "Fixed Asset"
},
"Gesch\u00e4ftsausstattung": {
"account_number": "0410",
"account_type": "Fixed Asset"
},
"B\u00fcroeinrichtung": {
"account_number": "0420",
"account_type": "Fixed Asset"
},
"Darlehen": {
"account_number": "0565"
},
"Maschinen": {
"account_number": "0210",
"account_type": "Fixed Asset"
},
"Betriebsausstattung": {
"account_number": "0400",
"account_type": "Fixed Asset"
},
"Ladeneinrichtung": {
"account_number": "0430",
"account_type": "Fixed Asset"
"A - Anlagevermögen": {
"is_group": 1,
"EDV-Software": {
"account_number": "0027",
"account_type": "Fixed Asset"
},
"Geschäftsausstattung": {
"account_number": "0410",
"account_type": "Fixed Asset"
},
"Büroeinrichtung": {
"account_number": "0420",
"account_type": "Fixed Asset"
},
"Darlehen": {
"account_number": "0565"
},
"Maschinen": {
"account_number": "0210",
"account_type": "Fixed Asset"
},
"Betriebsausstattung": {
"account_number": "0400",
"account_type": "Fixed Asset"
},
"Ladeneinrichtung": {
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
@@ -60,36 +60,46 @@
"Durchlaufende Posten": {
"account_number": "1590"
},
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
"Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": {
"account_number": "1371"
},
"Abziehbare Vorsteuer": {
"account_type": "Tax",
"is_group": 1,
"Abziehbare Vorsteuer 7%": {
"account_number": "1571"
"Abziehbare Vorsteuer 7 %": {
"account_number": "1571",
"account_type": "Tax",
"tax_rate": 7.0
},
"Abziehbare Vorsteuer 19%": {
"account_number": "1576"
"Abziehbare Vorsteuer 19 %": {
"account_number": "1576",
"account_type": "Tax",
"tax_rate": 19.0
},
"Abziehbare Vorsteuer nach \u00a713b UStG 19%": {
"account_number": "1577"
},
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
"account_number": "3120"
"Abziehbare Vorsteuer nach § 13b UStG 19 %": {
"account_number": "1577",
"account_type": "Tax",
"tax_rate": 19.0
}
}
},
"III. Wertpapiere": {
"is_group": 1
"is_group": 1,
"Anteile an verbundenen Unternehmen (Umlaufvermögen)": {
"account_number": "1340"
},
"Anteile an herrschender oder mit Mehrheit beteiligter Gesellschaft": {
"account_number": "1344"
},
"Sonstige Wertpapiere": {
"account_number": "1348"
}
},
"IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": {
"is_group": 1,
"Kasse": {
"account_type": "Cash",
"is_group": 1,
"account_type": "Cash",
"Kasse": {
"is_group": 1,
"account_number": "1000",
"account_type": "Cash"
}
@@ -111,21 +121,21 @@
"C - Rechnungsabgrenzungsposten": {
"is_group": 1,
"Aktive Rechnungsabgrenzung": {
"account_number": "0980"
"account_number": "0980"
}
},
"D - Aktive latente Steuern": {
"is_group": 1,
"Aktive latente Steuern": {
"account_number": "0983"
"account_number": "0983"
}
},
"E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": {
"is_group": 1
}
},
"Passiva": {
"is_group": 1,
},
"Passiva": {
"is_group": 1,
"root_type": "Liability",
"A. Eigenkapital": {
"is_group": 1,
@@ -200,26 +210,32 @@
},
"Umsatzsteuer": {
"is_group": 1,
"account_type": "Tax",
"Umsatzsteuer 7%": {
"account_number": "1771"
"Umsatzsteuer 7 %": {
"account_number": "1771",
"account_type": "Tax",
"tax_rate": 7.0
},
"Umsatzsteuer 19%": {
"account_number": "1776"
"Umsatzsteuer 19 %": {
"account_number": "1776",
"account_type": "Tax",
"tax_rate": 19.0
},
"Umsatzsteuer-Vorauszahlung": {
"account_number": "1780"
"account_number": "1780",
"account_type": "Tax"
},
"Umsatzsteuer-Vorauszahlung 1/11": {
"account_number": "1781"
},
"Umsatzsteuer \u00a7 13b UStG 19%": {
"account_number": "1787"
"Umsatzsteuer nach § 13b UStG 19 %": {
"account_number": "1787",
"account_type": "Tax",
"tax_rate": 19.0
},
"Umsatzsteuer Vorjahr": {
"account_number": "1790"
},
"Umsatzsteuer fr\u00fchere Jahre": {
"Umsatzsteuer frühere Jahre": {
"account_number": "1791"
}
}
@@ -234,44 +250,56 @@
"E. Passive latente Steuern": {
"is_group": 1
}
},
"Erl\u00f6se u. Ertr\u00e4ge 2/8": {
"is_group": 1,
"root_type": "Income",
"Erl\u00f6skonten 8": {
},
"Erlöse u. Erträge 2/8": {
"is_group": 1,
"root_type": "Income",
"Erlöskonten 8": {
"is_group": 1,
"Erl\u00f6se": {
"account_number": "8200",
"account_type": "Income Account"
},
"Erl\u00f6se USt. 19%": {
"account_number": "8400",
"account_type": "Income Account"
},
"Erl\u00f6se USt. 7%": {
"account_number": "8300",
"account_type": "Income Account"
}
},
"Ertragskonten 2": {
"is_group": 1,
"sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {
"account_number": "2650",
"account_type": "Income Account"
},
"Au\u00dferordentliche Ertr\u00e4ge": {
"account_number": "2500",
"account_type": "Income Account"
},
"Sonstige Ertr\u00e4ge": {
"account_number": "2700",
"account_type": "Income Account"
}
}
},
"Aufwendungen 2/4": {
"is_group": 1,
"Erlöse": {
"account_number": "8200",
"account_type": "Income Account"
},
"Erlöse USt. 19 %": {
"account_number": "8400",
"account_type": "Income Account"
},
"Erlöse USt. 7 %": {
"account_number": "8300",
"account_type": "Income Account"
}
},
"Ertragskonten 2": {
"is_group": 1,
"sonstige Zinsen und ähnliche Erträge": {
"account_number": "2650",
"account_type": "Income Account"
},
"Außerordentliche Erträge": {
"account_number": "2500",
"account_type": "Income Account"
},
"Sonstige Erträge": {
"account_number": "2700",
"account_type": "Income Account"
}
}
},
"Aufwendungen 2/4": {
"is_group": 1,
"root_type": "Expense",
"Fremdleistungen": {
"account_number": "3100",
"account_type": "Expense Account"
},
"Fremdleistungen ohne Vorsteuer": {
"account_number": "3109",
"account_type": "Expense Account"
},
"Bauleistungen eines im Inland ansässigen Unternehmers 19 % Vorsteuer und 19 % Umsatzsteuer": {
"account_number": "3120",
"account_type": "Expense Account"
},
"Wareneingang": {
"account_number": "3200"
},
@@ -298,234 +326,234 @@
"Gegenkonto 4996-4998": {
"account_number": "4999"
},
"Abschreibungen": {
"is_group": 1,
"Abschreibungen": {
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Accumulated Depreciation"
"account_number": "4830",
"account_type": "Accumulated Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",
"account_type": "Depreciation"
"account_number": "4831",
"account_type": "Depreciation"
},
"Abschreibungen auf Kfz": {
"account_number": "4832",
"account_type": "Depreciation"
"account_number": "4832",
"account_type": "Depreciation"
},
"Sofortabschreibung GWG": {
"account_number": "4855",
"account_type": "Expense Account"
"account_number": "4855",
"account_type": "Expense Account"
}
},
"Kfz-Kosten": {
"is_group": 1,
"Kfz-Steuer": {
"account_number": "4510",
"account_type": "Expense Account"
},
"Kfz-Versicherungen": {
"account_number": "4520",
"account_type": "Expense Account"
},
"laufende Kfz-Betriebskosten": {
"account_number": "4530",
"account_type": "Expense Account"
},
"Kfz-Reparaturen": {
"account_number": "4540",
"account_type": "Expense Account"
},
"Fremdfahrzeuge": {
"account_number": "4570",
"account_type": "Expense Account"
},
"sonstige Kfz-Kosten": {
"account_number": "4580",
"account_type": "Expense Account"
}
},
"Personalkosten": {
"is_group": 1,
"Geh\u00e4lter": {
"account_number": "4120",
"account_type": "Expense Account"
},
"gesetzliche soziale Aufwendungen": {
"account_number": "4130",
"account_type": "Expense Account"
},
"Aufwendungen f\u00fcr Altersvorsorge": {
"account_number": "4165",
"account_type": "Expense Account"
},
"Verm\u00f6genswirksame Leistungen": {
"account_number": "4170",
"account_type": "Expense Account"
},
"Aushilfsl\u00f6hne": {
"account_number": "4190",
"account_type": "Expense Account"
}
},
"Raumkosten": {
"is_group": 1,
"Miete und Nebenkosten": {
"account_number": "4210",
"account_type": "Expense Account"
},
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
"account_number": "4240",
"account_type": "Expense Account"
},
"Reinigung": {
"account_number": "4250",
"account_type": "Expense Account"
}
},
"Reparatur/Instandhaltung": {
"is_group": 1,
"Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": {
"account_number": "4805",
"account_type": "Expense Account"
}
},
"Versicherungsbeitr\u00e4ge": {
"is_group": 1,
"Versicherungen": {
"account_number": "4360",
"account_type": "Expense Account"
},
"Beitr\u00e4ge": {
"account_number": "4380",
"account_type": "Expense Account"
},
"sonstige Ausgaben": {
"account_number": "4390",
"account_type": "Expense Account"
},
"steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": {
"account_number": "4396",
"account_type": "Expense Account"
}
},
"Werbe-/Reisekosten": {
"is_group": 1,
"Werbekosten": {
"account_number": "4610",
"account_type": "Expense Account"
},
"Aufmerksamkeiten": {
"account_number": "4653",
"account_type": "Expense Account"
},
"nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": {
"account_number": "4665",
"account_type": "Expense Account"
},
"Reisekosten Unternehmer": {
"account_number": "4670",
"account_type": "Expense Account"
}
},
"verschiedene Kosten": {
"is_group": 1,
"Porto": {
"account_number": "4910",
"account_type": "Expense Account"
},
"Telekom": {
"account_number": "4920",
"account_type": "Expense Account"
},
"Mobilfunk D2": {
"account_number": "4921",
"account_type": "Expense Account"
},
"Internet": {
"account_number": "4922",
"account_type": "Expense Account"
},
"B\u00fcrobedarf": {
"account_number": "4930",
"account_type": "Expense Account"
},
"Zeitschriften, B\u00fccher": {
"account_number": "4940",
"account_type": "Expense Account"
},
"Fortbildungskosten": {
"account_number": "4945",
"account_type": "Expense Account"
},
"Buchf\u00fchrungskosten": {
"account_number": "4955",
"account_type": "Expense Account"
},
"Abschlu\u00df- u. Pr\u00fcfungskosten": {
"account_number": "4957",
"account_type": "Expense Account"
},
"Nebenkosten des Geldverkehrs": {
"account_number": "4970",
"account_type": "Expense Account"
},
"Werkzeuge und Kleinger\u00e4te": {
"account_number": "4985",
"account_type": "Expense Account"
}
},
"Zinsaufwendungen": {
"is_group": 1,
"Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": {
"account_number": "2110",
"account_type": "Expense Account"
},
"Zinsaufwendungen f\u00fcr KFZ Finanzierung": {
"account_number": "2121",
"account_type": "Expense Account"
}
}
},
"Anfangsbestand 9": {
"is_group": 1,
"root_type": "Equity",
"Saldenvortragskonten": {
"is_group": 1,
"Saldenvortrag Sachkonten": {
"account_number": "9000"
},
"Saldenvortr\u00e4ge Debitoren": {
"account_number": "9008"
},
"Saldenvortr\u00e4ge Kreditoren": {
"account_number": "9009"
}
}
},
"Privatkonten 1": {
"is_group": 1,
"root_type": "Equity",
"Privatentnahmen/-einlagen": {
"is_group": 1,
"Privatentnahme allgemein": {
"account_number": "1800"
},
"Privatsteuern": {
"account_number": "1810"
},
"Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": {
"account_number": "1820"
},
"Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": {
"account_number": "1830"
},
"Au\u00dfergew\u00f6hnliche Belastungen": {
"account_number": "1850"
},
"Privateinlagen": {
"account_number": "1890"
}
}
}
}
},
"Kfz-Kosten": {
"is_group": 1,
"Kfz-Steuer": {
"account_number": "4510",
"account_type": "Expense Account"
},
"Kfz-Versicherungen": {
"account_number": "4520",
"account_type": "Expense Account"
},
"laufende Kfz-Betriebskosten": {
"account_number": "4530",
"account_type": "Expense Account"
},
"Kfz-Reparaturen": {
"account_number": "4540",
"account_type": "Expense Account"
},
"Fremdfahrzeuge": {
"account_number": "4570",
"account_type": "Expense Account"
},
"sonstige Kfz-Kosten": {
"account_number": "4580",
"account_type": "Expense Account"
}
},
"Personalkosten": {
"is_group": 1,
"Gehälter": {
"account_number": "4120",
"account_type": "Expense Account"
},
"gesetzliche soziale Aufwendungen": {
"account_number": "4130",
"account_type": "Expense Account"
},
"Aufwendungen für Altersvorsorge": {
"account_number": "4165",
"account_type": "Expense Account"
},
"Vermögenswirksame Leistungen": {
"account_number": "4170",
"account_type": "Expense Account"
},
"Aushilfslöhne": {
"account_number": "4190",
"account_type": "Expense Account"
}
},
"Raumkosten": {
"is_group": 1,
"Miete und Nebenkosten": {
"account_number": "4210",
"account_type": "Expense Account"
},
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
"account_number": "4240",
"account_type": "Expense Account"
},
"Reinigung": {
"account_number": "4250",
"account_type": "Expense Account"
}
},
"Reparatur/Instandhaltung": {
"is_group": 1,
"Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": {
"account_number": "4805",
"account_type": "Expense Account"
}
},
"Versicherungsbeiträge": {
"is_group": 1,
"Versicherungen": {
"account_number": "4360",
"account_type": "Expense Account"
},
"Beiträge": {
"account_number": "4380",
"account_type": "Expense Account"
},
"sonstige Ausgaben": {
"account_number": "4390",
"account_type": "Expense Account"
},
"steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": {
"account_number": "4396",
"account_type": "Expense Account"
}
},
"Werbe-/Reisekosten": {
"is_group": 1,
"Werbekosten": {
"account_number": "4610",
"account_type": "Expense Account"
},
"Aufmerksamkeiten": {
"account_number": "4653",
"account_type": "Expense Account"
},
"nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": {
"account_number": "4665",
"account_type": "Expense Account"
},
"Reisekosten Unternehmer": {
"account_number": "4670",
"account_type": "Expense Account"
}
},
"verschiedene Kosten": {
"is_group": 1,
"Porto": {
"account_number": "4910",
"account_type": "Expense Account"
},
"Telekom": {
"account_number": "4920",
"account_type": "Expense Account"
},
"Mobilfunk D2": {
"account_number": "4921",
"account_type": "Expense Account"
},
"Internet": {
"account_number": "4922",
"account_type": "Expense Account"
},
"Bürobedarf": {
"account_number": "4930",
"account_type": "Expense Account"
},
"Zeitschriften, Bücher": {
"account_number": "4940",
"account_type": "Expense Account"
},
"Fortbildungskosten": {
"account_number": "4945",
"account_type": "Expense Account"
},
"Buchführungskosten": {
"account_number": "4955",
"account_type": "Expense Account"
},
"Abschluß- u. Prüfungskosten": {
"account_number": "4957",
"account_type": "Expense Account"
},
"Nebenkosten des Geldverkehrs": {
"account_number": "4970",
"account_type": "Expense Account"
},
"Werkzeuge und Kleingeräte": {
"account_number": "4985",
"account_type": "Expense Account"
}
},
"Zinsaufwendungen": {
"is_group": 1,
"Zinsaufwendungen für kurzfristige Verbindlichkeiten": {
"account_number": "2110",
"account_type": "Expense Account"
},
"Zinsaufwendungen für KFZ Finanzierung": {
"account_number": "2121",
"account_type": "Expense Account"
}
}
},
"Anfangsbestand 9": {
"is_group": 1,
"root_type": "Equity",
"Saldenvortragskonten": {
"is_group": 1,
"Saldenvortrag Sachkonten": {
"account_number": "9000"
},
"Saldenvorträge Debitoren": {
"account_number": "9008"
},
"Saldenvorträge Kreditoren": {
"account_number": "9009"
}
}
},
"Privatkonten 1": {
"is_group": 1,
"root_type": "Equity",
"Privatentnahmen/-einlagen": {
"is_group": 1,
"Privatentnahme allgemein": {
"account_number": "1800"
},
"Privatsteuern": {
"account_number": "1810"
},
"Sonderausgaben beschränkt abzugsfähig": {
"account_number": "1820"
},
"Sonderausgaben unbeschränkt abzugsfähig": {
"account_number": "1830"
},
"Außergewöhnliche Belastungen": {
"account_number": "1850"
},
"Privateinlagen": {
"account_number": "1890"
}
}
}
}
}

View File

@@ -87,6 +87,7 @@ class JournalEntry(AccountsController):
self.check_credit_limit()
self.make_gl_entries()
self.update_advance_paid()
self.update_asset_value()
self.update_expense_claim()
self.update_inter_company_jv()
self.update_invoice_discounting()
@@ -235,6 +236,29 @@ class JournalEntry(AccountsController):
for d in to_remove:
self.remove(d)
def update_asset_value(self):
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if (
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
continue
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
asset.set_status()
def update_inter_company_jv(self):
if (
self.voucher_type == "Inter Company Journal Entry"
@@ -293,19 +317,38 @@ class JournalEntry(AccountsController):
d.db_update()
def unlink_asset_reference(self):
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if d.reference_type == "Asset" and d.reference_name:
if (
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
for s in asset.get("schedules"):
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
idx = cint(s.finance_book_id) or 1
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
if asset.calculate_depreciation:
for s in asset.get("schedules"):
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
asset.set_status()
idx = cint(s.finance_book_id) or 1
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
asset.set_status()
break
else:
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
asset.set_status()
def unlink_inter_company_jv(self):
if (

View File

@@ -7,7 +7,7 @@ from functools import reduce
import frappe
from frappe import ValidationError, _, scrub, throw
from frappe.utils import cint, comma_or, flt, getdate, nowdate
from frappe.utils import cint, comma_or, flt, get_link_to_form, getdate, nowdate
from six import iteritems, string_types
import erpnext
@@ -168,8 +168,31 @@ class PaymentEntry(AccountsController):
for reference in self.references:
if reference.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
doc = frappe.get_doc(reference.reference_doctype, reference.reference_name)
repost_required = False
for adv_reference in doc.get("advances"):
if adv_reference.exchange_gain_loss != 0:
repost_required = True
break
if repost_required:
for item in doc.get("items"):
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
frappe.msgprint(
_(
"Linked Invoice {0} has Exchange Gain/Loss GL entries due to this Payment. Submit a Journal manually to reverse its effects."
).format(get_link_to_form(doc.doctype, doc.name))
)
repost_required = False
doc.delink_advance_entries(self.name)
if repost_required:
doc.reload()
doc.docstatus = 2
doc.make_gl_entries()
doc.docstatus = 1
doc.make_gl_entries()
def set_missing_values(self):
if self.payment_type == "Internal Transfer":
for field in (
@@ -1255,6 +1278,7 @@ def get_outstanding_reference_documents(args):
args.get("party_type"),
args.get("party"),
args.get("party_account"),
args.get("company"),
filters=args,
condition=condition,
)

View File

@@ -211,7 +211,7 @@ class PaymentReconciliation(Document):
condition += " and cost_center = '{0}' ".format(self.cost_center)
non_reconciled_invoices = get_outstanding_invoices(
self.party_type, self.party, self.receivable_payable_account, condition=condition
self.party_type, self.party, self.receivable_payable_account, self.company, condition=condition
)
if self.invoice_limit:

View File

@@ -21,8 +21,24 @@ class POSClosingEntry(StatusUpdater):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_duplicate_pos_invoices()
self.validate_pos_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
for idx, inv in enumerate(self.pos_transactions, 1):
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
error_list = []
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:

View File

@@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(
abs(available_batch_qty - reserved_batch_qty - item.qty)
abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
)
bold_invalid_batch_no = frappe.bold(item.batch_no)
@@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice):
).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"),
)
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
frappe.throw(
_(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
),
title=_("Item Unavailable"),
)
elif is_stock_item and flt(available_stock) < flt(d.qty):
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@@ -652,7 +652,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
max_available_bundles = available_qty / item.stock_qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):

View File

@@ -18,6 +18,22 @@ class POSInvoiceMergeLog(Document):
def validate(self):
self.validate_customer()
self.validate_pos_invoice_status()
self.validate_duplicate_pos_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
for idx, inv in enumerate(self.pos_invoices, 1):
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
error_list = []
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_customer(self):
if self.merge_invoices_based_on == "Customer Group":
@@ -427,6 +443,8 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
if closing_entry:
closing_entry.set_status(update=True, status="Failed")
if type(error_message) == list:
error_message = frappe.json.dumps(error_message)
closing_entry.db_set("error_message", error_message)
raise

View File

@@ -15,7 +15,7 @@
</div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
<h5 style="float: right;">
{{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}}
@@ -49,7 +49,6 @@
<br>
{% endif %}
{{ _("Against") }}: {{ row.against }}
<br>{{ _("Remarks") }}: {{ row.remarks }}
{% if row.bill_no %}
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}

View File

@@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document):
def validate(self):
if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.name }}"
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body:
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
@@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True):
"account": [doc.account] if doc.account else None,
"party_type": "Customer",
"party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency,
"group_by": doc.group_by,
"currency": doc.currency,
@@ -155,7 +156,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
]
return frappe.get_list(
"Customer",
fields=["name", "email_id"],
fields=["name", "customer_name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]],
)
@@ -178,7 +179,7 @@ def get_customers_based_on_sales_person(sales_person):
if sales_person_records.get("Customer"):
return frappe.get_list(
"Customer",
fields=["name", "email_id"],
fields=["name", "customer_name", "email_id"],
filters=[["name", "in", list(sales_person_records["Customer"])]],
)
else:
@@ -227,7 +228,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == "Sales Partner":
customers = frappe.get_list(
"Customer",
fields=["name", "email_id"],
fields=["name", "customer_name", "email_id"],
filters=[["default_sales_partner", "=", collection_name]],
)
else:
@@ -244,7 +245,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
continue
customer_list.append(
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
{
"name": customer.name,
"customer_name": customer.customer_name,
"primary_email": primary_email,
"billing_email": billing_email,
}
)
return customer_list

View File

@@ -1,12 +1,12 @@
{
"actions": [],
"allow_workflow": 1,
"creation": "2020-08-03 16:35:21.852178",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
"customer_name",
"billing_email",
"primary_email"
],
@@ -30,11 +30,18 @@
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Billing Email"
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-08-03 22:55:38.875601",
"modified": "2023-03-13 00:12:34.508086",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts Customer",
@@ -43,5 +50,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -598,7 +598,7 @@ class PurchaseInvoice(BuyingController):
def make_supplier_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
# because rounded_total had value even before introduction of posting GLE based on rounded total
grand_total = (
self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
)
@@ -799,10 +799,7 @@ class PurchaseInvoice(BuyingController):
else item.deferred_expense_account
)
if not item.is_fixed_asset:
dummy, amount = self.get_amount_and_base_amount(item, None)
else:
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
dummy, amount = self.get_amount_and_base_amount(item, None)
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:

View File

@@ -1078,7 +1078,7 @@ var select_loyalty_program = function(frm, loyalty_programs) {
]
});
dialog.set_primary_action(__("Set"), function() {
dialog.set_primary_action(__("Set Loyalty Program"), function() {
dialog.hide();
return frappe.call({
method: "frappe.client.set_value",

View File

@@ -1790,6 +1790,8 @@
"width": "50%"
},
{
"fetch_from": "sales_partner.commission_rate",
"fetch_if_empty": 1,
"fieldname": "commission_rate",
"fieldtype": "Float",
"hide_days": 1,
@@ -2045,7 +2047,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2022-09-16 17:44:22.227332",
"modified": "2023-01-28 19:45:47.538163",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2101,4 +2103,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -7,17 +7,7 @@ from frappe import _, msgprint, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.utils import (
add_days,
add_months,
cint,
cstr,
flt,
formatdate,
get_link_to_form,
getdate,
nowdate,
)
from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
from six import iteritems
import erpnext
@@ -33,10 +23,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center,
get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain,
make_depreciation_entry,
reset_depreciation_schedule,
reverse_depreciation_entry_made_after_disposal,
)
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.selling_controller import SellingController
@@ -1114,18 +1106,20 @@ class SalesInvoice(SellingController):
asset = self.get_asset(item)
if self.is_return:
if asset.calculate_depreciation:
self.reverse_depreciation_entry_made_after_sale(asset)
self.reset_depreciation_schedule(asset)
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset, item.base_net_amount, item.finance_book
)
asset.db_set("disposal_date", None)
if asset.calculate_depreciation:
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
reset_depreciation_schedule(asset, self.posting_date)
else:
if asset.calculate_depreciation:
self.depreciate_asset(asset)
depreciate_asset(asset, self.posting_date)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset, item.base_net_amount, item.finance_book
@@ -1193,95 +1187,6 @@ class SalesInvoice(SellingController):
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
)
def depreciate_asset(self, asset):
asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(date_of_sale=self.posting_date)
asset.save()
make_depreciation_entry(asset.name, self.posting_date)
asset.load_from_db()
def reset_depreciation_schedule(self, asset):
asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset
asset.prepare_depreciation_data(date_of_return=self.posting_date)
self.modify_depreciation_schedule_for_asset_repairs(asset)
asset.save()
asset.load_from_db()
def modify_depreciation_schedule_for_asset_repairs(self, asset):
asset_repairs = frappe.get_all(
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
)
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data()
def reverse_depreciation_entry_made_after_sale(self, asset):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
row = -1
finance_book = asset.get("schedules")[0].get("finance_book")
for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
else:
row += 1
if schedule.schedule_date == posting_date_of_original_invoice:
if not self.sale_was_made_on_original_schedule_date(
asset, schedule, row, posting_date_of_original_invoice
) or self.sale_happens_in_the_future(posting_date_of_original_invoice):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
asset.finance_books[0].value_after_depreciation += depreciation_amount
asset.save()
def get_posting_date_of_sales_invoice(self):
return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def sale_was_made_on_original_schedule_date(
self, asset, schedule, row, posting_date_of_original_invoice
):
for finance_book in asset.get("finance_books"):
if schedule.finance_book == finance_book.finance_book:
orginal_schedule_date = add_months(
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
)
if orginal_schedule_date == posting_date_of_original_invoice:
return True
return False
def sale_happens_in_the_future(self, posting_date_of_original_invoice):
if posting_date_of_original_invoice > getdate():
return True
return False
def get_depreciation_amount_in_je(self, journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):

View File

@@ -1117,6 +1117,46 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def test_bin_details_of_packed_item(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import make_item
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
bundle_item.append(
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
)
bundle_item.save(ignore_permissions=True)
make_item("_Packed Item New 1", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
si = create_sales_invoice(
item_code="_Test Product Bundle Item New",
update_stock=1,
warehouse="_Test Warehouse - _TC",
transaction_date=add_days(nowdate(), -1),
do_not_submit=1,
)
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
bin_details = frappe.db.get_value(
"Bin",
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
["actual_qty", "projected_qty", "ordered_qty"],
as_dict=1,
)
si.transaction_date = nowdate()
si.save()
packed_item = si.packed_items[0]
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
def test_pos_si_without_payment(self):
make_pos_profile()
@@ -3430,6 +3470,78 @@ class TestSalesInvoice(unittest.TestCase):
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled
)
def test_gain_loss_on_advance_cancellation(self):
unlink_enabled = frappe.db.get_single_value(
"Accounts Settings", "unlink_payment_on_cancellation_of_invoice"
)
frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice", 1)
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
"payment_type": "Receive",
"party_type": "Customer",
"party": "_Test Customer USD",
"company": "_Test Company",
"paid_from_account_currency": "USD",
"paid_to_account_currency": "INR",
"source_exchange_rate": 70,
"target_exchange_rate": 1,
"reference_no": "1",
"reference_date": nowdate(),
"received_amount": 70,
"paid_amount": 1,
"paid_from": "_Test Receivable USD - _TC",
"paid_to": "_Test Cash - _TC",
}
)
pe.insert()
pe.submit()
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=75,
do_not_save=1,
rate=1,
)
si = si.save()
si.append(
"advances",
{
"reference_type": "Payment Entry",
"reference_name": pe.name,
"advance_amount": 1,
"allocated_amount": 1,
"ref_exchange_rate": 70,
},
)
si.save()
si.submit()
expected_gle = [
["_Test Receivable USD - _TC", 75.0, 5.0],
["Exchange Gain/Loss - _TC", 5.0, 0.0],
["Sales - _TC", 0.0, 75.0],
]
check_gl_entries(self, si.name, expected_gle, nowdate())
# cancel advance payment
pe.reload()
pe.cancel()
expected_gle_after = [
["_Test Receivable USD - _TC", 75.0, 0.0],
["Sales - _TC", 0.0, 75.0],
]
check_gl_entries(self, si.name, expected_gle_after, nowdate())
frappe.db.set_single_value(
"Accounts Settings", "unlink_payment_on_cancellation_of_invoice", unlink_enabled
)
def test_batch_expiry_for_sales_invoice_return(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -256,7 +256,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
tax_amount = normal_round(tax_amount)
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
@@ -555,3 +555,20 @@ def is_valid_certificate(
valid = True
return valid
def normal_round(number):
"""
Rounds a number to the nearest integer.
:param number: The number to round.
"""
decimal_part = number - int(number)
if decimal_part >= 0.5:
decimal_part = 1
else:
decimal_part = 0
number = int(number) + decimal_part
return number

View File

@@ -135,6 +135,34 @@ def get_assets(filters):
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
gle.debit
else
0
end), 0) as depreciation_amount_during_the_period
from `tabGL Entry` gle
join `tabAsset` a on
gle.against_voucher = a.name
join `tabAsset Category Account` aca on
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
0

View File

@@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
for data in [asset_data, liability_data, equity_data]:
if data:
account_name = get_root_account_name(data[0].root_type, company)
opening_value += get_opening_balance(account_name, data, company) or 0.0
if account_name:
opening_value += get_opening_balance(account_name, data, company) or 0.0
opening_balance[company] = opening_value
@@ -155,7 +156,7 @@ def get_opening_balance(account_name, data, company):
def get_root_account_name(root_type, company):
return frappe.get_all(
root_account = frappe.get_all(
"Account",
fields=["account_name"],
filters={
@@ -165,7 +166,10 @@ def get_root_account_name(root_type, company):
"parent_account": ("is", "not set"),
},
as_list=1,
)[0][0]
)
if root_account:
return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters):

View File

@@ -378,15 +378,14 @@ class Deferred_Revenue_and_Expense_Report(object):
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"})
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)
for idx, period in enumerate(self.period_list, 0):
total_row[period.key] = self.period_total[idx].total
ret.append(total_row)
return ret

View File

@@ -25,8 +25,8 @@
<thead>
<tr>
<th style="width: 12%">{%= __("Date") %}</th>
<th style="width: 15%">{%= __("Ref") %}</th>
<th style="width: 25%">{%= __("Party") %}</th>
<th style="width: 15%">{%= __("Reference") %}</th>
<th style="width: 25%">{%= __("Remarks") %}</th>
<th style="width: 15%">{%= __("Debit") %}</th>
<th style="width: 15%">{%= __("Credit") %}</th>
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
@@ -38,23 +38,28 @@
{% if(data[i].posting_date) { %}
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
<td>{%= data[i].voucher_type %}
<br>{%= data[i].voucher_no %}</td>
<td>
<br>{%= data[i].voucher_no %}
</td>
{% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
<td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
<span>
{% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %}
<br>
{% } %}
{{ __("Against") }}: {%= data[i].against %}
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
{% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
</td>
<td style="text-align: right">
{%= format_currency(data[i].debit, filters.presentation_currency) %}</td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency) %}</td>
</span>
</td>
<td style="text-align: right">
{%= format_currency(data[i].debit, filters.presentation_currency) %}
</td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency) %}
</td>
{% } else { %}
<td></td>
<td></td>

View File

@@ -8,6 +8,7 @@ from frappe.query_builder import Order
from frappe.utils import cint, flt
from erpnext.controllers.queries import get_match_cond
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
from erpnext.stock.utils import get_incoming_rate
@@ -364,6 +365,7 @@ def get_column_names():
class GrossProfitGenerator(object):
def __init__(self, filters=None):
self.sle = {}
self.data = []
self.average_buying_rate = {}
self.filters = frappe._dict(filters)
@@ -373,7 +375,6 @@ class GrossProfitGenerator(object):
if filters.group_by == "Invoice":
self.group_items_by_invoice()
self.load_stock_ledger_entries()
self.load_product_bundle()
self.load_non_stock_items()
self.get_returned_invoice_items()
@@ -465,7 +466,14 @@ class GrossProfitGenerator(object):
):
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
row.qty += flt(returned_item_row.qty)
# returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
returned_item_row.qty = 0
else:
row.qty = 0
returned_item_row.qty += row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
if flt(row.qty) or row.base_amount:
@@ -563,7 +571,7 @@ class GrossProfitGenerator(object):
return flt(row.qty) * item_rate
else:
my_sle = self.sle.get((item_code, row.warehouse))
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent
if row.dn_detail:
@@ -581,14 +589,36 @@ class GrossProfitGenerator(object):
dn["item_row"],
dn["warehouse"],
)
my_sle = self.sle.get((item_code, warehouse))
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
elif row.sales_order and row.so_detail:
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
if incoming_amount:
return incoming_amount
else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
return 0.0
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
frappe.qb.from_(delivery_note_item)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
.where(delivery_note_item.docstatus == 1)
.where(delivery_note_item.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail)
.groupby(delivery_note_item.item_code)
)
incoming_amount = query.run()
return flt(incoming_amount[0][0]) if incoming_amount else 0
def get_average_buying_rate(self, row, item_code):
args = row
@@ -642,6 +672,19 @@ class GrossProfitGenerator(object):
if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s"
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
if self.filters.item_group:
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
if self.filters.sales_person:
conditions += """
and exists(select 1
from `tabSales Team` st
where st.parent = `tabSales Invoice`.name
and st.sales_person = %(sales_person)s)
"""
if self.filters.group_by == "Sales Person":
sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives"
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
@@ -665,7 +708,8 @@ class GrossProfitGenerator(object):
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.dn_detail,
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail,
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
@@ -814,24 +858,36 @@ class GrossProfitGenerator(object):
"Item", item_code, ["item_name", "description", "item_group", "brand"]
)
def load_stock_ledger_entries(self):
res = frappe.db.sql(
"""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry`
where company=%(company)s and is_cancelled = 0
order by
item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""",
self.filters,
as_dict=True,
)
self.sle = {}
for r in res:
if (r.item_code, r.warehouse) not in self.sle:
self.sle[(r.item_code, r.warehouse)] = []
def get_stock_ledger_entries(self, item_code, warehouse):
if item_code and warehouse:
if (item_code, warehouse) not in self.sle:
sle = qb.DocType("Stock Ledger Entry")
res = (
qb.from_(sle)
.select(
sle.item_code,
sle.voucher_type,
sle.voucher_no,
sle.voucher_detail_no,
sle.stock_value,
sle.warehouse,
sle.actual_qty.as_("qty"),
)
.where(
(sle.company == self.filters.company)
& (sle.item_code == item_code)
& (sle.warehouse == warehouse)
& (sle.is_cancelled == 0)
)
.orderby(sle.item_code)
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
.run(as_dict=True)
)
self.sle[(r.item_code, r.warehouse)].append(r)
self.sle[(item_code, warehouse)] = res
return self.sle[(item_code, warehouse)]
return []
def load_product_bundle(self):
self.product_bundles = {}

View File

@@ -301,3 +301,161 @@ class TestGrossProfit(FrappeTestCase):
columns, data = execute(filters=filters)
self.assertGreater(len(data), 0)
def test_order_connected_dn_and_inv(self):
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
"""
Test gp calculation when invoice and delivery note aren't directly connected.
SO -- INV
|
DN
"""
se = make_stock_entry(
company=self.company,
item_code=self.item,
target=self.warehouse,
qty=3,
basic_rate=100,
do_not_submit=True,
)
item = se.items[0]
se.append(
"items",
{
"item_code": item.item_code,
"s_warehouse": item.s_warehouse,
"t_warehouse": item.t_warehouse,
"qty": 10,
"basic_rate": 200,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"cost_center": item.cost_center,
"expense_account": item.expense_account,
},
)
se = se.save().submit()
so = make_sales_order(
customer=self.customer,
company=self.company,
warehouse=self.warehouse,
item=self.item,
qty=4,
do_not_save=False,
do_not_submit=False,
)
from erpnext.selling.doctype.sales_order.sales_order import (
make_delivery_note,
make_sales_invoice,
)
make_delivery_note(so.name).submit()
sinv = make_sales_invoice(so.name).submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 4.0,
"avg._selling_rate": 100.0,
"valuation_rate": 125.0,
"selling_amount": 400.0,
"buying_amount": 500.0,
"gross_profit": -100.0,
"gross_profit_%": -25.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])
def test_crnote_against_invoice_with_multiple_instances_of_same_item(self):
"""
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
"""
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
# Invoice with an item added twice
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False))
sinv = sinv.save().submit()
# Create Credit Note for Invoice
cr_note = make_sales_return(sinv.name)
cr_note = cr_note.save().submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 0.0,
"avg._selling_rate": 0.0,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
# Both items of Invoice should have '0' qty
self.assertEqual(len(gp_entry), 2)
self.assertDictContainsSubset(expected_entry, gp_entry[0])
self.assertDictContainsSubset(expected_entry, gp_entry[1])
def test_standalone_cr_notes(self):
"""
Standalone cr notes will be reported as usual
"""
# Make Cr Note
sinv = self.create_sales_invoice(
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
sinv.is_return = 1
sinv = sinv.save().submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": -1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])

View File

@@ -78,7 +78,6 @@ def validate_filters(filters):
def get_data(filters):
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
@@ -118,12 +117,10 @@ def get_data(filters):
ignore_closing_entries=not flt(filters.with_period_closing_entry),
)
total_row = calculate_values(
accounts, gl_entries_by_account, opening_balances, filters, company_currency
)
calculate_values(accounts, gl_entries_by_account, opening_balances)
accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency)
data = prepare_data(accounts, filters, parent_children_map, company_currency)
data = filter_out_zero_value_rows(
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
)
@@ -218,7 +215,7 @@ def get_rootwise_opening_balances(filters, report_type):
return opening
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency):
def calculate_values(accounts, gl_entries_by_account, opening_balances):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -228,22 +225,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
"closing_credit": 0.0,
}
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
"warn_if_negative": True,
"opening_debit": 0.0,
"opening_credit": 0.0,
"debit": 0.0,
"credit": 0.0,
"closing_debit": 0.0,
"closing_credit": 0.0,
"parent_account": None,
"indent": 0,
"has_value": True,
"currency": company_currency,
}
for d in accounts:
d.update(init.copy())
@@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
prepare_opening_closing(d)
for field in value_fields:
total_row[field] += d[field]
def calculate_total_row(accounts, company_currency):
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
"warn_if_negative": True,
"opening_debit": 0.0,
"opening_credit": 0.0,
"debit": 0.0,
"credit": 0.0,
"closing_debit": 0.0,
"closing_credit": 0.0,
"parent_account": None,
"indent": 0,
"has_value": True,
"currency": company_currency,
}
for d in accounts:
if not d.parent_account:
for field in value_fields:
total_row[field] += d[field]
return total_row
@@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
accounts_by_name[d.parent_account][key] += d[key]
def prepare_data(accounts, filters, total_row, parent_children_map, company_currency):
def prepare_data(accounts, filters, parent_children_map, company_currency):
data = []
for d in accounts:
@@ -305,6 +306,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
row["has_value"] = has_value
data.append(row)
total_row = calculate_total_row(accounts, company_currency)
data.extend([{}, total_row])
return data

View File

@@ -840,7 +840,7 @@ def remove_return_pos_invoices(party_type, party, invoice_list):
return invoice_list
def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
def get_outstanding_invoices(party_type, party, account, company, condition=None, filters=None):
outstanding_invoices = []
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
@@ -892,61 +892,73 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
invoice_list = remove_return_pos_invoices(party_type, party, invoice_list)
payment_entries = frappe.db.sql(
"""
select against_voucher_type, against_voucher,
ifnull(sum({payment_dr_or_cr}), 0) as payment_amount
from `tabGL Entry`
where party_type = %(party_type)s and party = %(party)s
and account = %(account)s
and {payment_dr_or_cr} > 0
and against_voucher is not null and against_voucher != ''
and is_cancelled=0
group by against_voucher_type, against_voucher
""".format(
payment_dr_or_cr=payment_dr_or_cr
),
{"party_type": party_type, "party": party, "account": account},
as_dict=True,
)
if invoice_list:
invoices = [d.voucher_no for d in invoice_list]
payment_entries = frappe.db.sql(
"""
select against_voucher_type, against_voucher,
ifnull(sum({payment_dr_or_cr}), 0) as payment_amount
from `tabGL Entry`
where
company = %(company)s
and party_type = %(party_type)s and party = %(party)s
and account = %(account)s
and {payment_dr_or_cr} > 0
and ifnull(against_voucher, '') != ''
and is_cancelled=0
and against_voucher in %(invoices)s
group by against_voucher_type, against_voucher
""".format(
payment_dr_or_cr=payment_dr_or_cr,
),
{
"company": company,
"party_type": party_type,
"party": party,
"account": account,
"invoices": invoices,
},
as_dict=True,
)
pe_map = frappe._dict()
for d in payment_entries:
pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount)
pe_map = frappe._dict()
for d in payment_entries:
pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount)
for d in invoice_list:
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
if outstanding_amount > 0.5 / (10**precision):
if (
filters
and filters.get("outstanding_amt_greater_than")
and not (
outstanding_amount >= filters.get("outstanding_amt_greater_than")
and outstanding_amount <= filters.get("outstanding_amt_less_than")
)
):
continue
if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices:
outstanding_invoices.append(
frappe._dict(
{
"voucher_no": d.voucher_no,
"voucher_type": d.voucher_type,
"posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount),
"payment_amount": payment_amount,
"outstanding_amount": outstanding_amount,
"due_date": d.due_date,
"currency": d.currency,
}
for d in invoice_list:
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
if outstanding_amount > 0.5 / (10**precision):
if (
filters
and filters.get("outstanding_amt_greater_than")
and not (
outstanding_amount >= filters.get("outstanding_amt_greater_than")
and outstanding_amount <= filters.get("outstanding_amt_less_than")
)
)
):
continue
if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices:
outstanding_invoices.append(
frappe._dict(
{
"voucher_no": d.voucher_no,
"voucher_type": d.voucher_type,
"posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount),
"payment_amount": payment_amount,
"outstanding_amount": outstanding_amount,
"due_date": d.due_date,
"currency": d.currency,
}
)
)
outstanding_invoices = sorted(
outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate())
)
outstanding_invoices = sorted(
outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate())
)
return outstanding_invoices

View File

@@ -132,6 +132,10 @@ frappe.ui.form.on('Asset', {
}, __("Manage"));
}
if (frm.doc.depr_entry_posting_status === "Failed") {
frm.trigger("set_depr_posting_failure_alert");
}
frm.trigger("setup_chart");
}
@@ -142,6 +146,19 @@ frappe.ui.form.on('Asset', {
}
},
set_depr_posting_failure_alert: function (frm) {
const alert = `
<div class="row">
<div class="col-xs-12 col-sm-6">
<span class="indicator whitespace-nowrap red">
<span>Failed to post depreciation entries</span>
</span>
</div>
</div>`;
frm.dashboard.set_headline_alert(alert);
},
toggle_reference_doc: function(frm) {
if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) {
frm.set_df_property('purchase_invoice', 'read_only', 1);
@@ -184,39 +201,58 @@ frappe.ui.form.on('Asset', {
})
},
setup_chart: function(frm) {
var x_intervals = [frm.doc.purchase_date];
var asset_values = [frm.doc.gross_purchase_amount];
var last_depreciation_date = frm.doc.purchase_date;
if(frm.doc.opening_accumulated_depreciation) {
last_depreciation_date = frappe.datetime.add_months(frm.doc.next_depreciation_date,
-1*frm.doc.frequency_of_depreciation);
x_intervals.push(last_depreciation_date);
asset_values.push(flt(frm.doc.gross_purchase_amount) -
flt(frm.doc.opening_accumulated_depreciation));
setup_chart: async function(frm) {
if(frm.doc.finance_books.length > 1) {
return
}
$.each(frm.doc.schedules || [], function(i, v) {
x_intervals.push(v.schedule_date);
var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount);
if(v.journal_entry) {
last_depreciation_date = v.schedule_date;
asset_values.push(asset_value);
} else {
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
asset_values.push(null);
} else {
asset_values.push(asset_value)
}
var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: 'Date' })];
var asset_values = [frm.doc.gross_purchase_amount];
if(frm.doc.calculate_depreciation) {
if(frm.doc.opening_accumulated_depreciation) {
var depreciation_date = frappe.datetime.add_months(
frm.doc.finance_books[0].depreciation_start_date,
-1 * frm.doc.finance_books[0].frequency_of_depreciation
);
x_intervals.push(frappe.format(depreciation_date, { fieldtype: 'Date' }));
asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
}
});
$.each(frm.doc.schedules || [], function(i, v) {
x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' }));
var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount'));
if(v.journal_entry) {
asset_values.push(asset_value);
} else {
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
asset_values.push(null);
} else {
asset_values.push(asset_value);
}
}
});
} else {
if(frm.doc.opening_accumulated_depreciation) {
x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' }));
asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
}
let depr_entries = (await frappe.call({
method: "get_manual_depreciation_entries",
doc: frm.doc,
})).message;
$.each(depr_entries || [], function(i, v) {
x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' }));
let last_asset_value = asset_values[asset_values.length - 1]
asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount')));
});
}
if(in_list(["Scrapped", "Sold"], frm.doc.status)) {
x_intervals.push(frm.doc.disposal_date);
x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: 'Date' }));
asset_values.push(0);
last_depreciation_date = frm.doc.disposal_date;
}
frm.dashboard.render_graph({
@@ -260,10 +296,6 @@ frappe.ui.form.on('Asset', {
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
},
opening_accumulated_depreciation: function(frm) {
erpnext.asset.set_accumulated_depreciation(frm);
},
make_schedules_editable: function(frm) {
if (frm.doc.finance_books) {
var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
@@ -483,19 +515,23 @@ frappe.ui.form.on('Depreciation Schedule', {
},
depreciation_amount: function(frm, cdt, cdn) {
erpnext.asset.set_accumulated_depreciation(frm);
erpnext.asset.set_accumulated_depreciation(frm, locals[cdt][cdn].finance_book_id);
}
})
});
erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return;
erpnext.asset.set_accumulated_depreciation = function(frm, finance_book_id) {
var depreciation_method = frm.doc.finance_books[Number(finance_book_id) - 1].depreciation_method;
if(depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name,
"accumulated_depreciation_amount", accumulated_depreciation);
if (row.finance_book_id === finance_book_id) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
};
})
};

View File

@@ -68,6 +68,7 @@
"column_break_51",
"purchase_receipt_amount",
"default_finance_book",
"depr_entry_posting_status",
"amended_from"
],
"fields": [
@@ -473,6 +474,16 @@
"fieldname": "section_break_36",
"fieldtype": "Section Break",
"label": "Finance Books"
},
{
"fieldname": "depr_entry_posting_status",
"fieldtype": "Select",
"hidden": 1,
"label": "Depreciation Entry Posting Status",
"no_copy": 1,
"options": "\nSuccessful\nFailed",
"print_hide": 1,
"read_only": 1
}
],
"idx": 72,
@@ -487,15 +498,21 @@
{
"group": "Repair",
"link_doctype": "Asset Repair",
"link_fieldname": "asset_name"
"link_fieldname": "asset"
},
{
"group": "Value",
"link_doctype": "Asset Value Adjustment",
"link_fieldname": "asset"
},
{
"group": "Journal Entry",
"link_doctype": "Journal Entry",
"link_fieldname": "reference_name",
"table_fieldname": "accounts"
}
],
"modified": "2022-07-20 16:22:44.437579",
"modified": "2023-01-31 01:03:09.467817",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -27,6 +27,7 @@ from erpnext.accounts.general_ledger import make_reverse_gl_entries
from erpnext.assets.doctype.asset.depreciation import (
get_depreciation_accounts,
get_disposal_account_and_cost_center,
is_last_day_of_the_month,
)
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.controllers.accounts_controller import AccountsController
@@ -79,18 +80,59 @@ class Asset(AccountsController):
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
)
def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None):
def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None):
if self.calculate_depreciation:
self.value_after_depreciation = 0
self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_sale)
self.set_accumulated_depreciation(date_of_sale, date_of_return)
if self.should_prepare_depreciation_schedule():
self.make_depreciation_schedule(date_of_disposal)
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
else:
self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation
)
def should_prepare_depreciation_schedule(self):
if not self.get("schedules"):
return True
old_asset_doc = self.get_doc_before_save()
if not old_asset_doc:
return True
have_asset_details_been_modified = (
old_asset_doc.gross_purchase_amount != self.gross_purchase_amount
or old_asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
or old_asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
)
if have_asset_details_been_modified:
return True
manual_fb_idx = -1
for d in self.finance_books:
if d.depreciation_method == "Manual":
manual_fb_idx = d.idx - 1
no_manual_depr_or_have_manual_depr_details_been_modified = (
manual_fb_idx == -1
or old_asset_doc.finance_books[manual_fb_idx].total_number_of_depreciations
!= self.finance_books[manual_fb_idx].total_number_of_depreciations
or old_asset_doc.finance_books[manual_fb_idx].frequency_of_depreciation
!= self.finance_books[manual_fb_idx].frequency_of_depreciation
or old_asset_doc.finance_books[manual_fb_idx].depreciation_start_date
!= getdate(self.finance_books[manual_fb_idx].depreciation_start_date)
or old_asset_doc.finance_books[manual_fb_idx].expected_value_after_useful_life
!= self.finance_books[manual_fb_idx].expected_value_after_useful_life
)
if no_manual_depr_or_have_manual_depr_details_been_modified:
return True
return False
def validate_item(self):
item = frappe.get_cached_value(
"Item", self.item_code, ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1
@@ -223,10 +265,8 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
def make_depreciation_schedule(self, date_of_sale):
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
"schedules"
):
def make_depreciation_schedule(self, date_of_disposal):
if not self.get("schedules"):
self.schedules = []
if not self.available_for_use_date:
@@ -279,17 +319,17 @@ class Asset(AccountsController):
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
# if asset is being sold
if date_of_sale:
if date_of_disposal:
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
finance_book, depreciation_amount, from_date, date_of_disposal
)
if depreciation_amount > 0:
self.append(
"schedules",
{
"schedule_date": date_of_sale,
"schedule_date": date_of_disposal,
"depreciation_amount": depreciation_amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
@@ -340,12 +380,19 @@ class Asset(AccountsController):
value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the 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
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
)
and (
not self.flags.increase_in_asset_value_due_to_repair
or not finance_book.depreciation_method in ("Written Down Value", "Double Declining Balance")
)
or value_after_depreciation < finance_book.expected_value_after_useful_life
):
depreciation_amount += (
value_after_depreciation - finance_book.expected_value_after_useful_life
@@ -364,6 +411,9 @@ class Asset(AccountsController):
},
)
if len(self.get("finance_books")) > 1 and any(start):
self.sort_depreciation_schedule()
# 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):
@@ -399,6 +449,14 @@ class Asset(AccountsController):
return start
def sort_depreciation_schedule(self):
self.schedules = sorted(
self.schedules, key=lambda s: (int(s.finance_book_id), getdate(s.schedule_date))
)
for idx, s in enumerate(self.schedules, 1):
s.idx = idx
def get_from_date(self, finance_book):
if not self.get("schedules"):
return self.available_for_use_date
@@ -531,11 +589,9 @@ class Asset(AccountsController):
return True
def set_accumulated_depreciation(
self, date_of_sale=None, date_of_return=None, ignore_booked_entry=False
self, date_of_disposal=None, date_of_return=None, ignore_booked_entry=False
):
straight_line_idx = [
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
]
straight_line_idx = []
finance_books = []
for i, d in enumerate(self.get("schedules")):
@@ -543,8 +599,16 @@ class Asset(AccountsController):
continue
if int(d.finance_book_id) not in finance_books:
straight_line_idx = [
s.idx
for s in self.get("schedules")
if s.finance_book_id == d.finance_book_id
and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual")
]
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
value_after_depreciation = flt(
self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation
)
finance_books.append(int(d.finance_book_id))
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
@@ -554,7 +618,7 @@ class Asset(AccountsController):
if (
straight_line_idx
and i == max(straight_line_idx) - 1
and not date_of_sale
and not date_of_disposal
and not date_of_return
):
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
@@ -569,9 +633,6 @@ class Asset(AccountsController):
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
)
def get_value_after_depreciation(self, idx):
return flt(self.get("finance_books")[cint(idx) - 1].value_after_depreciation)
def validate_expected_value_after_useful_life(self):
for row in self.get("finance_books"):
accumulated_depreciation_after_full_schedule = [
@@ -626,15 +687,20 @@ class Asset(AccountsController):
movement.cancel()
def delete_depreciation_entries(self):
for d in self.get("schedules"):
if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
d.db_set("journal_entry", None)
if self.calculate_depreciation:
for d in self.get("schedules"):
if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
else:
depr_entries = self.get_manual_depreciation_entries()
self.db_set(
"value_after_depreciation",
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
)
for depr_entry in depr_entries or []:
frappe.get_doc("Journal Entry", depr_entry.name).cancel()
self.db_set(
"value_after_depreciation",
(flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)),
)
def set_status(self, status=None):
"""Get and update status"""
@@ -651,11 +717,15 @@ class Asset(AccountsController):
if self.journal_entry_for_scrap:
status = "Scrapped"
elif self.finance_books:
idx = self.get_default_finance_book_idx() or 0
else:
expected_value_after_useful_life = 0
value_after_depreciation = self.value_after_depreciation
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation
if self.calculate_depreciation:
idx = self.get_default_finance_book_idx() or 0
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation
if flt(value_after_depreciation) <= expected_value_after_useful_life:
status = "Fully Depreciated"
@@ -665,6 +735,19 @@ class Asset(AccountsController):
status = "Cancelled"
return status
def get_value_after_depreciation(self, finance_book=None):
if not self.calculate_depreciation:
return flt(self.value_after_depreciation, self.precision("gross_purchase_amount"))
if not finance_book:
return flt(
self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount")
)
for row in self.get("finance_books"):
if finance_book == row.finance_book:
return flt(row.value_after_depreciation, self.precision("gross_purchase_amount"))
def get_default_finance_book_idx(self):
if not self.get("default_finance_book") and self.company:
self.default_finance_book = erpnext.get_default_finance_book(self.company)
@@ -790,6 +873,25 @@ class Asset(AccountsController):
make_gl_entries(gl_entries)
self.db_set("booked_fixed_asset", 1)
@frappe.whitelist()
def get_manual_depreciation_entries(self):
(_, _, depreciation_expense_account) = get_depreciation_accounts(self)
gle = frappe.qb.DocType("GL Entry")
records = (
frappe.qb.from_(gle)
.select(gle.voucher_no.as_("name"), gle.debit.as_("value"), gle.posting_date)
.where(gle.against_voucher == self.name)
.where(gle.account == depreciation_expense_account)
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.orderby(gle.posting_date)
.orderby(gle.creation)
).run(as_dict=True)
return records
@frappe.whitelist()
def get_depreciation_rate(self, args, on_validate=False):
if isinstance(args, string_types):
@@ -838,7 +940,6 @@ def update_maintenance_status():
def make_post_gl_entry():
asset_categories = frappe.db.get_all("Asset Category", fields=["name", "enable_cwip_accounting"])
for asset_category in asset_categories:
@@ -991,7 +1092,7 @@ def make_journal_entry(asset_name):
depreciation_expense_account,
) = get_depreciation_accounts(asset)
depreciation_cost_center, depreciation_series = frappe.db.get_value(
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
@@ -1058,6 +1159,13 @@ def is_cwip_accounting_enabled(asset_category):
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting"))
@frappe.whitelist()
def get_asset_value_after_depreciation(asset_name, finance_book=None):
asset = frappe.get_doc("Asset", asset_name)
return asset.get_value_after_depreciation(finance_book)
def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1)
@@ -1067,26 +1175,24 @@ def get_total_days(date, frequency):
return date_diff(date, period_start_date)
def is_last_day_of_the_month(date):
last_day_of_the_month = get_last_day(date)
return getdate(last_day_of_the_month) == getdate(date)
@erpnext.allow_regional
def get_depreciation_amount(asset, depreciable_value, row):
if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
depreciation_amount = (
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
depreciation_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
elif asset.flags.increase_in_asset_value_due_to_repair:
depreciation_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being prepared for the first time
else:
depreciation_amount = (
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))

View File

@@ -4,7 +4,17 @@
import frappe
from frappe import _
from frappe.utils import cint, flt, getdate, today
from frappe.utils import (
add_months,
cint,
flt,
get_last_day,
get_link_to_form,
getdate,
nowdate,
today,
)
from frappe.utils.user import get_users_with_role
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
@@ -20,9 +30,22 @@ def post_depreciation_entries(date=None):
if not date:
date = today()
for asset in get_depreciable_assets(date):
make_depreciation_entry(asset, date)
frappe.db.commit()
failed_asset_names = []
for asset_name in get_depreciable_assets(date):
try:
make_depreciation_entry(asset_name, date)
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
failed_asset_names.append(asset_name)
if failed_asset_names:
set_depr_entry_posting_status_for_failed_assets(failed_asset_names)
notify_depr_entry_posting_error(failed_asset_names)
frappe.db.commit()
def get_depreciable_assets(date):
@@ -121,6 +144,8 @@ def make_depreciation_entry(asset_name, date=None):
finance_books.value_after_depreciation -= d.depreciation_amount
finance_books.db_update()
asset.db_set("depr_entry_posting_status", "Successful")
asset.set_status()
return asset
@@ -184,6 +209,42 @@ def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation
return credit_account, debit_account
def set_depr_entry_posting_status_for_failed_assets(failed_asset_names):
for asset_name in failed_asset_names:
frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Failed")
def notify_depr_entry_posting_error(failed_asset_names):
recipients = get_users_with_role("Accounts Manager")
if not recipients:
recipients = get_users_with_role("System Manager")
subject = _("Error while posting depreciation entries")
asset_links = get_comma_separated_asset_links(failed_asset_names)
message = (
_("Hi,")
+ "<br>"
+ _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
+ "."
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
def get_comma_separated_asset_links(asset_names):
asset_links = []
for asset_name in asset_names:
asset_links.append(get_link_to_form("Asset", asset_name))
asset_links = ", ".join(asset_links)
return asset_links
@frappe.whitelist()
def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
@@ -195,6 +256,11 @@ def scrap_asset(asset_name):
_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)
)
date = today()
depreciate_asset(asset, date)
asset.reload()
depreciation_series = frappe.get_cached_value(
"Company", asset.company, "series_for_depreciation_entry"
)
@@ -202,7 +268,7 @@ def scrap_asset(asset_name):
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Journal Entry"
je.naming_series = depreciation_series
je.posting_date = today()
je.posting_date = date
je.company = asset.company
je.remark = "Scrap Entry for asset {0}".format(asset_name)
@@ -213,7 +279,7 @@ def scrap_asset(asset_name):
je.flags.ignore_permissions = True
je.submit()
frappe.db.set_value("Asset", asset_name, "disposal_date", today())
frappe.db.set_value("Asset", asset_name, "disposal_date", date)
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
asset.set_status("Scrapped")
@@ -224,6 +290,9 @@ def scrap_asset(asset_name):
def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
reset_depreciation_schedule(asset, asset.disposal_date)
je = asset.journal_entry_for_scrap
asset.db_set("disposal_date", None)
@@ -234,6 +303,99 @@ def restore_asset(asset_name):
asset.set_status()
def depreciate_asset(asset, date):
asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(date_of_disposal=date)
asset.save()
make_depreciation_entry(asset.name, date)
def reset_depreciation_schedule(asset, date):
asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset
asset.prepare_depreciation_data(date_of_return=date)
modify_depreciation_schedule_for_asset_repairs(asset)
asset.save()
def modify_depreciation_schedule_for_asset_repairs(asset):
asset_repairs = frappe.get_all(
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
)
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data()
def reverse_depreciation_entry_made_after_disposal(asset, date):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
row = -1
finance_book = asset.get("schedules")[0].get("finance_book")
for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
else:
row += 1
if schedule.schedule_date == date:
if not disposal_was_made_on_original_schedule_date(
asset, schedule, row, date
) or disposal_happens_in_the_future(date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
idx = cint(schedule.finance_book_id)
asset.finance_books[idx - 1].value_after_depreciation += depreciation_amount
asset.save()
def get_depreciation_amount_in_je(journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal):
for finance_book in asset.get("finance_books"):
if schedule.finance_book == finance_book.finance_book:
orginal_schedule_date = add_months(
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
)
if is_last_day_of_the_month(finance_book.depreciation_start_date):
orginal_schedule_date = get_last_day(orginal_schedule_date)
if orginal_schedule_date == posting_date_of_disposal:
return True
return False
def disposal_happens_in_the_future(posting_date_of_disposal):
if posting_date_of_disposal > getdate():
return True
return False
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
(
fixed_asset_account,
@@ -307,18 +469,8 @@ def get_asset_details(asset, finance_book=None):
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
idx = 1
if finance_book:
for d in asset.finance_books:
if d.finance_book == finance_book:
idx = d.idx
break
value_after_depreciation = asset.get_value_after_depreciation(finance_book)
value_after_depreciation = (
asset.finance_books[idx - 1].value_after_depreciation
if asset.calculate_depreciation
else asset.value_after_depreciation
)
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation)
return (
@@ -358,3 +510,9 @@ def get_disposal_account_and_cost_center(company):
frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company))
return disposal_account, depreciation_cost_center
def is_last_day_of_the_month(date):
last_day_of_the_month = get_last_day(date)
return getdate(last_day_of_the_month) == getdate(date)

View File

@@ -4,11 +4,22 @@
import unittest
import frappe
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
from frappe.utils import (
add_days,
add_months,
cstr,
flt,
get_first_day,
get_last_day,
getdate,
nowdate,
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status
from erpnext.assets.doctype.asset.depreciation import (
is_last_day_of_the_month,
post_depreciation_entries,
restore_asset,
scrap_asset,
@@ -153,28 +164,59 @@ class TestAsset(AssetSetup):
self.assertEqual(doc.items[0].is_fixed_asset, 1)
def test_scrap_asset(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2020-01-01",
purchase_date="2020-01-01",
available_for_use_date=purchase_date,
purchase_date=purchase_date,
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
post_depreciation_entries(date=add_months("2020-01-01", 4))
post_depreciation_entries(date=add_months(purchase_date, 2))
asset.load_from_db()
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
self.assertEquals(accumulated_depr_amount, 18000.0)
scrap_asset(asset.name)
asset.load_from_db()
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
self.assertEquals(
accumulated_depr_amount,
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
)
self.assertEqual(asset.status, "Scrapped")
self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = (
("_Test Accumulated Depreciations - _TC", 36000.0, 0.0),
(
"_Test Accumulated Depreciations - _TC",
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0),
(
"_Test Gain/Loss on Asset Disposal - _TC",
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
)
gle = frappe.db.sql(
@@ -183,7 +225,7 @@ class TestAsset(AssetSetup):
order by account""",
asset.journal_entry_for_scrap,
)
self.assertEqual(gle, expected_gle)
self.assertSequenceEqual(gle, expected_gle)
restore_asset(asset.name)
@@ -191,34 +233,57 @@ class TestAsset(AssetSetup):
self.assertFalse(asset.journal_entry_for_scrap)
self.assertEqual(asset.status, "Partially Depreciated")
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
this_month_depr_amount = 9000.0 if is_last_day_of_the_month(date) else 0
self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount)
def test_gle_made_by_asset_sale(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2020-06-06",
purchase_date="2020-01-01",
available_for_use_date=purchase_date,
purchase_date=purchase_date,
expected_value_after_useful_life=10000,
total_number_of_depreciations=3,
frequency_of_depreciation=10,
depreciation_start_date="2020-12-31",
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
post_depreciation_entries(date="2021-01-01")
post_depreciation_entries(date=add_months(purchase_date, 2))
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
si.customer = "_Test Customer"
si.set_posting_time = 1
si.posting_date = "2021-10-31"
si.due_date = "2021-10-31"
si.get("items")[0].rate = 75000
si.due_date = nowdate()
si.get("items")[0].rate = 25000
si.insert()
si.submit()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
expected_gle = (
("_Test Accumulated Depreciations - _TC", 50490.2, 0.0),
(
"_Test Accumulated Depreciations - _TC",
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 0.0, 25490.2),
("Debtors - _TC", 75000.0, 0.0),
(
"_Test Gain/Loss on Asset Disposal - _TC",
flt(57000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
0.0,
),
("Debtors - _TC", 25000.0, 0.0),
)
gle = frappe.db.sql(
@@ -228,14 +293,9 @@ class TestAsset(AssetSetup):
si.name,
)
for i, gle_entry in enumerate(gle):
self.assertEqual(gle_entry[0], expected_gle[i][0])
self.assertEqual(flt(gle_entry[1], 1), flt(expected_gle[i][1], 1))
self.assertEqual(flt(gle_entry[2], 1), flt(expected_gle[i][2], 1))
self.assertSequenceEqual(gle, expected_gle)
si.load_from_db()
si.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
def test_asset_with_maintenance_required_status_after_sale(self):
@@ -1351,6 +1411,36 @@ class TestDepreciationBasics(AssetSetup):
for i, schedule in enumerate(asset.schedules):
self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date))
def test_manual_depreciation_for_existing_asset(self):
asset = create_asset(
item_code="Macbook Pro",
is_existing_asset=1,
purchase_date="2020-01-30",
available_for_use_date="2020-01-30",
submit=1,
)
self.assertEqual(asset.status, "Submitted")
self.assertEqual(asset.get("value_after_depreciation"), 100000)
jv = make_journal_entry(
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
)
for d in jv.accounts:
d.reference_type = "Asset"
d.reference_name = asset.name
jv.voucher_type = "Depreciation Entry"
jv.insert()
jv.submit()
asset.reload()
self.assertEqual(asset.get("value_after_depreciation"), 99900)
jv.cancel()
asset.reload()
self.assertEqual(asset.get("value_after_depreciation"), 100000)
def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"):
@@ -1387,6 +1477,7 @@ def create_asset(**args):
"location": args.location or "Test Location",
"asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1,
"depr_entry_posting_status": args.depr_entry_posting_status or "",
}
)

View File

@@ -39,7 +39,11 @@ class AssetRepair(AccountsController):
def before_submit(self):
self.check_repair_status()
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
self.increase_asset_value()
if self.get("stock_consumption"):
@@ -49,20 +53,23 @@ class AssetRepair(AccountsController):
if self.get("capitalize_repair_cost"):
self.make_gl_entries()
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
self.modify_depreciation_schedule()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
if self.asset_doc.calculate_depreciation:
self.update_asset_expected_value_after_useful_life()
self.asset_doc.save()
def before_cancel(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
self.decrease_asset_value()
if self.get("stock_consumption"):
@@ -72,16 +79,18 @@ class AssetRepair(AccountsController):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True)
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
self.revert_depreciation_schedule_on_cancellation()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
if self.asset_doc.calculate_depreciation:
self.update_asset_expected_value_after_useful_life()
self.asset_doc.save()
def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status()
def check_repair_status(self):
if self.repair_status == "Pending":
frappe.throw(_("Please update Repair Status."))
@@ -97,6 +106,26 @@ class AssetRepair(AccountsController):
title=_("Missing Warehouse"),
)
def update_asset_expected_value_after_useful_life(self):
for row in self.asset_doc.get("finance_books"):
if row.depreciation_method in ("Written Down Value", "Double Declining Balance"):
accumulated_depreciation_after_full_schedule = [
d.accumulated_depreciation_amount
for d in self.asset_doc.get("schedules")
if cint(d.finance_book_id) == row.idx
]
accumulated_depreciation_after_full_schedule = max(
accumulated_depreciation_after_full_schedule
)
asset_value_after_full_schedule = flt(
flt(row.value_after_depreciation) - flt(accumulated_depreciation_after_full_schedule),
row.precision("expected_value_after_useful_life"),
)
row.expected_value_after_useful_life = asset_value_after_full_schedule
def increase_asset_value(self):
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()

View File

@@ -6,6 +6,7 @@ import unittest
import frappe
from frappe.utils import flt, nowdate
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.test_asset import (
create_asset,
create_asset_data,
@@ -105,20 +106,20 @@ class TestAssetRepair(unittest.TestCase):
def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation=1, submit=1)
initial_asset_value = get_asset_value(asset)
initial_asset_value = get_asset_value_after_depreciation(asset.name)
asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1)
asset.reload()
increase_in_asset_value = get_asset_value(asset) - initial_asset_value
increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
asset = create_asset(calculate_depreciation=1, submit=1)
initial_asset_value = get_asset_value(asset)
initial_asset_value = get_asset_value_after_depreciation(asset.name)
asset_repair = create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1)
asset.reload()
increase_in_asset_value = get_asset_value(asset) - initial_asset_value
increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value
self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
def test_purchase_invoice(self):
@@ -143,10 +144,6 @@ class TestAssetRepair(unittest.TestCase):
)
def get_asset_value(asset):
return asset.finance_books[0].value_after_depreciation
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations

View File

@@ -47,7 +47,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
set_current_asset_value: function(frm) {
if (frm.doc.asset) {
frm.call({
method: "erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment.get_current_asset_value",
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
args: {
asset: frm.doc.asset,
finance_book: frm.doc.finance_book

View File

@@ -10,7 +10,10 @@ from frappe.utils import cint, date_diff, flt, formatdate, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.assets.doctype.asset.asset import get_depreciation_amount
from erpnext.assets.doctype.asset.asset import (
get_asset_value_after_depreciation,
get_depreciation_amount,
)
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.regional.india.utils import (
get_depreciation_amount as get_depreciation_amount_for_india,
@@ -45,7 +48,7 @@ class AssetValueAdjustment(Document):
def set_current_asset_value(self):
if not self.current_asset_value and self.asset:
self.current_asset_value = get_current_asset_value(self.asset, self.finance_book)
self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book)
def make_depreciation_entry(self):
asset = frappe.get_doc("Asset", self.asset)
@@ -148,12 +151,3 @@ class AssetValueAdjustment(Document):
for asset_data in asset.schedules:
if not asset_data.journal_entry:
asset_data.db_update()
@frappe.whitelist()
def get_current_asset_value(asset, finance_book=None):
cond = {"parent": asset, "parenttype": "Asset"}
if finance_book:
cond.update({"finance_book": finance_book})
return frappe.db.get_value("Asset Finance Book", cond, "value_after_depreciation")

View File

@@ -6,10 +6,8 @@ import unittest
import frappe
from frappe.utils import add_days, get_last_day, nowdate
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
get_current_asset_value,
)
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -43,7 +41,7 @@ class TestAssetValueAdjustment(unittest.TestCase):
)
asset_doc.submit()
current_value = get_current_asset_value(asset_doc.name)
current_value = get_asset_value_after_depreciation(asset_doc.name)
self.assertEqual(current_value, 100000.0)
def test_asset_depreciation_value_adjustment(self):
@@ -73,7 +71,7 @@ class TestAssetValueAdjustment(unittest.TestCase):
)
asset_doc.submit()
current_value = get_current_asset_value(asset_doc.name)
current_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
)

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.utils import cstr, flt, formatdate, getdate
from erpnext.accounts.report.financial_statements import (
@@ -11,6 +12,8 @@ from erpnext.accounts.report.financial_statements import (
get_period_list,
validate_fiscal_year,
)
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
def execute(filters=None):
@@ -85,7 +88,9 @@ def get_data(filters):
"asset_name",
"status",
"department",
"company",
"cost_center",
"calculate_depreciation",
"purchase_receipt",
"asset_category",
"purchase_date",
@@ -97,12 +102,21 @@ def get_data(filters):
]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},
pluck="parent",
)
for asset in assets_record:
asset_value = (
asset.gross_purchase_amount
- flt(asset.opening_accumulated_depreciation)
- flt(depreciation_amount_map.get(asset.name))
)
if filters.finance_book:
if asset.asset_id not in assets_linked_to_fb:
continue
else:
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
continue
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
row = {
"asset_id": asset.asset_id,
"asset_name": asset.asset_name,
@@ -113,7 +127,7 @@ def get_data(filters):
or pi_supplier_map.get(asset.purchase_invoice),
"gross_purchase_amount": asset.gross_purchase_amount,
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
"depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
"depreciated_amount": get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters),
"available_for_use_date": asset.available_for_use_date,
"location": asset.location,
"asset_category": asset.asset_category,
@@ -137,6 +151,7 @@ def prepare_chart_data(data, filters):
filters.filter_based_on,
"Monthly",
company=filters.company,
ignore_fiscal_year=True,
)
for d in period_list:
@@ -170,6 +185,15 @@ def prepare_chart_data(data, filters):
}
def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
if asset.calculate_depreciation:
depr_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
else:
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
return flt(depr_amount, 2)
def get_finance_book_value_map(filters):
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
@@ -189,6 +213,31 @@ def get_finance_book_value_map(filters):
)
def get_manual_depreciation_amount_of_asset(asset, filters):
date = filters.to_date if filters.filter_based_on == "Date Range" else filters.year_end_date
(_, _, depreciation_expense_account) = get_depreciation_accounts(asset)
gle = frappe.qb.DocType("GL Entry")
result = (
frappe.qb.from_(gle)
.select(Sum(gle.debit))
.where(gle.against_voucher == asset.asset_id)
.where(gle.account == depreciation_expense_account)
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.where(gle.posting_date <= date)
).run()
if result and result[0] and result[0][0]:
depr_amount = result[0][0]
else:
depr_amount = 0
return depr_amount
def get_purchase_receipt_supplier_map():
return frappe._dict(
frappe.db.sql(

View File

@@ -14,6 +14,7 @@
"column_break_3",
"po_required",
"pr_required",
"over_order_allowance",
"maintain_same_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
@@ -42,57 +43,6 @@
"label": "Default Buying Price List",
"options": "Price List"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "po_required",
"fieldtype": "Select",
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
"options": "No\nYes"
},
{
"fieldname": "pr_required",
"fieldtype": "Select",
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
"options": "No\nYes"
},
{
"default": "0",
"fieldname": "maintain_same_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout the Purchase Cycle"
},
{
"default": "0",
"fieldname": "allow_multiple_items",
"fieldtype": "Check",
"label": "Allow Item To Be Added Multiple Times in a Transaction"
},
{
"fieldname": "subcontract",
"fieldtype": "Section Break",
"label": "Subcontract"
},
{
"default": "Material Transferred for Subcontract",
"fieldname": "backflush_raw_materials_of_subcontract_based_on",
"fieldtype": "Select",
"label": "Backflush Raw Materials of Subcontract Based On",
"options": "BOM\nMaterial Transferred for Subcontract"
},
{
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"BOM\"",
"description": "Percentage you are allowed to transfer more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to transfer 110 units.",
"fieldname": "over_transfer_allowance",
"fieldtype": "Float",
"label": "Over Transfer Allowance (%)"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Stop",
"depends_on": "maintain_same_rate",
@@ -110,12 +60,70 @@
"label": "Role Allowed to Override Stop Action",
"options": "Role"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "po_required",
"fieldtype": "Select",
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
"options": "No\nYes"
},
{
"fieldname": "pr_required",
"fieldtype": "Select",
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
"options": "No\nYes"
},
{
"default": "0",
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
"fieldname": "over_order_allowance",
"fieldtype": "Float",
"label": "Over Order Allowance (%)"
},
{
"default": "0",
"fieldname": "maintain_same_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout the Purchase Cycle"
},
{
"default": "0",
"fieldname": "allow_multiple_items",
"fieldtype": "Check",
"label": "Allow Item To Be Added Multiple Times in a Transaction"
},
{
"default": "1",
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice"
},
{
"fieldname": "subcontract",
"fieldtype": "Section Break",
"label": "Subcontract"
},
{
"default": "Material Transferred for Subcontract",
"fieldname": "backflush_raw_materials_of_subcontract_based_on",
"fieldtype": "Select",
"label": "Backflush Raw Materials of Subcontract Based On",
"options": "BOM\nMaterial Transferred for Subcontract"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"BOM\"",
"description": "Percentage you are allowed to transfer more against the quantity ordered. For example: If you have ordered 100 units. and your Allowance is 10% then you are allowed to transfer 110 units.",
"fieldname": "over_transfer_allowance",
"fieldtype": "Float",
"label": "Over Transfer Allowance (%)"
}
],
"icon": "fa fa-cog",
@@ -123,7 +131,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-09-08 19:26:23.548837",
"modified": "2023-03-22 13:01:49.640869",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
validate_against_blanket_order,
)
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
@@ -72,6 +75,7 @@ class PurchaseOrder(BuyingController):
self.validate_bom_for_subcontracting_items()
self.create_raw_materials_supplied("supplied_items")
self.set_received_qty_for_drop_ship_items()
validate_against_blanket_order(self)
validate_inter_company_party(
self.doctype, self.supplier, self.company, self.inter_company_order_reference
)

View File

@@ -1239,6 +1239,11 @@ class TestPurchaseOrder(FrappeTestCase):
automatically_fetch_payment_terms(enable=0)
def test_variant_item_po(self):
po = create_purchase_order(item_code="_Test Variant Item", qty=1, rate=100, do_not_save=1)
self.assertRaises(frappe.ValidationError, po.save)
def make_pr_against_po(po, received_qty=0):
pr = make_purchase_receipt(po)
@@ -1342,8 +1347,8 @@ def create_purchase_order(**args):
},
)
po.set_missing_values()
if not args.do_not_save:
po.set_missing_values()
po.insert()
if not args.do_not_submit:
if po.is_subcontracted == "Yes":

View File

@@ -383,7 +383,7 @@ class AccountsController(TransactionBase):
self.get("inter_company_reference")
or self.get("inter_company_invoice_reference")
or self.get("inter_company_order_reference")
):
) and not self.get("is_return"):
msg = _("Internal Sale or Delivery Reference missing.")
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))

View File

@@ -755,6 +755,8 @@ class BuyingController(StockController, Subcontracting):
asset.purchase_date = self.posting_date
asset.supplier = self.supplier
elif self.docstatus == 2:
if asset.docstatus == 2:
continue
if asset.docstatus == 0:
asset.set(field, None)
asset.supplier = None

View File

@@ -131,7 +131,7 @@ def validate_returned_items(doc):
)
elif ref.serial_no:
if not d.serial_no:
if d.qty and not d.serial_no:
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
else:
serial_nos = get_serial_nos(d.serial_no)
@@ -301,7 +301,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
# Used retrun against and supplier and is_retrun because there is an index added for it
data = frappe.db.get_list(
data = frappe.get_all(
doctype,
fields=fields,
filters=[
@@ -393,6 +393,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
if serial_nos:
target_doc.serial_no = "\n".join(serial_nos)
if source_doc.get("rejected_serial_no"):
returned_serial_nos = get_returned_serial_nos(
source_doc, source_parent, serial_no_field="rejected_serial_no"
)
rejected_serial_nos = list(
set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos)
)
if rejected_serial_nos:
target_doc.rejected_serial_no = "\n".join(rejected_serial_nos)
if doctype == "Purchase Receipt":
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
@@ -587,7 +597,7 @@ def get_filters(
return filters
def get_returned_serial_nos(child_doc, parent_doc):
def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
return_ref_field = frappe.scrub(child_doc.doctype)
@@ -596,7 +606,7 @@ def get_returned_serial_nos(child_doc, parent_doc):
serial_nos = []
fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)]
fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name],
@@ -606,6 +616,6 @@ def get_returned_serial_nos(child_doc, parent_doc):
]
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
serial_nos.extend(get_serial_nos(row.serial_no))
serial_nos.extend(get_serial_nos(row.get(serial_no_field)))
return serial_nos

View File

@@ -25,7 +25,7 @@ class SellingController(StockController):
def onload(self):
super(SellingController, self).onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
for item in self.get("items"):
for item in self.get("items") + (self.get("packed_items") or []):
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
def validate(self):

View File

@@ -58,7 +58,7 @@ status_map = {
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1",
],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
["On Hold", "eval:self.status=='On Hold'"],
],
"Purchase Order": [
@@ -79,7 +79,7 @@ status_map = {
["Delivered", "eval:self.status=='Delivered'"],
["Cancelled", "eval:self.docstatus==2"],
["On Hold", "eval:self.status=='On Hold'"],
["Closed", "eval:self.status=='Closed'"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],
"Delivery Note": [
["Draft", None],
@@ -87,7 +87,7 @@ status_map = {
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],
"Purchase Receipt": [
["Draft", None],
@@ -95,7 +95,7 @@ status_map = {
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],
"Material Request": [
["Draft", None],

View File

@@ -102,7 +102,7 @@
}
],
"links": [],
"modified": "2021-06-29 18:27:02.832979",
"modified": "2022-12-28 16:35:34.377575",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
@@ -121,16 +121,6 @@
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
},
{
"create": 1,
"delete": 1,

View File

@@ -5,7 +5,9 @@
from collections import Counter
import frappe
import frappe.share
from frappe import _
from frappe.desk.form.assign_to import add as add_assignment
from frappe.model.document import Document
from frappe.utils import get_url, getdate
from frappe.utils.verified_command import get_signed_params
@@ -118,21 +120,18 @@ class Appointment(Document):
self.party = lead.name
def auto_assign(self):
from frappe.desk.form.assign_to import add as add_assignemnt
existing_assignee = self.get_assignee_from_latest_opportunity()
if existing_assignee:
# If the latest opportunity is assigned to someone
# Assign the appointment to the same
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]})
self.assign_agent(existing_assignee)
return
if self._assign:
return
available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time))
for agent in available_agents:
if _check_agent_availability(agent, self.scheduled_time):
agent = agent[0]
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
self.assign_agent(agent[0])
break
def get_assignee_from_latest_opportunity(self):
@@ -187,9 +186,15 @@ class Appointment(Document):
params = {"email": self.customer_email, "appointment": self.name}
return get_url(verify_route + "?" + get_signed_params(params))
def assign_agent(self, agent):
if not frappe.has_permission(doc=self, user=agent):
frappe.share.add(self.doctype, self.name, agent, flags={"ignore_share_permission": True})
add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
def _get_agents_sorted_by_asc_workload(date):
appointments = frappe.db.get_list("Appointment", fields="*")
appointments = frappe.get_all("Appointment", fields="*")
agent_list = _get_agent_list_as_strings()
if not appointments:
return agent_list
@@ -214,7 +219,7 @@ def _get_agent_list_as_strings():
def _check_agent_availability(agent_email, scheduled_time):
appointemnts_at_scheduled_time = frappe.get_list(
appointemnts_at_scheduled_time = frappe.get_all(
"Appointment", filters={"scheduled_time": scheduled_time}
)
for appointment in appointemnts_at_scheduled_time:

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2019-08-27 10:56:48.309824",
"doctype": "DocType",
"editable_grid": 1,
@@ -101,7 +102,8 @@
}
],
"issingle": 1,
"modified": "2019-11-26 12:14:17.669366",
"links": [],
"modified": "2022-12-28 16:41:28.773090",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment Booking Settings",
@@ -117,13 +119,6 @@
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Guest",
"share": 1
},
{
"create": 1,
"email": 1,

View File

@@ -173,7 +173,10 @@ class TestWebsiteItem(unittest.TestCase):
# Website Item Portal Tests Begin
def test_website_item_breadcrumbs(self):
"Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group."
"""
Check if breadcrumbs include homepage, product listing navigation page,
parent item group(s) and item group
"""
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
item_code = "Test Breadcrumb Item"
@@ -196,7 +199,7 @@ class TestWebsiteItem(unittest.TestCase):
breadcrumbs = get_parent_item_groups(item.item_group)
self.assertEqual(breadcrumbs[0]["name"], "Home")
self.assertEqual(breadcrumbs[1]["name"], "Shop by Category")
self.assertEqual(breadcrumbs[1]["name"], "All Products")
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")

View File

@@ -345,7 +345,8 @@
"image_field": "website_image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-06-28 17:10:30.613251",
"make_attachments_public": 1,
"modified": "2022-09-13 04:05:11.614087",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Website Item",

View File

@@ -90,6 +90,7 @@ class LeaveAllocation(Document):
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
def on_update_after_submit(self):
if self.has_value_changed("new_leaves_allocated"):
self.validate_against_leave_applications()
@@ -99,7 +100,11 @@ class LeaveAllocation(Document):
# run required validations again since total leaves are being updated
self.validate_leave_days_and_dates()
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
leaves_to_be_added = flt(
(self.new_leaves_allocated - self.get_existing_leave_count()),
self.precision("new_leaves_allocated"),
)
args = {
"leaves": leaves_to_be_added,
"from_date": self.from_date,
@@ -118,14 +123,13 @@ class LeaveAllocation(Document):
"employee": self.employee,
"company": self.company,
"leave_type": self.leave_type,
"is_carry_forward": 0,
"docstatus": 1,
},
pluck="leaves",
fields=["SUM(leaves) as total_leaves"],
)
total_existing_leaves = 0
for entry in ledger_entries:
total_existing_leaves += entry
return total_existing_leaves
return ledger_entries[0].total_leaves if ledger_entries else 0
def validate_against_leave_applications(self):
leaves_taken = get_approved_leaves_for_period(

View File

@@ -18,6 +18,7 @@ class TestLeaveAllocation(FrappeTestCase):
def setUp(self):
frappe.db.delete("Leave Period")
frappe.db.delete("Leave Allocation")
frappe.db.delete("Leave Ledger Entry")
emp_id = make_employee("test_emp_leave_allocation@salary.com", company="_Test Company")
self.employee = frappe.get_doc("Employee", emp_id)
@@ -69,7 +70,6 @@ class TestLeaveAllocation(FrappeTestCase):
def test_validation_for_over_allocation(self):
leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1)
leave_type.save()
doc = frappe.get_doc(
{
@@ -137,9 +137,9 @@ class TestLeaveAllocation(FrappeTestCase):
)
).insert()
leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
leave_type.max_leaves_allowed = 25
leave_type.save()
leave_type = create_leave_type(
leave_type_name="_Test Allocation Validation", is_carry_forward=1, max_leaves_allowed=25
)
# 15 leaves allocated in this period
allocation = create_leave_allocation(
@@ -174,9 +174,9 @@ class TestLeaveAllocation(FrappeTestCase):
)
).insert()
leave_type = create_leave_type(leave_type_name="_Test Allocation Validation", is_carry_forward=1)
leave_type.max_leaves_allowed = 30
leave_type.save()
leave_type = create_leave_type(
leave_type_name="_Test Allocation Validation", is_carry_forward=1, max_leaves_allowed=30
)
# 15 leaves allocated
allocation = create_leave_allocation(
@@ -207,7 +207,6 @@ class TestLeaveAllocation(FrappeTestCase):
def test_validate_back_dated_allocation_update(self):
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
leave_type.save()
# initial leave allocation = 15
leave_allocation = create_leave_allocation(
@@ -235,10 +234,12 @@ class TestLeaveAllocation(FrappeTestCase):
self.assertRaises(BackDatedAllocationError, leave_allocation.save)
def test_carry_forward_calculation(self):
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
leave_type.save()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave",
is_carry_forward=1,
maximum_carry_forwarded_leaves=10,
max_leaves_allowed=30,
)
# initial leave allocation = 15
leave_allocation = create_leave_allocation(
@@ -286,7 +287,6 @@ class TestLeaveAllocation(FrappeTestCase):
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
)
leave_type.save()
# initial leave allocation
leave_allocation = create_leave_allocation(
@@ -352,12 +352,51 @@ class TestLeaveAllocation(FrappeTestCase):
)
leave_allocation.submit()
leave_allocation.reload()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
self.assertEqual(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 40
leave_allocation.submit()
leave_allocation.save()
leave_allocation.reload()
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
updated_entry = frappe.db.get_all(
"Leave Ledger Entry",
{"transaction_name": leave_allocation.name},
pluck="leaves",
order_by="creation desc",
limit=1,
)
self.assertEqual(updated_entry[0], 25)
self.assertEqual(leave_allocation.total_leaves_allocated, 40)
def test_leave_addition_after_submit_with_carry_forward(self):
from erpnext.hr.doctype.leave_application.test_leave_application import (
create_carry_forwarded_allocation,
)
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
include_holiday=True,
)
leave_allocation = create_carry_forwarded_allocation(self.employee, leave_type)
# 15 new leaves, 15 carry forwarded leaves
self.assertEqual(leave_allocation.total_leaves_allocated, 30)
leave_allocation.new_leaves_allocated = 32
leave_allocation.save()
leave_allocation.reload()
updated_entry = frappe.db.get_all(
"Leave Ledger Entry",
{"transaction_name": leave_allocation.name},
pluck="leaves",
order_by="creation desc",
limit=1,
)
self.assertEqual(updated_entry[0], 17)
self.assertEqual(leave_allocation.total_leaves_allocated, 47)
def test_leave_subtraction_after_submit(self):
leave_allocation = create_leave_allocation(
@@ -365,12 +404,49 @@ class TestLeaveAllocation(FrappeTestCase):
)
leave_allocation.submit()
leave_allocation.reload()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
self.assertEqual(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 10
leave_allocation.submit()
leave_allocation.reload()
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
updated_entry = frappe.db.get_all(
"Leave Ledger Entry",
{"transaction_name": leave_allocation.name},
pluck="leaves",
order_by="creation desc",
limit=1,
)
self.assertEqual(updated_entry[0], -5)
self.assertEqual(leave_allocation.total_leaves_allocated, 10)
def test_leave_subtraction_after_submit_with_carry_forward(self):
from erpnext.hr.doctype.leave_application.test_leave_application import (
create_carry_forwarded_allocation,
)
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
include_holiday=True,
)
leave_allocation = create_carry_forwarded_allocation(self.employee, leave_type)
self.assertEqual(leave_allocation.total_leaves_allocated, 30)
leave_allocation.new_leaves_allocated = 8
leave_allocation.save()
updated_entry = frappe.db.get_all(
"Leave Ledger Entry",
{"transaction_name": leave_allocation.name},
pluck="leaves",
order_by="creation desc",
limit=1,
)
self.assertEqual(updated_entry[0], -7)
self.assertEqual(leave_allocation.total_leaves_allocated, 23)
def test_validation_against_leave_application_after_submit(self):
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list

View File

@@ -817,7 +817,9 @@ def get_leave_balance_on(
allocation = allocation_records.get(leave_type, frappe._dict())
end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
cf_expiry = get_allocation_expiry_for_cf_leaves(
employee, leave_type, to_date, allocation.from_date
)
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
@@ -832,6 +834,7 @@ def get_leave_balance_on(
def get_leave_allocation_records(employee, date, leave_type=None):
"""Returns the total allocated leaves and carry forwarded leaves based on ledger entries"""
Ledger = frappe.qb.DocType("Leave Ledger Entry")
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
cf_leave_case = (
frappe.qb.terms.Case().when(Ledger.is_carry_forward == "1", Ledger.leaves).else_(0)
@@ -845,21 +848,33 @@ def get_leave_allocation_records(employee, date, leave_type=None):
query = (
frappe.qb.from_(Ledger)
.inner_join(LeaveAllocation)
.on(Ledger.transaction_name == LeaveAllocation.name)
.select(
sum_cf_leaves,
sum_new_leaves,
Min(Ledger.from_date).as_("from_date"),
Max(Ledger.to_date).as_("to_date"),
Ledger.leave_type,
Ledger.employee,
)
.where(
(Ledger.from_date <= date)
& (Ledger.to_date >= date)
& (Ledger.docstatus == 1)
& (Ledger.transaction_type == "Leave Allocation")
& (Ledger.employee == employee)
& (Ledger.is_expired == 0)
& (Ledger.is_lwp == 0)
& (
# newly allocated leave's end date is same as the leave allocation's to date
((Ledger.is_carry_forward == 0) & (Ledger.to_date >= date))
# carry forwarded leave's end date won't be same as the leave allocation's to date
# it's between the leave allocation's from and to date
| (
(Ledger.is_carry_forward == 1)
& (Ledger.to_date.between(LeaveAllocation.from_date, LeaveAllocation.to_date))
)
)
)
)
@@ -881,6 +896,7 @@ def get_leave_allocation_records(employee, date, leave_type=None):
"unused_leaves": d.cf_leaves,
"new_leaves_allocated": d.new_leaves,
"leave_type": d.leave_type,
"employee": d.employee,
}
),
)
@@ -919,22 +935,51 @@ def get_remaining_leaves(
return remaining_leaves
leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(
leaves_taken
)
# balance for carry forwarded leaves
if cf_expiry and allocation.unused_leaves:
cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken)
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
# allocation contains both carry forwarded and new leaves
new_leaves_taken, cf_leaves_taken = get_new_and_cf_leaves_taken(allocation, cf_expiry)
leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves)
leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves)
if getdate(date) > getdate(cf_expiry):
# carry forwarded leaves have expired
cf_leaves = remaining_cf_leaves = 0
else:
cf_leaves = flt(allocation.unused_leaves) + flt(cf_leaves_taken)
remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry)
# new leaves allocated - new leaves taken + cf leave balance
# Note: `new_leaves_taken` is added here because its already a -ve number in the ledger
leave_balance = (flt(allocation.new_leaves_allocated) + flt(new_leaves_taken)) + flt(cf_leaves)
leave_balance_for_consumption = (
flt(allocation.new_leaves_allocated) + flt(new_leaves_taken)
) + flt(remaining_cf_leaves)
else:
# allocation only contains newly allocated leaves
leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(
leaves_taken
)
remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date)
return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves)
def get_new_and_cf_leaves_taken(allocation: Dict, cf_expiry: str) -> Tuple[float, float]:
"""returns new leaves taken and carry forwarded leaves taken within an allocation period based on cf leave expiry"""
cf_leaves_taken = get_leaves_for_period(
allocation.employee, allocation.leave_type, allocation.from_date, cf_expiry
)
new_leaves_taken = get_leaves_for_period(
allocation.employee, allocation.leave_type, add_days(cf_expiry, 1), allocation.to_date
)
# using abs because leaves taken is a -ve number in the ledger
if abs(cf_leaves_taken) > allocation.unused_leaves:
# adjust the excess leaves in new_leaves_taken
new_leaves_taken += -(abs(cf_leaves_taken) - allocation.unused_leaves)
cf_leaves_taken = -allocation.unused_leaves
return new_leaves_taken, cf_leaves_taken
def get_leaves_for_period(
employee: str, leave_type: str, from_date: str, to_date: str, skip_expired_leaves: bool = True
) -> float:

View File

@@ -28,6 +28,7 @@ from erpnext.hr.doctype.leave_application.leave_application import (
get_leave_allocation_records,
get_leave_balance_on,
get_leave_details,
get_new_and_cf_leaves_taken,
)
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
create_assignment_for_multiple_employees,
@@ -96,6 +97,9 @@ class TestLeaveApplication(unittest.TestCase):
from_date = get_year_start(getdate())
to_date = get_year_ending(getdate())
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
list_without_weekly_offs = make_holiday_list(
"Holiday List w/o Weekly Offs", from_date=from_date, to_date=to_date, add_weekly_offs=False
)
if not frappe.db.exists("Leave Type", "_Test Leave Type"):
frappe.get_doc(
@@ -699,7 +703,6 @@ class TestLeaveApplication(unittest.TestCase):
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
)
leave_type.insert()
create_carry_forwarded_allocation(employee, leave_type)
details = get_leave_balance_on(
@@ -771,7 +774,6 @@ class TestLeaveApplication(unittest.TestCase):
employee = get_employee()
leave_type = create_leave_type(leave_type_name="Test Leave Type 1")
leave_type.save()
leave_allocation = create_leave_allocation(
employee=employee.name, employee_name=employee.employee_name, leave_type=leave_type.name
@@ -814,7 +816,6 @@ class TestLeaveApplication(unittest.TestCase):
expire_carry_forwarded_leaves_after_days=90,
include_holiday=True,
)
leave_type.submit()
create_carry_forwarded_allocation(employee, leave_type)
@@ -853,7 +854,6 @@ class TestLeaveApplication(unittest.TestCase):
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
)
leave_type.submit()
create_carry_forwarded_allocation(employee, leave_type)
@@ -991,18 +991,169 @@ class TestLeaveApplication(unittest.TestCase):
}
self.assertEqual(leave_allocation, expected)
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_get_leave_allocation_records(self):
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
def test_leave_details_with_expired_cf_leaves(self):
"""Tests leave details:
Case 1: All leaves available before cf leave expiry
Case 2: Remaining Leaves after cf leave expiry
"""
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
)
leave_type.insert()
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
details = get_leave_allocation_records(employee.name, getdate(), leave_type.name)
cf_expiry = frappe.db.get_value(
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
)
# case 1: all leaves available before cf leave expiry
leave_details = get_leave_details(employee.name, add_days(cf_expiry, -1))
self.assertEqual(leave_details["leave_allocation"][leave_type.name]["remaining_leaves"], 30.0)
# case 2: cf leaves expired
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 1))
expected_data = {
"total_leaves": 30.0,
"expired_leaves": 15.0,
"leaves_taken": 0.0,
"leaves_pending_approval": 0.0,
"remaining_leaves": 15.0,
}
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
def test_leave_details_with_application_across_cf_expiry(self):
"""Tests leave details with leave application across cf expiry, such that:
cf leaves are partially expired and partially consumed
"""
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
)
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
cf_expiry = frappe.db.get_value(
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
)
# leave application across cf expiry
application = make_leave_application(
employee.name,
cf_expiry,
add_days(cf_expiry, 3),
leave_type.name,
)
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
expected_data = {
"total_leaves": 30.0,
"expired_leaves": 14.0,
"leaves_taken": 4.0,
"leaves_pending_approval": 0.0,
"remaining_leaves": 12.0,
}
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
def test_leave_details_with_application_across_cf_expiry_2(self):
"""Tests the same case as above but with leave days greater than cf leaves allocated"""
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
)
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
cf_expiry = frappe.db.get_value(
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
)
# leave application across cf expiry, 20 days leave
application = make_leave_application(
employee.name,
add_days(cf_expiry, -16),
add_days(cf_expiry, 3),
leave_type.name,
)
# 15 cf leaves and 5 new leaves should be consumed
# after adjustment of the actual days breakup (17 and 3) because only 15 cf leaves have been allocated
new_leaves_taken, cf_leaves_taken = get_new_and_cf_leaves_taken(leave_alloc, cf_expiry)
self.assertEqual(new_leaves_taken, -5.0)
self.assertEqual(cf_leaves_taken, -15.0)
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
expected_data = {
"total_leaves": 30.0,
"expired_leaves": 0,
"leaves_taken": 20.0,
"leaves_pending_approval": 0.0,
"remaining_leaves": 10.0,
}
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
@set_holiday_list("Holiday List w/o Weekly Offs", "_Test Company")
def test_leave_details_with_application_after_cf_expiry(self):
"""Tests leave details with leave application after cf expiry, such that:
cf leaves are completely expired and only newly allocated leaves are consumed
"""
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
)
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
cf_expiry = frappe.db.get_value(
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
)
# leave application after cf expiry
application = make_leave_application(
employee.name,
add_days(cf_expiry, 1),
add_days(cf_expiry, 4),
leave_type.name,
)
leave_details = get_leave_details(employee.name, add_days(cf_expiry, 4))
expected_data = {
"total_leaves": 30.0,
"expired_leaves": 15.0,
"leaves_taken": 4.0,
"leaves_pending_approval": 0.0,
"remaining_leaves": 11.0,
}
self.assertEqual(leave_details["leave_allocation"][leave_type.name], expected_data)
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
def test_get_leave_allocation_records(self):
"""Tests if total leaves allocated before and after carry forwarded leave expiry is same"""
employee = get_employee()
leave_type = create_leave_type(
leave_type_name="_Test_CF_leave_expiry",
is_carry_forward=1,
expire_carry_forwarded_leaves_after_days=90,
)
leave_alloc = create_carry_forwarded_allocation(employee, leave_type)
cf_expiry = frappe.db.get_value(
"Leave Ledger Entry", {"transaction_name": leave_alloc.name, "is_carry_forward": 1}, "to_date"
)
# test total leaves allocated before cf leave expiry
details = get_leave_allocation_records(employee.name, add_days(cf_expiry, -1), leave_type.name)
expected_data = {
"from_date": getdate(leave_alloc.from_date),
"to_date": getdate(leave_alloc.to_date),
@@ -1010,9 +1161,15 @@ class TestLeaveApplication(unittest.TestCase):
"unused_leaves": 15.0,
"new_leaves_allocated": 15.0,
"leave_type": leave_type.name,
"employee": employee.name,
}
self.assertEqual(details.get(leave_type.name), expected_data)
# test leaves allocated after carry forwarded leaves expiry, should be same thoroughout allocation period
# cf leaves should show up under expired or taken leaves later
details = get_leave_allocation_records(employee.name, add_days(cf_expiry, 1), leave_type.name)
self.assertEqual(details.get(leave_type.name), expected_data)
def create_carry_forwarded_allocation(employee, leave_type):
# initial leave allocation

View File

@@ -9,7 +9,8 @@ test_records = frappe.get_test_records("Leave Type")
def create_leave_type(**args):
args = frappe._dict(args)
if frappe.db.exists("Leave Type", args.leave_type_name):
return frappe.get_doc("Leave Type", args.leave_type_name)
frappe.delete_doc("Leave Type", args.leave_type_name, force=True)
leave_type = frappe.get_doc(
{
"doctype": "Leave Type",
@@ -23,10 +24,14 @@ def create_leave_type(**args):
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
"encashment_threshold_days": args.encashment_threshold_days or 5,
"earning_component": "Leave Encashment",
"max_leaves_allowed": args.max_leaves_allowed,
"maximum_carry_forwarded_leaves": args.maximum_carry_forwarded_leaves,
}
)
if leave_type.is_ppl:
leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
leave_type.insert()
return leave_type

View File

@@ -2,53 +2,60 @@
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Employee Leave Balance"] = {
"filters": [
filters: [
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.defaults.get_default("year_start_date")
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
reqd: 1,
default: frappe.defaults.get_default("year_start_date")
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.defaults.get_default("year_end_date")
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
reqd: 1,
default: frappe.defaults.get_default("year_end_date")
},
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
reqd: 1,
default: frappe.defaults.get_user_default("Company")
},
{
"fieldname": "department",
"label": __("Department"),
"fieldtype": "Link",
"options": "Department",
fieldname: "department",
label: __("Department"),
fieldtype: "Link",
options: "Department",
},
{
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee",
fieldname: "employee",
label: __("Employee"),
fieldtype: "Link",
options: "Employee",
},
{
"fieldname": "employee_status",
"label": __("Employee Status"),
"fieldtype": "Select",
"options": [
fieldname: "employee_status",
label: __("Employee Status"),
fieldtype: "Select",
options: [
"",
{ "value": "Active", "label": __("Active") },
{ "value": "Inactive", "label": __("Inactive") },
{ "value": "Suspended", "label": __("Suspended") },
{ "value": "Left", "label": __("Left") },
],
"default": "Active",
default: "Active",
},
{
fieldname: "consolidate_leave_types",
label: __("Consolidate Leave Types"),
fieldtype: "Check",
default: 1,
depends_on: "eval: !doc.employee",
}
],

View File

@@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Tuple
import frappe
from frappe import _
from frappe.utils import add_days, getdate
from frappe.utils import add_days, cint, getdate
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation
from erpnext.hr.doctype.leave_application.leave_application import (
@@ -24,7 +24,7 @@ def execute(filters: Optional[Filters] = None) -> Tuple:
columns = get_columns()
data = get_data(filters)
charts = get_chart_data(data)
charts = get_chart_data(data, filters)
return columns, data, None, charts
@@ -89,7 +89,7 @@ def get_data(filters: Filters) -> List:
conditions = get_conditions(filters)
user = frappe.session.user
department_approver_map = get_department_leave_approver_map(filters.get("department"))
department_approver_map = get_department_leave_approver_map(filters.department)
active_employees = frappe.get_list(
"Employee",
@@ -97,48 +97,49 @@ def get_data(filters: Filters) -> List:
fields=["name", "employee_name", "department", "user_id", "leave_approver"],
)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
consolidate_leave_types = len(active_employees) > 1 and filters.consolidate_leave_types
row = None
data = []
for leave_type in leave_types:
if len(active_employees) > 1:
if consolidate_leave_types:
data.append({"leave_type": leave_type})
else:
row = frappe._dict({"leave_type": leave_type})
for employee in active_employees:
leave_approvers = department_approver_map.get(employee.department_name, []).append(
employee.leave_approver
)
if (
(leave_approvers and len(leave_approvers) and user in leave_approvers)
or (user in ["Administrator", employee.user_id])
or ("HR Manager" in frappe.get_roles(user))
):
if len(active_employees) > 1:
row = frappe._dict()
row.employee = employee.name
row.employee_name = employee.employee_name
if consolidate_leave_types:
row = frappe._dict()
else:
row = frappe._dict({"leave_type": leave_type})
leaves_taken = (
get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1
)
row.employee = employee.name
row.employee_name = employee.employee_name
new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
filters.from_date, filters.to_date, employee.name, leave_type
)
opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
leaves_taken = (
get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1
)
row.leaves_allocated = new_allocation
row.leaves_expired = expired_leaves
row.opening_balance = opening
row.leaves_taken = leaves_taken
new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves(
filters.from_date, filters.to_date, employee.name, leave_type
)
opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves)
# not be shown on the basis of days left it create in user mind for carry_forward leave
row.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken)
row.indent = 1
data.append(row)
row.leaves_allocated = new_allocation
row.leaves_expired = expired_leaves
row.opening_balance = opening
row.leaves_taken = leaves_taken
# not be shown on the basis of days left it create in user mind for carry_forward leave
row.closing_balance = new_allocation + opening - (row.leaves_expired + leaves_taken)
row.indent = 1
data.append(row)
return data
@@ -170,17 +171,17 @@ def get_opening_balance(
def get_conditions(filters: Filters) -> Dict:
conditions = {}
if filters.get("employee"):
conditions["name"] = filters.get("employee")
if filters.employee:
conditions["name"] = filters.employee
if filters.get("company"):
conditions["company"] = filters.get("company")
if filters.company:
conditions["company"] = filters.company
if filters.get("department"):
conditions["department"] = filters.get("department")
if filters.department:
conditions["department"] = filters.department
if filters.get("employee_status"):
conditions["status"] = filters.get("employee_status")
if filters.employee_status:
conditions["status"] = filters.employee_status
return conditions
@@ -272,12 +273,15 @@ def get_leave_ledger_entries(
return records
def get_chart_data(data: List) -> Dict:
def get_chart_data(data: List, filters: Filters) -> Dict:
labels = []
datasets = []
employee_data = data
if data and data[0].get("employee_name"):
if not data:
return None
if data and filters.employee:
get_dataset_for_chart(employee_data, datasets, labels)
chart = {

View File

@@ -154,7 +154,6 @@ class TestEmployeeLeaveBalance(unittest.TestCase):
@set_holiday_list("_Test Emp Balance Holiday List", "_Test Company")
def test_opening_balance_considers_carry_forwarded_leaves(self):
leave_type = create_leave_type(leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1)
leave_type.insert()
# 30 leaves allocated for first half of the year
allocation1 = make_allocation_record(

View File

@@ -65,21 +65,16 @@ def get_data(filters, leave_types):
if employee.leave_approver:
leave_approvers.append(employee.leave_approver)
if (
(len(leave_approvers) and user in leave_approvers)
or (user in ["Administrator", employee.user_id])
or ("HR Manager" in frappe.get_roles(user))
):
row = [employee.name, employee.employee_name, employee.department]
available_leave = get_leave_details(employee.name, filters.date)
for leave_type in leave_types:
remaining = 0
if leave_type in available_leave["leave_allocation"]:
# opening balance
remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"]
row = [employee.name, employee.employee_name, employee.department]
available_leave = get_leave_details(employee.name, filters.date)
for leave_type in leave_types:
remaining = 0
if leave_type in available_leave["leave_allocation"]:
# opening balance
remaining = available_leave["leave_allocation"][leave_type]["remaining_leaves"]
row += [remaining]
row += [remaining]
data.append(row)
data.append(row)
return data

View File

@@ -64,8 +64,6 @@
"fieldtype": "Section Break"
},
{
"fetch_from": "prevdoc_detail_docname.sales_person",
"fetch_if_empty": 1,
"fieldname": "service_person",
"fieldtype": "Link",
"in_list_view": 1,
@@ -110,13 +108,15 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-05-27 17:47:21.474282",
"modified": "2023-02-27 11:09:33.114458",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit Purpose",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', {
},
setup: function(frm) {
frm.custom_make_buttons = {
'Purchase Order': 'Purchase Order',
'Sales Order': 'Sales Order',
'Quotation': 'Quotation',
};
frm.add_fetch("customer", "customer_name", "customer_name");
frm.add_fetch("supplier", "supplier_name", "supplier_name");
},

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from erpnext.stock.doctype.item.item import get_item_defaults
@@ -29,21 +30,23 @@ class BlanketOrder(Document):
def update_ordered_qty(self):
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
trans = frappe.qb.DocType(ref_doctype)
trans_item = frappe.qb.DocType(f"{ref_doctype} Item")
item_ordered_qty = frappe._dict(
frappe.db.sql(
"""
select trans_item.item_code, sum(trans_item.stock_qty) as qty
from `tab{0} Item` trans_item, `tab{0}` trans
where trans.name = trans_item.parent
and trans_item.blanket_order=%s
and trans.docstatus=1
and trans.status not in ('Closed', 'Stopped')
group by trans_item.item_code
""".format(
ref_doctype
),
self.name,
)
(
frappe.qb.from_(trans_item)
.from_(trans)
.select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty"))
.where(
(trans.name == trans_item.parent)
& (trans_item.blanket_order == self.name)
& (trans.docstatus == 1)
& (trans.status.notin(["Stopped", "Closed"]))
)
.groupby(trans_item.item_code)
).run()
)
for d in self.items:
@@ -79,7 +82,43 @@ def make_order(source_name):
"doctype": doctype + " Item",
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
"postprocess": update_item,
"condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0,
},
},
)
return target_doc
def validate_against_blanket_order(order_doc):
if order_doc.doctype in ("Sales Order", "Purchase Order"):
order_data = {}
for item in order_doc.get("items"):
if item.against_blanket_order and item.blanket_order:
if item.blanket_order in order_data:
if item.item_code in order_data[item.blanket_order]:
order_data[item.blanket_order][item.item_code] += item.qty
else:
order_data[item.blanket_order][item.item_code] = item.qty
else:
order_data[item.blanket_order] = {item.item_code: item.qty}
if order_data:
allowance = flt(
frappe.db.get_single_value(
"Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings",
"over_order_allowance",
)
)
for bo_name, item_data in order_data.items():
bo_doc = frappe.get_doc("Blanket Order", bo_name)
for item in bo_doc.get("items"):
if item.item_code in item_data:
remaining_qty = item.qty - item.ordered_qty
allowed_qty = remaining_qty + (remaining_qty * (allowance / 100))
if allowed_qty < item_data[item.item_code]:
frappe.throw(
_("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format(
item.item_code, allowed_qty, bo_name
)
)

View File

@@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase):
po1.currency = get_company_currency(po1.company)
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
def test_over_order_allowance(self):
# Sales Order
bo = make_blanket_order(blanket_order_type="Selling", quantity=100)
frappe.flags.args.doctype = "Sales Order"
so = make_order(bo.name)
so.currency = get_company_currency(so.company)
so.delivery_date = today()
so.items[0].qty = 110
self.assertRaises(frappe.ValidationError, so.submit)
frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10)
so.submit()
# Purchase Order
bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100)
frappe.flags.args.doctype = "Purchase Order"
po = make_order(bo.name)
po.currency = get_company_currency(po.company)
po.schedule_date = today()
po.items[0].qty = 110
self.assertRaises(frappe.ValidationError, po.submit)
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
po.submit()
def make_blanket_order(**args):
args = frappe._dict(args)

View File

@@ -31,7 +31,7 @@ class BOMTree:
# specifying the attributes to save resources
# ref: https://docs.python.org/3/reference/datamodel.html#slots
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
__slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"]
def __init__(
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1
@@ -50,9 +50,10 @@ class BOMTree:
def __create_tree(self):
bom = frappe.get_cached_doc("BOM", self.name)
self.item_code = bom.item
self.bom_qty = bom.quantity
for item in bom.get("items", []):
qty = item.qty / bom.quantity # quantity per unit
qty = item.stock_qty / bom.quantity # quantity per unit
exploded_qty = self.exploded_qty * qty
if item.bom_no:
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)

View File

@@ -536,7 +536,34 @@ class JobCard(Document):
)
def set_transferred_qty_in_job_card_item(self, ste_doc):
from frappe.query_builder.functions import Sum
def _get_job_card_items_transferred_qty(ste_doc):
from frappe.query_builder.functions import Sum
job_card_items_transferred_qty = {}
job_card_items = [
x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")
]
if job_card_items:
se = frappe.qb.DocType("Stock Entry")
sed = frappe.qb.DocType("Stock Entry Detail")
query = (
frappe.qb.from_(sed)
.join(se)
.on(sed.parent == se.name)
.select(sed.job_card_item, Sum(sed.qty))
.where(
(sed.job_card_item.isin(job_card_items))
& (se.docstatus == 1)
& (se.purpose == "Material Transfer for Manufacture")
)
.groupby(sed.job_card_item)
)
job_card_items_transferred_qty = frappe._dict(query.run(as_list=True))
return job_card_items_transferred_qty
def _validate_over_transfer(row, transferred_qty):
"Block over transfer of items if not allowed in settings."
@@ -553,29 +580,23 @@ class JobCard(Document):
exc=JobCardOverTransferError,
)
for row in ste_doc.items:
if not row.job_card_item:
continue
sed = frappe.qb.DocType("Stock Entry Detail")
se = frappe.qb.DocType("Stock Entry")
transferred_qty = (
frappe.qb.from_(sed)
.join(se)
.on(sed.parent == se.name)
.select(Sum(sed.qty))
.where(
(sed.job_card_item == row.job_card_item)
& (se.docstatus == 1)
& (se.purpose == "Material Transfer for Manufacture")
)
).run()[0][0]
job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
if job_card_items_transferred_qty:
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
if not allow_excess:
_validate_over_transfer(row, transferred_qty)
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
for row in ste_doc.items:
if not row.job_card_item:
continue
transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item))
if not allow_excess:
_validate_over_transfer(row, transferred_qty)
frappe.db.set_value(
"Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)
)
def set_transferred_qty(self, update_status=False):
"Set total FG Qty in Job Card for which RM was transferred."

View File

@@ -476,7 +476,7 @@ frappe.ui.form.on("Work Order Item", {
callback: function(r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, {
"required_qty": 1,
"required_qty": row.required_qty || 1,
"item_name": r.message.item_name,
"description": r.message.description,
"source_warehouse": r.message.default_warehouse,

View File

@@ -690,7 +690,7 @@ class WorkOrder(Document):
for node in bom_traversal:
if node.is_bom:
operations.extend(_get_operations(node.name, qty=node.exploded_qty))
operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty))
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))

View File

@@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = {
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.id == "item") {
if (data["enough_parts_to_build"] > 0) {
if (data["in_stock_qty"] >= data["required_qty"]) {
value = `<a style='color:green' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else {
value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;

View File

@@ -4,7 +4,8 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Floor, Sum
from frappe.utils import cint
from pypika.terms import ExistsCriterion
@@ -34,57 +35,55 @@ def get_columns():
def get_bom_stock(filters):
qty_to_produce = filters.get("qty_to_produce") or 1
if int(qty_to_produce) < 0:
frappe.throw(_("Quantity to Produce can not be less than Zero"))
qty_to_produce = filters.get("qty_to_produce")
if cint(qty_to_produce) <= 0:
frappe.throw(_("Quantity to Produce should be greater than zero."))
if filters.get("show_exploded_view"):
bom_item_table = "BOM Explosion Item"
else:
bom_item_table = "BOM Item"
bin = frappe.qb.DocType("Bin")
bom = frappe.qb.DocType("BOM")
bom_item = frappe.qb.DocType(bom_item_table)
query = (
frappe.qb.from_(bom)
.inner_join(bom_item)
.on(bom.name == bom_item.parent)
.left_join(bin)
.on(bom_item.item_code == bin.item_code)
.select(
bom_item.item_code,
bom_item.description,
bom_item.stock_qty,
bom_item.stock_uom,
(bom_item.stock_qty / bom.quantity) * qty_to_produce,
Sum(bin.actual_qty),
Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
)
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
.groupby(bom_item.item_code)
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
BOM = frappe.qb.DocType("BOM")
BOM_ITEM = frappe.qb.DocType(bom_item_table)
BIN = frappe.qb.DocType("Bin")
WH = frappe.qb.DocType("Warehouse")
CONDITIONS = ()
if warehouse_details:
wh = frappe.qb.DocType("Warehouse")
query = query.where(
ExistsCriterion(
frappe.qb.from_(wh)
.select(wh.name)
.where(
(wh.lft >= warehouse_details.lft)
& (wh.rgt <= warehouse_details.rgt)
& (bin.warehouse == wh.name)
)
)
if warehouse_details:
CONDITIONS = ExistsCriterion(
frappe.qb.from_(WH)
.select(WH.name)
.where(
(WH.lft >= warehouse_details.lft)
& (WH.rgt <= warehouse_details.rgt)
& (BIN.warehouse == WH.name)
)
else:
query = query.where(bin.warehouse == filters.get("warehouse"))
)
else:
CONDITIONS = BIN.warehouse == filters.get("warehouse")
return query.run()
QUERY = (
frappe.qb.from_(BOM)
.inner_join(BOM_ITEM)
.on(BOM.name == BOM_ITEM.parent)
.left_join(BIN)
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
.select(
BOM_ITEM.item_code,
BOM_ITEM.description,
BOM_ITEM.stock_qty,
BOM_ITEM.stock_uom,
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
Sum(BIN.actual_qty).as_("actual_qty"),
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
)
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
.groupby(BOM_ITEM.item_code)
)
return QUERY.run()

View File

@@ -0,0 +1,110 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.exceptions import ValidationError
from frappe.tests.utils import FrappeTestCase
from frappe.utils import floor
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
get_bom_stock as bom_stock_report,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestBomStockReport(FrappeTestCase):
def setUp(self):
self.warehouse = "_Test Warehouse - _TC"
self.fg_item, self.rm_items = create_items()
make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
def test_bom_stock_report(self):
# Test 1: When `qty_to_produce` is 0.
filters = frappe._dict(
{
"bom": self.bom.name,
"warehouse": "Stores - _TC",
"qty_to_produce": 0,
}
)
self.assertRaises(ValidationError, bom_stock_report, filters)
# Test 2: When stock is not available.
data = bom_stock_report(
frappe._dict(
{
"bom": self.bom.name,
"warehouse": "Stores - _TC",
"qty_to_produce": 1,
}
)
)
expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
# Test 3: When stock is available.
data = bom_stock_report(
frappe._dict(
{
"bom": self.bom.name,
"warehouse": self.warehouse,
"qty_to_produce": 1,
}
)
)
expected_data = get_expected_data(self.bom, self.warehouse, 1)
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
def create_items():
fg_item = make_item(properties={"is_stock_item": 1}).name
rm_item1 = make_item(
properties={
"is_stock_item": 1,
"standard_rate": 100,
"opening_stock": 100,
"last_purchase_rate": 100,
}
).name
rm_item2 = make_item(
properties={
"is_stock_item": 1,
"standard_rate": 200,
"opening_stock": 200,
"last_purchase_rate": 200,
}
).name
return fg_item, [rm_item1, rm_item2]
def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
expected_data = []
for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
in_stock_qty = None
if frappe.db.exists("Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"):
in_stock_qty = frappe.get_cached_value(
"Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
)
expected_data.append(
[
item.item_code,
item.description,
item.stock_qty,
item.stock_uom,
item.stock_qty * qty_to_produce / bom.quantity,
in_stock_qty,
floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
if in_stock_qty
else None,
]
)
return expected_data

View File

@@ -13,38 +13,24 @@ frappe.query_reports["Work Order Summary"] = {
reqd: 1
},
{
fieldname: "fiscal_year",
label: __("Fiscal Year"),
fieldtype: "Link",
options: "Fiscal Year",
default: frappe.defaults.get_user_default("fiscal_year"),
reqd: 1,
on_change: function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year;
if (!fiscal_year) {
return;
}
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
from_date: fy.year_start_date,
to_date: fy.year_end_date
});
});
}
label: __("Based On"),
fieldname:"based_on",
fieldtype: "Select",
options: "Creation Date\nPlanned Date\nActual Date",
default: "Creation Date"
},
{
label: __("From Posting Date"),
fieldname:"from_date",
fieldtype: "Date",
default: frappe.defaults.get_user_default("year_start_date"),
default: frappe.datetime.add_months(frappe.datetime.get_today(), -3),
reqd: 1
},
{
label: __("To Posting Date"),
fieldname:"to_date",
fieldtype: "Date",
default: frappe.defaults.get_user_default("year_end_date"),
default: frappe.datetime.get_today(),
reqd: 1,
},
{

View File

@@ -31,6 +31,7 @@ def get_data(filters):
"sales_order",
"production_item",
"qty",
"creation",
"produced_qty",
"planned_start_date",
"planned_end_date",
@@ -47,8 +48,14 @@ def get_data(filters):
if filters.get(field):
query_filters[field] = filters.get(field)
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
if filters.get("based_on") == "Planned Date":
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
elif filters.get("based_on") == "Actual Date":
query_filters["actual_start_date"] = (">=", filters.get("from_date"))
query_filters["actual_end_date"] = ("<=", filters.get("to_date"))
else:
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
data = frappe.get_all(
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
@@ -212,6 +219,12 @@ def get_columns(filters):
"options": "Sales Order",
"width": 90,
},
{
"label": _("Created On"),
"fieldname": "creation",
"fieldtype": "Date",
"width": 150,
},
{
"label": _("Planned Start Date"),
"fieldname": "planned_start_date",

View File

@@ -374,4 +374,6 @@ erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0})
erpnext.patches.v13_0.update_schedule_type_in_loans
erpnext.patches.v13_0.update_schedule_type_in_loans
erpnext.patches.v13_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v13_0.update_docs_link

View File

@@ -7,6 +7,7 @@ from erpnext.stock.stock_ledger import update_entries_after
def execute():
doctypes_to_reload = [
("setup", "company"),
("stock", "repost_item_valuation"),
("stock", "stock_entry_detail"),
("stock", "purchase_receipt_item"),

View File

@@ -1,16 +1,61 @@
import frappe
from erpnext.regional.india.setup import make_custom_fields
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
if frappe.get_all("Company", filters={"country": "India"}):
frappe.reload_doc("accounts", "doctype", "POS Invoice")
frappe.reload_doc("accounts", "doctype", "POS Invoice Item")
make_custom_fields()
custom_fields = get_non_profit_custom_fields()
create_custom_fields(custom_fields, update=True)
if not frappe.db.exists("Party Type", "Donor"):
frappe.get_doc(
{"doctype": "Party Type", "party_type": "Donor", "account_type": "Receivable"}
).insert(ignore_permissions=True)
).insert(ignore_permissions=True, ignore_mandatory=True)
def get_non_profit_custom_fields():
return {
"Company": [
{
"fieldname": "non_profit_section",
"label": "Non Profit Settings",
"fieldtype": "Section Break",
"insert_after": "asset_received_but_not_billed",
"collapsible": 1,
},
{
"fieldname": "company_80g_number",
"label": "80G Number",
"fieldtype": "Data",
"insert_after": "non_profit_section",
},
{
"fieldname": "with_effect_from",
"label": "80G With Effect From",
"fieldtype": "Date",
"insert_after": "company_80g_number",
},
{
"fieldname": "pan_details",
"label": "PAN Number",
"fieldtype": "Data",
"insert_after": "with_effect_from",
},
],
"Member": [
{
"fieldname": "pan_number",
"label": "PAN Details",
"fieldtype": "Data",
"insert_after": "email_id",
},
],
"Donor": [
{
"fieldname": "pan_number",
"label": "PAN Details",
"fieldtype": "Data",
"insert_after": "email",
},
],
}

View File

@@ -0,0 +1,38 @@
import frappe
from frappe.query_builder.functions import IfNull, Sum
def execute():
asset = frappe.qb.DocType("Asset")
gle = frappe.qb.DocType("GL Entry")
aca = frappe.qb.DocType("Asset Category Account")
company = frappe.qb.DocType("Company")
asset_total_depr_value_map = (
frappe.qb.from_(gle)
.join(asset)
.on(gle.against_voucher == asset.name)
.join(aca)
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
.join(company)
.on(company.name == asset.company)
.select(Sum(gle.debit).as_("value"), asset.name.as_("asset_name"))
.where(
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
)
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.where(asset.docstatus == 1)
.where(asset.calculate_depreciation == 0)
.groupby(asset.name)
)
frappe.qb.update(asset).join(asset_total_depr_value_map).on(
asset_total_depr_value_map.asset_name == asset.name
).set(
asset.value_after_depreciation, asset.value_after_depreciation - asset_total_depr_value_map.value
).where(
asset.docstatus == 1
).where(
asset.calculate_depreciation == 0
).run()

View File

@@ -0,0 +1,14 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
def execute():
navbar_settings = frappe.get_single("Navbar Settings")
for item in navbar_settings.help_dropdown:
if item.is_standard and item.route == "https://erpnext.com/docs/user/manual":
item.route = "https://docs.erpnext.com/docs/v13/user/manual/en/introduction"
navbar_settings.save()

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import datetime
import math
@@ -316,6 +315,8 @@ class SalarySlip(TransactionBase):
)
working_days = date_diff(self.end_date, self.start_date) + 1
working_days_list = [add_days(self.start_date, i) for i in range(working_days)]
if for_preview:
self.total_working_days = working_days
self.payment_days = working_days
@@ -323,8 +324,12 @@ class SalarySlip(TransactionBase):
holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
joining_date, relieving_date = self.get_joining_and_relieving_dates()
if not cint(include_holidays_in_total_working_days):
working_days -= len(holidays)
working_days_list = [cstr(day) for day in working_days_list if cstr(day) not in holidays]
if working_days < 0:
frappe.throw(_("There are more holidays than working days this month."))
@@ -332,10 +337,14 @@ class SalarySlip(TransactionBase):
frappe.throw(_("Please set Payroll based on in Payroll settings"))
if payroll_based_on == "Attendance":
actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays)
actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(
holidays, relieving_date
)
self.absent_days = absent
else:
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days)
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(
holidays, working_days_list, relieving_date
)
if not lwp:
lwp = actual_lwp
@@ -458,16 +467,21 @@ class SalarySlip(TransactionBase):
def get_holidays_for_employee(self, start_date, end_date):
return get_holiday_dates_for_employee(self.employee, start_date, end_date)
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
def calculate_lwp_or_ppl_based_on_leave_application(
self, holidays, working_days_list, relieving_date=None
):
lwp = 0
holidays = "','".join(holidays)
daily_wages_fraction_for_half_day = (
flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
)
for d in range(working_days):
date = add_days(cstr(getdate(self.start_date)), d)
leave = get_lwp_or_ppl_for_date(date, self.employee, holidays)
for d in working_days_list:
if relieving_date and getdate(d) > getdate(relieving_date):
break
leave = get_lwp_or_ppl_for_date(d, self.employee, holidays)
if leave:
equivalent_lwp_count = 0
@@ -486,10 +500,15 @@ class SalarySlip(TransactionBase):
return lwp
def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays):
def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays, relieving_date=None):
lwp = 0
absent = 0
end_date = self.end_date
if relieving_date:
end_date = relieving_date
daily_wages_fraction_for_half_day = (
flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
)
@@ -504,7 +523,7 @@ class SalarySlip(TransactionBase):
for leave_type in leave_types:
leave_type_map[leave_type.name] = leave_type
attendances = frappe.db.sql(
attendances = frappe.db.sql( # nosemgrep
"""
SELECT attendance_date, status, leave_type
FROM `tabAttendance`
@@ -514,7 +533,7 @@ class SalarySlip(TransactionBase):
AND docstatus = 1
AND attendance_date between %s and %s
""",
values=(self.employee, self.start_date, self.end_date),
values=(self.employee, self.start_date, end_date),
as_dict=1,
)

View File

@@ -267,7 +267,6 @@ class TestSalarySlip(FrappeTestCase):
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl=1)
leave_type_ppl.save()
alloc = create_leave_allocation(
employee=emp_id,
@@ -1128,6 +1127,35 @@ class TestSalarySlip(FrappeTestCase):
if deduction.salary_component == "TDS":
self.assertEqual(deduction.amount, rounded(monthly_tax_amount))
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
def test_lwp_calculation_based_on_relieving_date(self):
emp_id = make_employee("test_lwp_based_on_relieving_date@salary.com")
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
month_start_date = get_first_day(nowdate())
first_sunday = get_first_sunday(for_date=month_start_date)
relieving_date = add_days(first_sunday, 10)
leave_start_date = add_days(first_sunday, 16)
leave_end_date = add_days(leave_start_date, 2)
make_leave_application(emp_id, leave_start_date, leave_end_date, "Leave Without Pay")
frappe.db.set_value("Employee", emp_id, {"relieving_date": relieving_date, "status": "Left"})
ss = make_employee_salary_slip(
"test_lwp_based_on_relieving_date@salary.com",
"Monthly",
"Test Payment Based On Leave Application",
)
holidays = ss.get_holidays_for_employee(month_start_date, relieving_date)
days_between_start_and_relieving = date_diff(relieving_date, month_start_date) + 1
self.assertEqual(ss.leave_without_pay, 0)
self.assertEqual(ss.payment_days, (days_between_start_and_relieving - len(holidays)))
def get_no_of_days():
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
@@ -1587,9 +1615,8 @@ def setup_test():
frappe.db.set_value("HR Settings", None, "leave_approval_notification_template", None)
def make_holiday_list(list_name=None, from_date=None, to_date=None):
if not (from_date and to_date):
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
def make_holiday_list(list_name=None, from_date=None, to_date=None, add_weekly_offs=True):
fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company())
name = list_name or "Salary Slip Test Holiday List"
frappe.delete_doc_if_exists("Holiday List", name, force=True)
@@ -1600,10 +1627,13 @@ def make_holiday_list(list_name=None, from_date=None, to_date=None):
"holiday_list_name": name,
"from_date": from_date or fiscal_year[1],
"to_date": to_date or fiscal_year[2],
"weekly_off": "Sunday",
}
).insert()
holiday_list.get_weekly_off_dates()
if add_weekly_offs:
holiday_list.weekly_off = "Sunday"
holiday_list.get_weekly_off_dates()
holiday_list.save()
holiday_list = holiday_list.name

View File

@@ -115,24 +115,25 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
calculate_item_values: function() {
let me = this;
if (!this.discount_amount_applied) {
$.each(this.frm.doc["items"] || [], function(i, item) {
for (const item of this.frm.doc.items || []) {
frappe.model.round_floats_in(item);
item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
if ((!item.qty) && me.frm.doc.is_return) {
item.amount = flt(item.rate * -1, precision("amount", item));
} else if ((!item.qty) && me.frm.doc.is_debit_note) {
item.amount = flt(item.rate, precision("amount", item));
} else {
item.amount = flt(item.rate * item.qty, precision("amount", item));
if (!(me.frm.doc.is_return || me.frm.doc.is_debit_note)) {
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
}
else {
// allow for '0' qty on Credit/Debit notes
let qty = item.qty || me.frm.doc.is_debit_note ? 1 : -1;
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}
item.net_amount = item.amount;
item.item_tax_amount = 0.0;
item.total_weight = flt(item.weight_per_unit * item.stock_qty);
me.set_in_company_currency(item, ["price_list_rate", "rate", "amount", "net_rate", "net_amount"]);
});
}
}
},

View File

@@ -1781,6 +1781,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
var me = this;
var valid = true;
if (frappe.flags.ignore_company_party_validation) {
return valid;
}
$.each(["company", "customer"], function(i, fieldname) {
if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && me.frm.doc.doctype != "Purchase Order") {
if (!me.frm.doc[fieldname]) {
@@ -1971,11 +1975,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
get_advances: function() {
if(!this.frm.is_return) {
var me = this;
return this.frm.call({
method: "set_advances",
doc: this.frm.doc,
callback: function(r, rt) {
refresh_field("advances");
me.frm.dirty();
}
})
}

View File

@@ -466,7 +466,20 @@ erpnext.utils.update_child_items = function(opts) {
const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`);
const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision;
this.data = [];
this.data = frm.doc[opts.child_docname].map((d) => {
return {
"docname": d.name,
"name": d.name,
"item_code": d.item_code,
"delivery_date": d.delivery_date,
"schedule_date": d.schedule_date,
"conversion_factor": d.conversion_factor,
"qty": d.qty,
"rate": d.rate,
"uom": d.uom
}
});
const fields = [{
fieldtype:'Data',
fieldname:"docname",
@@ -559,7 +572,7 @@ erpnext.utils.update_child_items = function(opts) {
})
}
const dialog = new frappe.ui.Dialog({
new frappe.ui.Dialog({
title: __("Update Items"),
fields: [
{
@@ -595,24 +608,7 @@ erpnext.utils.update_child_items = function(opts) {
refresh_field("items");
},
primary_action_label: __('Update')
});
frm.doc[opts.child_docname].forEach(d => {
dialog.fields_dict.trans_items.df.data.push({
"docname": d.name,
"name": d.name,
"item_code": d.item_code,
"delivery_date": d.delivery_date,
"schedule_date": d.schedule_date,
"conversion_factor": d.conversion_factor,
"qty": d.qty,
"rate": d.rate,
"uom": d.uom
});
this.data = dialog.fields_dict.trans_items.df.data;
dialog.fields_dict.trans_items.grid.refresh();
})
dialog.show();
}).show();
}
erpnext.utils.map_current_doc = function(opts) {

View File

@@ -71,7 +71,11 @@ def validate_eligibility(doc):
# if export invoice, then taxes can be empty
# invoice can only be ineligible if no taxes applied and is not an export invoice
no_taxes_applied = not doc.get("taxes") and not doc.get("gst_category") == "Overseas"
no_taxes_applied = (
not doc.get("taxes")
and not doc.get("gst_category") == "Overseas"
and not doc.get("gst_category") == "SEZ"
)
has_non_gst_item = any(d for d in doc.get("items", []) if d.get("is_non_gst"))
if (

View File

@@ -1101,18 +1101,21 @@ def update_taxable_values(doc, method):
def get_depreciation_amount(asset, depreciable_value, row):
if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
depreciation_amount = (
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
depreciation_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
elif asset.flags.increase_in_asset_value_due_to_repair:
depreciation_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being prepared for the first time
else:
depreciation_amount = (
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
else:
rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation

View File

@@ -84,11 +84,15 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({
}
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
this.frm.add_custom_button(
__("Sales Order"),
this.frm.cscript["Make Sales Order"],
__("Create")
);
if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation
|| (!doc.valid_till)
|| frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
this.frm.add_custom_button(
__("Sales Order"),
this.frm.cscript["Make Sales Order"],
__("Create")
);
}
if(doc.status!=="Ordered") {
this.frm.add_custom_button(__('Set as Lost'), () => {

View File

@@ -191,14 +191,18 @@ def get_list_context(context=None):
@frappe.whitelist()
def make_sales_order(source_name, target_doc=None):
quotation = frappe.db.get_value(
"Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
)
if quotation.valid_till and (
quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
def make_sales_order(source_name: str, target_doc=None):
if not frappe.db.get_singles_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
):
frappe.throw(_("Validity period of this quotation has ended."))
quotation = frappe.db.get_value(
"Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
)
if quotation.valid_till and (
quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
):
frappe.throw(_("Validity period of this quotation has ended."))
return _make_sales_order(source_name, target_doc)

View File

@@ -118,18 +118,31 @@ class TestQuotation(FrappeTestCase):
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
)
def test_valid_till(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
def test_valid_till_before_transaction_date(self):
quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(quotation.transaction_date, -1)
self.assertRaises(frappe.ValidationError, quotation.validate)
def test_so_from_expired_quotation(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
frappe.db.set_single_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0
)
quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(nowdate(), -1)
quotation.insert()
quotation.submit()
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
frappe.db.set_single_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1
)
make_sales_order(quotation.name)
def test_shopping_cart_without_website_item(self):
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):
frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete()

View File

@@ -280,9 +280,12 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
make_work_order() {
var me = this;
this.frm.call({
doc: this.frm.doc,
method: 'get_work_order_items',
me.frm.call({
method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
args: {
sales_order: this.frm.docname,
},
freeze: true,
callback: function(r) {
if(!r.message) {
frappe.msgprint({
@@ -292,14 +295,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
});
return;
}
else if(!r.message) {
frappe.msgprint({
title: __('Work Order not created'),
message: __('Work Order already created for all items with BOM'),
indicator: 'orange'
});
return;
} else {
else {
const fields = [{
label: 'Items',
fieldtype: 'Table',
@@ -400,9 +396,9 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
make_raw_material_request: function() {
var me = this;
this.frm.call({
doc: this.frm.doc,
method: 'get_work_order_items',
method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
args: {
sales_order: this.frm.docname,
for_raw_material_request: 1
},
callback: function(r) {
@@ -421,6 +417,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
},
make_raw_material_request_dialog: function(r) {
var me = this;
var fields = [
{fieldtype:'Check', fieldname:'include_exploded_items',
label: __('Include Exploded Items')},

View File

@@ -6,11 +6,12 @@ import json
import frappe
import frappe.utils
from frappe import _
from frappe import _, qb
from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html
from six import string_types
@@ -21,6 +22,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
)
from erpnext.accounts.party import get_party_account
from erpnext.controllers.selling_controller import SellingController
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
validate_against_blanket_order,
)
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
)
@@ -52,6 +56,7 @@ class SalesOrder(SellingController):
self.validate_warehouse()
self.validate_drop_ship()
self.validate_serial_no_based_delivery()
validate_against_blanket_order(self)
validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_order_reference
)
@@ -481,51 +486,6 @@ class SalesOrder(SellingController):
self.indicator_color = "green"
self.indicator_title = _("Paid")
@frappe.whitelist()
def get_work_order_items(self, for_raw_material_request=0):
"""Returns items with BOM that already do not have a linked work order"""
items = []
item_codes = [i.item_code for i in self.items]
product_bundle_parents = [
pb.new_item_code
for pb in frappe.get_all(
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
)
]
for table in [self.items, self.packed_items]:
for i in table:
bom = get_default_bom(i.item_code)
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
if not for_raw_material_request:
total_work_order_qty = flt(
frappe.db.sql(
"""select sum(qty) from `tabWork Order`
where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""",
(i.item_code, self.name, i.name),
)[0][0]
)
pending_qty = stock_qty - total_work_order_qty
else:
pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents:
items.append(
dict(
name=i.name,
item_code=i.item_code,
description=i.description,
bom=bom or "",
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,
sales_order_item=i.name,
)
)
return items
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
@@ -1399,3 +1359,57 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
return
frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty)
@frappe.whitelist()
def get_work_order_items(sales_order, for_raw_material_request=0):
"""Returns items with BOM that already do not have a linked work order"""
if sales_order:
so = frappe.get_doc("Sales Order", sales_order)
wo = qb.DocType("Work Order")
items = []
item_codes = [i.item_code for i in so.items]
product_bundle_parents = [
pb.new_item_code
for pb in frappe.get_all(
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
)
]
for table in [so.items, so.packed_items]:
for i in table:
bom = get_default_bom(i.item_code)
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
if not for_raw_material_request:
total_work_order_qty = flt(
qb.from_(wo)
.select(Sum(wo.qty))
.where(
(wo.production_item == i.item_code)
& (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
& (wo.docstatus.lte(2))
)
.run()[0][0]
)
pending_qty = stock_qty - total_work_order_qty
else:
pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents:
items.append(
dict(
name=i.name,
item_code=i.item_code,
description=i.description,
bom=bom or "",
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,
sales_order_item=i.name,
)
)
return items

View File

@@ -14,7 +14,6 @@ def get_data():
},
"internal_links": {
"Quotation": ["items", "prevdoc_docname"],
"Material Request": ["items", "material_request"],
},
"transactions": [
{

View File

@@ -546,6 +546,42 @@ class TestSalesOrder(FrappeTestCase):
workflow.is_active = 0
workflow.save()
def test_bin_details_of_packed_item(self):
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
bundle_item.append(
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
)
bundle_item.save(ignore_permissions=True)
make_item("_Packed Item New 1", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
so = make_sales_order(
item_code="_Test Product Bundle Item New",
warehouse="_Test Warehouse - _TC",
transaction_date=add_days(nowdate(), -1),
do_not_submit=1,
)
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
bin_details = frappe.db.get_value(
"Bin",
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
["actual_qty", "projected_qty", "ordered_qty"],
as_dict=1,
)
so.transaction_date = nowdate()
so.save()
packed_item = so.packed_items[0]
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
def test_update_child_product_bundle(self):
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Product Bundle Item"):
@@ -1175,6 +1211,8 @@ class TestSalesOrder(FrappeTestCase):
self.assertTrue(si.get("payment_schedule"))
def test_make_work_order(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
# Make a new Sales Order
so = make_sales_order(
**{
@@ -1188,7 +1226,7 @@ class TestSalesOrder(FrappeTestCase):
# Raise Work Orders
po_items = []
so_item_name = {}
for item in so.get_work_order_items():
for item in get_work_order_items(so.name):
po_items.append(
{
"warehouse": item.get("warehouse"),
@@ -1379,6 +1417,7 @@ class TestSalesOrder(FrappeTestCase):
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
make_item( # template item
"Test-WO-Tshirt",
@@ -1418,7 +1457,7 @@ class TestSalesOrder(FrappeTestCase):
]
}
)
wo_items = so.get_work_order_items()
wo_items = get_work_order_items(so.name)
self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
@@ -1428,6 +1467,8 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
item = make_item(
"_Test Finished Item",
{
@@ -1460,7 +1501,7 @@ class TestSalesOrder(FrappeTestCase):
so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]})
so.submit()
mr_dict = frappe._dict()
items = so.get_work_order_items(1)
items = get_work_order_items(so.name, 1)
mr_dict["items"] = items
mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1

View File

@@ -809,7 +809,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-04-27 03:15:34.366563",
"modified": "2022-12-25 02:51:10.247569",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
@@ -820,4 +820,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -30,11 +30,18 @@
"so_required",
"dn_required",
"sales_update_frequency",
"over_order_allowance",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
"hide_tax_id"
"hide_tax_id",
"allow_sales_order_creation_for_expired_quotation"
],
"fields": [
{
"fieldname": "customer_defaults_section",
"fieldtype": "Section Break",
"label": "Customer Defaults"
},
{
"default": "Customer Name",
"fieldname": "cust_master_name",
@@ -43,13 +50,6 @@
"label": "Customer Naming By",
"options": "Customer Name\nNaming Series\nAuto Name"
},
{
"fieldname": "campaign_naming_by",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Campaign Naming By",
"options": "Campaign Name\nNaming Series\nAuto Name"
},
{
"fieldname": "customer_group",
"fieldtype": "Link",
@@ -57,6 +57,10 @@
"label": "Default Customer Group",
"options": "Customer Group"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "territory",
"fieldtype": "Link",
@@ -65,11 +69,31 @@
"options": "Territory"
},
{
"fieldname": "selling_price_list",
"fieldtype": "Link",
"fieldname": "crm_settings_section",
"fieldtype": "Section Break",
"label": "CRM Settings"
},
{
"fieldname": "campaign_naming_by",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Default Price List",
"options": "Price List"
"label": "Campaign Naming By",
"options": "Campaign Name\nNaming Series\nAuto Name"
},
{
"fieldname": "contract_naming_by",
"fieldtype": "Select",
"label": "Contract Naming By",
"options": "Party Name\nNaming Series"
},
{
"fieldname": "default_valid_till",
"fieldtype": "Data",
"label": "Default Quotation Validity Days"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"default": "15",
@@ -79,9 +103,65 @@
"label": "Close Opportunity After Days"
},
{
"fieldname": "default_valid_till",
"fieldtype": "Data",
"label": "Default Quotation Validity Days"
"fieldname": "item_price_settings_section",
"fieldtype": "Section Break",
"label": "Item Price Settings"
},
{
"fieldname": "selling_price_list",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Price List",
"options": "Price List"
},
{
"default": "Stop",
"depends_on": "maintain_same_sales_rate",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action if Same Rate is Not Maintained Throughout Sales Cycle",
"mandatory_depends_on": "maintain_same_sales_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "maintain_same_sales_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout Sales Cycle"
},
{
"default": "0",
"fieldname": "editable_price_list_rate",
"fieldtype": "Check",
"label": "Allow User to Edit Price List Rate in Transactions"
},
{
"default": "0",
"fieldname": "validate_selling_price",
"fieldtype": "Check",
"label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate"
},
{
"default": "0",
"fieldname": "editable_bundle_item_rates",
"fieldtype": "Check",
"label": "Calculate Product Bundle Price based on Child Items' Rates"
},
{
"fieldname": "sales_transactions_settings_section",
"fieldtype": "Section Break",
"label": "Transaction Settings"
},
{
"fieldname": "so_required",
@@ -106,15 +186,10 @@
},
{
"default": "0",
"fieldname": "maintain_same_sales_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout Sales Cycle"
},
{
"default": "0",
"fieldname": "editable_price_list_rate",
"fieldtype": "Check",
"label": "Allow User to Edit Price List Rate in Transactions"
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
"fieldname": "over_order_allowance",
"fieldtype": "Float",
"label": "Over Order Allowance (%)"
},
{
"default": "0",
@@ -128,77 +203,17 @@
"fieldtype": "Check",
"label": "Allow Multiple Sales Orders Against a Customer's Purchase Order"
},
{
"default": "0",
"fieldname": "validate_selling_price",
"fieldtype": "Check",
"label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate"
},
{
"default": "0",
"fieldname": "hide_tax_id",
"fieldtype": "Check",
"label": "Hide Customer's Tax ID from Sales Transactions"
},
{
"default": "Stop",
"depends_on": "maintain_same_sales_rate",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action if Same Rate is Not Maintained Throughout Sales Cycle",
"mandatory_depends_on": "maintain_same_sales_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "editable_bundle_item_rates",
"fieldname": "allow_sales_order_creation_for_expired_quotation",
"fieldtype": "Check",
"label": "Calculate Product Bundle Price based on Child Items' Rates"
},
{
"fieldname": "customer_defaults_section",
"fieldtype": "Section Break",
"label": "Customer Defaults"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "crm_settings_section",
"fieldtype": "Section Break",
"label": "CRM Settings"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "item_price_settings_section",
"fieldtype": "Section Break",
"label": "Item Price Settings"
},
{
"fieldname": "sales_transactions_settings_section",
"fieldtype": "Section Break",
"label": "Transaction Settings"
},
{
"fieldname": "contract_naming_by",
"fieldtype": "Select",
"label": "Contract Naming By",
"options": "Party Name\nNaming Series"
"label": "Allow Sales Order Creation For Expired Quotation"
}
],
"icon": "fa fa-cog",
@@ -206,7 +221,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-03-28 12:18:06.768403",
"modified": "2023-03-22 13:09:38.513317",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -522,7 +522,7 @@ erpnext.PointOfSale.Controller = class {
const from_selector = field === 'qty' && value === "+1";
if (from_selector)
value = flt(item_row.qty) + flt(value);
value = flt(item_row.stock_qty) + flt(value);
if (item_row_exists) {
if (field === 'qty')

View File

@@ -41,8 +41,20 @@ def get_columns(filters):
{"label": _("Description"), "fieldtype": "Data", "fieldname": "description", "width": 150},
{"label": _("Quantity"), "fieldtype": "Float", "fieldname": "quantity", "width": 150},
{"label": _("UOM"), "fieldtype": "Link", "fieldname": "uom", "options": "UOM", "width": 100},
{"label": _("Rate"), "fieldname": "rate", "options": "Currency", "width": 120},
{"label": _("Amount"), "fieldname": "amount", "options": "Currency", "width": 120},
{
"label": _("Rate"),
"fieldname": "rate",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Sales Order"),
"fieldtype": "Link",
@@ -93,8 +105,9 @@ def get_columns(filters):
},
{
"label": _("Billed Amount"),
"fieldtype": "currency",
"fieldtype": "Currency",
"fieldname": "billed_amount",
"options": "currency",
"width": 120,
},
{
@@ -104,6 +117,13 @@ def get_columns(filters):
"options": "Company",
"width": 100,
},
{
"label": _("Currency"),
"fieldtype": "Link",
"fieldname": "currency",
"options": "Currency",
"hidden": 1,
},
]
@@ -141,31 +161,12 @@ def get_data(filters):
"billed_amount": flt(record.get("billed_amt")),
"company": record.get("company"),
}
row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency")
data.append(row)
return data
def get_conditions(filters):
conditions = ""
if filters.get("item_group"):
conditions += "AND so_item.item_group = %s" % frappe.db.escape(filters.item_group)
if filters.get("from_date"):
conditions += "AND so.transaction_date >= '%s'" % filters.from_date
if filters.get("to_date"):
conditions += "AND so.transaction_date <= '%s'" % filters.to_date
if filters.get("item_code"):
conditions += "AND so_item.item_code = %s" % frappe.db.escape(filters.item_code)
if filters.get("customer"):
conditions += "AND so.customer = %s" % frappe.db.escape(filters.customer)
return conditions
def get_customer_details():
details = frappe.get_all("Customer", fields=["name", "customer_name", "customer_group"])
customer_details = {}
@@ -187,29 +188,50 @@ def get_item_details():
def get_sales_order_details(company_list, filters):
conditions = get_conditions(filters)
db_so = frappe.qb.DocType("Sales Order")
db_so_item = frappe.qb.DocType("Sales Order Item")
return frappe.db.sql(
"""
SELECT
so_item.item_code, so_item.description, so_item.qty,
so_item.uom, so_item.base_rate, so_item.base_amount,
so.name, so.transaction_date, so.customer,so.territory,
so.project, so_item.delivered_qty,
so_item.billed_amt, so.company
FROM
`tabSales Order` so, `tabSales Order Item` so_item
WHERE
so.name = so_item.parent
AND so.company in ({0})
AND so.docstatus = 1 {1}
""".format(
",".join(["%s"] * len(company_list)), conditions
),
tuple(company_list),
as_dict=1,
query = (
frappe.qb.from_(db_so)
.inner_join(db_so_item)
.on(db_so_item.parent == db_so.name)
.select(
db_so.name,
db_so.customer,
db_so.transaction_date,
db_so.territory,
db_so.project,
db_so.company,
db_so_item.item_code,
db_so_item.description,
db_so_item.qty,
db_so_item.uom,
db_so_item.base_rate,
db_so_item.base_amount,
db_so_item.delivered_qty,
(db_so_item.billed_amt * db_so.conversion_rate).as_("billed_amt"),
)
.where(db_so.docstatus == 1)
.where(db_so.company.isin(tuple(company_list)))
)
if filters.get("item_group"):
query = query.where(db_so_item.item_group == filters.item_group)
if filters.get("from_date"):
query = query.where(db_so.transaction_date >= filters.from_date)
if filters.get("to_date"):
query = query.where(db_so.transaction_date <= filters.to_date)
if filters.get("item_code"):
query = query.where(db_so_item.item_code == filters.item_code)
if filters.get("customer"):
query = query.where(db_so.customer == filters.customer)
return query.run(as_dict=1)
def get_chart_data(data):
item_wise_sales_map = {}

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