Compare commits

...

156 Commits

Author SHA1 Message Date
Frappe PR Bot
0da6237d22 chore(release): Bumped to Version 13.49.9
## [13.49.9](https://github.com/frappe/erpnext/compare/v13.49.8...v13.49.9) (2023-03-28)

### Bug Fixes

* `Blanket Order` (backport [#34279](https://github.com/frappe/erpnext/issues/34279)) ([#34548](https://github.com/frappe/erpnext/issues/34548)) ([8ddbac5](8ddbac5158))
* **client:** Amount calculation for 0 qty debit notes ([#34455](https://github.com/frappe/erpnext/issues/34455)) ([19dda80](19dda807d1))
* exchange gain/loss GL's should be removed if advance is cancelled ([#34529](https://github.com/frappe/erpnext/issues/34529)) ([00518eb](00518eb384))
* german translations ([#34312](https://github.com/frappe/erpnext/issues/34312)) ([661030a](661030aba1))
* incorrect `Opening Value` in `Stock Balance` report (backport [#34461](https://github.com/frappe/erpnext/issues/34461)) ([#34622](https://github.com/frappe/erpnext/issues/34622)) ([e53a96a](e53a96ae1d))
* incorrect depr schedules after asset repair [v13] ([#34520](https://github.com/frappe/erpnext/issues/34520)) ([ae88ba5](ae88ba5d18))
* Overallocation of 'qty' from Cr Notes to Parent Invoice ([d2a1acc](d2a1acc2e2))
* Party Name in SOA print when viewed from Customer/Supplier master ([#34597](https://github.com/frappe/erpnext/issues/34597)) ([4bdea43](4bdea436e3))
* Percentage billing in Sales Order ([#34606](https://github.com/frappe/erpnext/issues/34606)) ([3aab6e6](3aab6e6fa8))
* recalculate WDV rate after asset repair [v13] ([#34567](https://github.com/frappe/erpnext/issues/34567)) ([c8bde39](c8bde399e5))
* Search field not working for customer, supplier ([#32693](https://github.com/frappe/erpnext/issues/32693)) ([dbe289e](dbe289e734))
* unset address and contact on trash (backport [#34495](https://github.com/frappe/erpnext/issues/34495)) ([#34561](https://github.com/frappe/erpnext/issues/34561)) ([7f83d15](7f83d15bda))
* valuation rate issue while making stock entry from PO ([3574d49](3574d490db))
2023-03-28 18:17:57 +00:00
Deepesh Garg
526e350d98 Merge pull request #34610 from frappe/version-13-hotfix
chore: release v13
2023-03-28 23:46:31 +05:30
mergify[bot]
e53a96ae1d fix: incorrect Opening Value in Stock Balance report (backport #34461) (#34622)
* fix: incorrect `Opening Value` in `Stock Balance` report

(cherry picked from commit b04a101c11)

# Conflicts:
#	erpnext/stock/report/stock_balance/stock_balance.py

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-03-28 21:09:09 +05:30
mergify[bot]
3aab6e6fa8 fix: Percentage billing in Sales Order (#34606)
* fix: Percentage billing in Sales Order (#34606)

(cherry picked from commit 12ad2aa2e5)

# Conflicts:
#	erpnext/controllers/status_updater.py

* chore: resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-28 18:38:52 +05:30
Deepesh Garg
2e9e6eef05 chore: Update ubuntu version (#34620) 2023-03-28 17:25:33 +05:30
mergify[bot]
4bdea436e3 fix: Party Name in SOA print when viewed from Customer/Supplier master (#34597)
fix: Party Name in SOA print when viewed from Customer/Supplier master (#34597)

fix: Party Name in SOA print when viewd from Customer/Supplier master
(cherry picked from commit 50c1172f29)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-28 16:49:20 +05:30
mergify[bot]
dbe289e734 fix: Search field not working for customer, supplier (#32693)
* fix: searchfield not working for cuctsomer, supplier as per customize form

(cherry picked from commit 46d148defd)

* test: added test case to validate seachfields for customer, supplier

(cherry picked from commit 5f84993bae)

* fix: not able to select customer / supplier

---------

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2023-03-28 16:06:27 +05:30
Frappe PR Bot
8c4f45307e chore(release): Bumped to Version 13.49.8
## [13.49.8](https://github.com/frappe/erpnext/compare/v13.49.7...v13.49.8) (2023-03-25)

### Bug Fixes

* valuation rate issue while making stock entry from PO ([78bd698](78bd698f9e))
2023-03-25 14:50:08 +00:00
rohitwaghchaure
7b2dc2449d Merge pull request #34590 from frappe/mergify/bp/version-13/pr-34555
fix: valuation rate issue while making stock entry from PO (backport #34555)
2023-03-25 20:18:27 +05:30
Rohit Waghchaure
78bd698f9e fix: valuation rate issue while making stock entry from PO
(cherry picked from commit 3574d490db)
2023-03-25 13:32:41 +00:00
Frappe PR Bot
544e37ca5c chore(release): Bumped to Version 13.49.7
## [13.49.7](https://github.com/frappe/erpnext/compare/v13.49.6...v13.49.7) (2023-03-23)

### Bug Fixes

* recalculate WDV rate after asset repair [v13] ([#34567](https://github.com/frappe/erpnext/issues/34567)) ([5f5fa84](5f5fa843ac))
2023-03-23 15:43:53 +00:00
Anand Baburajan
c7bdb1bbf9 Merge pull request #34572 from frappe/mergify/bp/version-13/pr-34567
fix: recalculate WDV rate after asset repair [v13] (backport #34567)
2023-03-23 21:11:48 +05:30
Anand Baburajan
5f5fa843ac fix: recalculate WDV rate after asset repair [v13] (#34567)
fix: recalculate wdv rate after asset repair
(cherry picked from commit c8bde399e5)
2023-03-23 14:28:08 +00:00
Anand Baburajan
c8bde399e5 fix: recalculate WDV rate after asset repair [v13] (#34567)
fix: recalculate wdv rate after asset repair
2023-03-23 19:44:13 +05:30
mergify[bot]
7f83d15bda fix: unset address and contact on trash (backport #34495) (#34561)
fix: unset address and contact on trash (#34495)

* fix(Customer): unset address and contact on trash

* fix(Supplier): unset address and contact on trash

---------

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

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-03-23 12:58:54 +05:30
rohitwaghchaure
ad5eb6da4e Merge pull request #34555 from rohitwaghchaure/dont-raise-error-while-mkaing-ste
fix: valuation rate issue while making stock entry from PO
2023-03-22 23:36:57 +05:30
Rohit Waghchaure
3574d490db fix: valuation rate issue while making stock entry from PO 2023-03-22 16:52:07 +05:30
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
mergify[bot]
8ddbac5158 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>
2023-03-22 13:50:29 +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
ruthra kumar
00518eb384 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
2023-03-21 16:16:36 +05:30
mergify[bot]
661030aba1 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>
2023-03-21 16:08:50 +05:30
mergify[bot]
19dda807d1 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>
2023-03-21 16:06:10 +05:30
Anand Baburajan
0d5abf1c95 refactor: calc depr in asset repair and if statement (#34526)
refactor: minor asset repair of calc depr and if statement
2023-03-21 14:30:28 +05:30
Anand Baburajan
ae88ba5d18 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
2023-03-21 12:35:44 +05:30
ruthra kumar
d855b532e4 Merge pull request #34511 from ruthra-kumar/manual_backport_of_34456
fix: Gross Profit reports Invoices with -ve qty for Invoices with Cr Notes (manual backport to version 13)
2023-03-20 15:42:09 +05:30
ruthra kumar
4e38e8da1b 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)
2023-03-20 14:48:53 +05:30
ruthra kumar
6eeac48f17 refactor: Ignore linked Cr Notes in Report output
(cherry picked from commit d0715a82eb)
2023-03-20 14:48:53 +05:30
ruthra kumar
d2a1acc2e2 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)
2023-03-20 14:48:53 +05:30
mergify[bot]
a3aa4d536a 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>
2023-03-19 18:08:15 +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
81 changed files with 1844 additions and 606 deletions

View File

@@ -14,7 +14,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
timeout-minutes: 60
name: Patch Test

View File

@@ -18,7 +18,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
timeout-minutes: 60
strategy:

View File

@@ -8,7 +8,7 @@ on:
jobs:
check_translation:
name: Translation Syntax Check
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Setup python3

View File

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

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 (

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

@@ -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')}}

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

@@ -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

@@ -3470,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

@@ -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

@@ -38,8 +38,11 @@
{% 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>
@@ -49,11 +52,14 @@
{% 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

@@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = {
{
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Link",
"options": "Party Type",
"default": "",
"fieldtype": "Autocomplete",
options: Object.keys(frappe.boot.party_account_types),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
}

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,7 +589,7 @@ 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
)
@@ -597,15 +605,12 @@ class GrossProfitGenerator(object):
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
frappe.qb.from_(delivery_note)
.inner_join(delivery_note_item)
.on(delivery_note.name == delivery_note_item.parent)
frappe.qb.from_(delivery_note_item)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
.where(delivery_note.docstatus == 1)
.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)
@@ -667,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"
@@ -840,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

@@ -380,3 +380,82 @@ class TestGrossProfit(FrappeTestCase):
}
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

@@ -296,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
@@ -519,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

@@ -84,14 +84,55 @@ class Asset(AccountsController):
if self.calculate_depreciation:
self.value_after_depreciation = 0
self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_disposal)
self.set_accumulated_depreciation(date_of_disposal, 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
@@ -225,9 +266,7 @@ class Asset(AccountsController):
)
def make_depreciation_schedule(self, date_of_disposal):
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
"schedules"
):
if not self.get("schedules"):
self.schedules = []
if not self.available_for_use_date:
@@ -545,9 +584,7 @@ class Asset(AccountsController):
def set_accumulated_depreciation(
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")):
@@ -555,6 +592,12 @@ 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("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation
@@ -853,10 +896,19 @@ class Asset(AccountsController):
return 200.0 / args.get("total_number_of_depreciations")
if args.get("depreciation_method") == "Written Down Value":
if args.get("rate_of_depreciation") and on_validate:
if (
args.get("rate_of_depreciation")
and on_validate
and not self.flags.increase_in_asset_value_due_to_repair
):
return args.get("rate_of_depreciation")
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
if self.flags.increase_in_asset_value_due_to_repair:
value = flt(args.get("expected_value_after_useful_life")) / flt(
args.get("value_after_depreciation")
)
else:
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
@@ -1128,17 +1180,21 @@ def get_total_days(date, frequency):
@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

@@ -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,10 +53,7 @@ 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
@@ -62,7 +63,11 @@ class AssetRepair(AccountsController):
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,10 +77,7 @@ 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

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
)
@@ -640,7 +644,7 @@ def make_rm_stock_entry(purchase_order, rm_items):
}
stock_entry.add_to_stock_entry_detail(items_dict)
stock_entry.set_missing_values()
stock_entry.set_missing_values(raise_error_if_no_rate=False)
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer"))

View File

@@ -64,7 +64,7 @@ frappe.ui.form.on("Supplier", {
// custom buttons
frm.add_custom_button(__('Accounting Ledger'), function () {
frappe.set_route('query-report', 'General Ledger',
{ party_type: 'Supplier', party: frm.doc.name });
{ party_type: 'Supplier', party: frm.doc.name, party_name: frm.doc.supplier_name });
}, __("View"));
frm.add_custom_button(__('Accounts Payable'), function () {

View File

@@ -128,18 +128,9 @@ class Supplier(TransactionBase):
def on_trash(self):
if self.supplier_primary_contact:
frappe.db.sql(
"""
UPDATE `tabSupplier`
SET
supplier_primary_contact=null,
supplier_primary_address=null,
mobile_no=null,
email_id=null,
primary_address=null
WHERE name=%(name)s""",
{"name": self.name},
)
self.db_set("supplier_primary_contact", None)
if self.supplier_primary_address:
self.db_set("supplier_primary_address", None)
delete_contact_and_address("Supplier", self.name)

View File

@@ -3,6 +3,7 @@
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
@@ -151,6 +152,44 @@ class TestSupplier(FrappeTestCase):
# Rollback
address.delete()
def test_serach_fields_for_supplier(self):
from erpnext.controllers.queries import supplier_query
frappe.db.set_value("Buying Settings", None, "supp_master_name", "Naming Series")
supplier_name = create_supplier(supplier_name="Test Supplier 1").name
make_property_setter(
"Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype"
)
data = supplier_query(
"Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
)
self.assertEqual(data[0].name, supplier_name)
self.assertEqual(data[0].supplier_group, "Services")
self.assertTrue("supplier_type" not in data[0])
make_property_setter(
"Supplier",
None,
"search_fields",
"supplier_group, supplier_type",
"Data",
for_doctype="Doctype",
)
data = supplier_query(
"Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
)
self.assertEqual(data[0].name, supplier_name)
self.assertEqual(data[0].supplier_group, "Services")
self.assertEqual(data[0].supplier_type, "Company")
self.assertTrue("supplier_type" in data[0])
frappe.db.set_value("Buying Settings", None, "supp_master_name", "Supplier Name")
def create_supplier(**args):
args = frappe._dict(args)

View File

@@ -78,18 +78,16 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def customer_query(doctype, txt, searchfield, start, page_len, filters):
def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
doctype = "Customer"
conditions = []
cust_master_name = frappe.defaults.get_user_default("cust_master_name")
if cust_master_name == "Customer Name":
fields = ["name", "customer_group", "territory"]
else:
fields = ["name", "customer_name", "customer_group", "territory"]
fields = ["name"]
if cust_master_name != "Customer Name":
fields.append("customer_name")
fields = get_fields(doctype, fields)
searchfields = frappe.get_meta(doctype).get_search_fields()
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
@@ -112,20 +110,20 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
as_dict=as_dict,
)
# searches for supplier
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def supplier_query(doctype, txt, searchfield, start, page_len, filters):
def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
doctype = "Supplier"
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
if supp_master_name == "Supplier Name":
fields = ["name", "supplier_group"]
else:
fields = ["name", "supplier_name", "supplier_group"]
fields = ["name"]
if supp_master_name != "Supplier Name":
fields.append("supplier_name")
fields = get_fields(doctype, fields)
@@ -145,6 +143,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
),
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
as_dict=as_dict,
)

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

@@ -450,7 +450,7 @@ class StatusUpdater(Document):
ifnull((select
ifnull(sum(if(abs(%(target_ref_field)s) > abs(%(target_field)s), abs(%(target_field)s), abs(%(target_ref_field)s))), 0)
/ sum(abs(%(target_ref_field)s)) * 100
from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
from `tab%(target_dt)s` where parent='%(name)s' and parenttype='%(target_parent_dt)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
%(update_modified)s
where name='%(name)s'"""
% args

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

@@ -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

@@ -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

@@ -376,3 +376,4 @@ 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_asset_value_for_manual_depr_entries
erpnext.patches.v13_0.update_docs_link

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,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

@@ -324,6 +324,8 @@ 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]
@@ -335,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_list)
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(
holidays, working_days_list, relieving_date
)
if not lwp:
lwp = actual_lwp
@@ -461,7 +467,10 @@ 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_list):
def calculate_lwp_or_ppl_based_on_leave_application(
self, holidays, working_days_list, relieving_date=None
):
lwp = 0
daily_wages_fraction_for_half_day = (
@@ -469,6 +478,9 @@ class SalarySlip(TransactionBase):
)
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:
@@ -488,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
)
@@ -506,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`
@@ -516,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

@@ -124,8 +124,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
}
else {
let qty = item.qty || 1;
qty = me.frm.doc.is_return ? -1 * qty : qty;
// 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));
}

View File

@@ -1975,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

@@ -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

@@ -123,7 +123,7 @@ frappe.ui.form.on("Customer", {
frm.add_custom_button(__('Accounting Ledger'), function () {
frappe.set_route('query-report', 'General Ledger',
{party_type: 'Customer', party: frm.doc.name});
{party_type: 'Customer', party: frm.doc.name, party_name: frm.doc.customer_name});
}, __('View'));
frm.add_custom_button(__('Pricing Rule'), function () {

View File

@@ -275,18 +275,9 @@ class Customer(TransactionBase):
def on_trash(self):
if self.customer_primary_contact:
frappe.db.sql(
"""
UPDATE `tabCustomer`
SET
customer_primary_contact=null,
customer_primary_address=null,
mobile_no=null,
email_id=null,
primary_address=null
WHERE name=%(name)s""",
{"name": self.name},
)
self.db_set("customer_primary_contact", None)
if self.customer_primary_address:
self.db_set("customer_primary_address", None)
delete_contact_and_address("Customer", self.name)
if self.lead_name:

View File

@@ -3,6 +3,7 @@
import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt
@@ -343,6 +344,37 @@ class TestCustomer(FrappeTestCase):
due_date = get_due_date("2017-01-22", "Customer", "_Test Customer")
self.assertEqual(due_date, "2017-01-22")
def test_serach_fields_for_customer(self):
from erpnext.controllers.queries import customer_query
frappe.db.set_value("Selling Settings", None, "cust_master_name", "Naming Series")
make_property_setter(
"Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype"
)
data = customer_query(
"Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
)
self.assertEqual(data[0].name, "_Test Customer")
self.assertEqual(data[0].customer_group, "_Test Customer Group")
self.assertTrue("territory" not in data[0])
make_property_setter(
"Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype"
)
data = customer_query(
"Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
)
self.assertEqual(data[0].name, "_Test Customer")
self.assertEqual(data[0].customer_group, "_Test Customer Group")
self.assertEqual(data[0].territory, "_Test Territory")
self.assertTrue("territory" in data[0])
frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name")
def get_customer_dict(customer_name):
return {

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

@@ -1211,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(
**{
@@ -1224,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"),
@@ -1415,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",
@@ -1454,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)
@@ -1464,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",
{
@@ -1496,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

@@ -30,12 +30,18 @@
"so_required",
"dn_required",
"sales_update_frequency",
"over_order_allowance",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
"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",
@@ -44,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",
@@ -58,6 +57,10 @@
"label": "Default Customer Group",
"options": "Customer Group"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "territory",
"fieldtype": "Link",
@@ -66,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",
@@ -80,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",
@@ -107,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",
@@ -129,83 +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"
},
{
"default": "0",
"fieldname": "allow_sales_order_creation_for_expired_quotation",
"fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation"
"label": "Allow Sales Order Creation For Expired Quotation"
}
],
"icon": "fa fa-cog",
@@ -213,7 +221,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-02-04 12:37:53.380857",
"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

@@ -417,9 +417,14 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
args: args,
callback: function(r) {
if(r.message) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
} else {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
if (r.message.batch_no != null) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message.batch_no);
} else if (r.message.msg_print) {
frappe.show_alert({
message: r.message.msg_print,
indicator:'orange'
}, 5);
}
}
}
});

View File

@@ -150,7 +150,7 @@ def add_standard_navbar_items():
{
"item_label": "Documentation",
"item_type": "Route",
"route": "https://erpnext.com/docs/user/manual",
"route": "https://docs.erpnext.com/docs/v13/user/manual/en/introduction",
"is_standard": 1,
},
{

View File

@@ -260,7 +260,9 @@ def set_batch_nos(doc, warehouse_field, throw=False, child_table="items"):
warehouse = d.get(warehouse_field, None)
if warehouse and qty > 0 and frappe.db.get_value("Item", d.item_code, "has_batch_no"):
if not d.batch_no:
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no)
d.batch_no = get_batch_no(d.item_code, warehouse, qty, throw, d.serial_no).get(
"batch_no", None
)
else:
batch_qty = get_batch_qty(batch_no=d.batch_no, warehouse=warehouse)
if flt(batch_qty, d.precision("qty")) < flt(qty, d.precision("qty")):
@@ -282,6 +284,7 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
"""
batch_no = None
message = None
batches = get_batches(item_code, warehouse, qty, throw, serial_no)
for batch in batches:
@@ -290,15 +293,18 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None):
break
if not batch_no:
frappe.msgprint(
_(
"Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement"
).format(frappe.bold(item_code))
)
message = _(
"Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement"
).format(frappe.bold(item_code))
if throw:
frappe.msgprint(
_(
"Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement"
).format(frappe.bold(item_code))
)
raise UnableToSelectBatchError
return batch_no
return {"batch_no": batch_no, "msg_print": message}
def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):

View File

@@ -99,7 +99,8 @@ class TestBatch(FrappeTestCase):
# shipped from FEFO batch
self.assertEqual(
delivery_note.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty)
delivery_note.items[0].batch_no,
get_batch_no(item_code, receipt.items[0].warehouse, batch_qty).get("batch_no", None),
)
def test_delivery_note_fail(self):
@@ -145,7 +146,8 @@ class TestBatch(FrappeTestCase):
# assert same batch is selected
self.assertEqual(
stock_entry.items[0].batch_no, get_batch_no(item_code, receipt.items[0].warehouse, batch_qty)
stock_entry.items[0].batch_no,
get_batch_no(item_code, receipt.items[0].warehouse, batch_qty).get("batch_no", None),
)
def test_batch_split(self):

View File

@@ -33,6 +33,9 @@ frappe.ui.form.on("Item", {
'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items");
},
'Stock Entry': () => {
open_form(frm, "Stock Entry", "Stock Entry Detail", "items");
},
};
},
@@ -848,6 +851,9 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description;
if (!new_child_doc.qty) {
new_child_doc.qty = 1.0;
}
frappe.run_serially([
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),

View File

@@ -54,7 +54,7 @@ class ItemAlternative(Document):
if not item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code))
if self.two_way and not alternative_item_data.allow_alternative_item:
frappe.throw(alternate_item_check_msg.format(self.item_code))
frappe.throw(alternate_item_check_msg.format(self.alternative_item_code))
def validate_duplicate(self):
if frappe.db.get_value(

View File

@@ -2,7 +2,18 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Item Price", {
onload: function (frm) {
setup(frm) {
frm.set_query("item_code", function() {
return {
filters: {
"disabled": 0,
"has_variants": 0
}
};
});
},
onload(frm) {
// Fetch price list details
frm.add_fetch("price_list", "buying", "buying");
frm.add_fetch("price_list", "selling", "selling");

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe import _, bold
from frappe.model.document import Document
from frappe.utils import getdate
@@ -19,6 +19,7 @@ class ItemPrice(Document):
self.update_price_list_details()
self.update_item_details()
self.check_duplicates()
self.validate_item_template()
def validate_item(self):
if not frappe.db.exists("Item", self.item_code):
@@ -47,6 +48,12 @@ class ItemPrice(Document):
"Item", self.item_code, ["item_name", "description"]
)
def validate_item_template(self):
if frappe.get_cached_value("Item", self.item_code, "has_variants"):
msg = f"Item Price cannot be created for the template item {bold(self.item_code)}"
frappe.throw(_(msg))
def check_duplicates(self):
conditions = (
"""where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s"""

View File

@@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase):
frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True)
def test_template_item_price(self):
from erpnext.stock.doctype.item.test_item import make_item
item = make_item(
"Test Template Item 1",
{
"has_variants": 1,
"variant_based_on": "Manufacturer",
},
)
doc = frappe.get_doc(
{
"doctype": "Item Price",
"price_list": "_Test Price List",
"item_code": item.name,
"price_list_rate": 100,
}
)
self.assertRaises(frappe.ValidationError, doc.save)
def test_duplicate_item(self):
doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.save)

View File

@@ -55,7 +55,6 @@ class LandedCostVoucher(Document):
self.get_items_from_purchase_receipts()
self.set_applicable_charges_on_item()
self.validate_applicable_charges_for_item()
def check_mandatory(self):
if not self.get("purchase_receipts"):
@@ -115,6 +114,13 @@ class LandedCostVoucher(Document):
total_item_cost += item.get(based_on_field)
for item in self.get("items"):
if not total_item_cost and not item.get(based_on_field):
frappe.throw(
_(
"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'"
)
)
item.applicable_charges = flt(
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
item.precision("applicable_charges"),
@@ -162,6 +168,7 @@ class LandedCostVoucher(Document):
)
def on_submit(self):
self.validate_applicable_charges_for_item()
self.update_landed_cost()
def on_cancel(self):

View File

@@ -175,6 +175,59 @@ class TestLandedCostVoucher(FrappeTestCase):
)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
def test_landed_cost_voucher_for_zero_purchase_rate(self):
"Test impact of LCV on future stock balances."
from erpnext.stock.doctype.item.test_item import make_item
item = make_item("LCV Stock Item", {"is_stock_item": 1})
warehouse = "Stores - _TC"
pr = make_purchase_receipt(
item_code=item.name,
warehouse=warehouse,
qty=10,
rate=0,
posting_date=add_days(frappe.utils.nowdate(), -2),
)
self.assertEqual(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
"stock_value_difference",
),
0,
)
lcv = make_landed_cost_voucher(
company=pr.company,
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=100,
distribute_charges_based_on="Distribute Manually",
do_not_save=True,
)
lcv.get_items_from_purchase_receipts()
lcv.items[0].applicable_charges = 100
lcv.save()
lcv.submit()
self.assertTrue(
frappe.db.exists(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
)
)
self.assertEqual(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
"stock_value_difference",
),
100,
)
def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice(
@@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args):
lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = args.company or "_Test Company"
lcv.distribute_charges_based_on = "Amount"
lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
lcv.set(
"purchase_receipts",

View File

@@ -10,6 +10,7 @@ import json
import frappe
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
from six import string_types
@@ -185,6 +186,34 @@ class MaterialRequest(BuyingController):
self.update_requested_qty()
self.update_requested_qty_in_production_plan()
def get_mr_items_ordered_qty(self, mr_items):
mr_items_ordered_qty = {}
mr_items = [d.name for d in self.get("items") if d.name in mr_items]
doctype = qty_field = None
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
doctype = frappe.qb.DocType("Stock Entry Detail")
qty_field = doctype.transfer_qty
elif self.material_request_type == "Manufacture":
doctype = frappe.qb.DocType("Work Order")
qty_field = doctype.qty
if doctype and qty_field:
query = (
frappe.qb.from_(doctype)
.select(doctype.material_request_item, Sum(qty_field))
.where(
(doctype.material_request == self.name)
& (doctype.material_request_item.isin(mr_items))
& (doctype.docstatus == 1)
)
.groupby(doctype.material_request_item)
)
mr_items_ordered_qty = frappe._dict(query.run())
return mr_items_ordered_qty
def update_completed_qty(self, mr_items=None, update_modified=True):
if self.material_request_type == "Purchase":
return
@@ -192,18 +221,13 @@ class MaterialRequest(BuyingController):
if not mr_items:
mr_items = [d.name for d in self.get("items")]
mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items)
mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
for d in self.get("items"):
if d.name in mr_items:
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
d.ordered_qty = flt(
frappe.db.sql(
"""select sum(transfer_qty)
from `tabStock Entry Detail` where material_request = %s
and material_request_item = %s and docstatus = 1""",
(self.name, d.name),
)[0][0]
)
mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
if mr_qty_allowance:
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
@@ -224,14 +248,7 @@ class MaterialRequest(BuyingController):
)
elif self.material_request_type == "Manufacture":
d.ordered_qty = flt(
frappe.db.sql(
"""select sum(qty)
from `tabWork Order` where material_request = %s
and material_request_item = %s and docstatus = 1""",
(self.name, d.name),
)[0][0]
)
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
@@ -594,6 +611,9 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.purpose = source.material_request_type
target.from_warehouse = source.set_from_warehouse
target.to_warehouse = source.set_warehouse
if source.job_card:
target.purpose = "Material Transfer for Manufacture"

View File

@@ -436,7 +436,7 @@ class PurchaseReceipt(BuyingController):
)
divisional_loss = flt(
valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount")
valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount")
)
if divisional_loss:
@@ -831,7 +831,7 @@ def update_billing_percentage(pr_doc, update_modified=True):
# Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0
for item in pr_doc.items:
return_data = frappe.db.get_list(
return_data = frappe.get_all(
"Purchase Receipt",
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
filters=[
@@ -1064,13 +1064,25 @@ def get_item_account_wise_additional_cost(purchase_document):
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
"amount"
] += (account.amount * item.get(based_on_field) / total_item_cost)
if total_item_cost > 0:
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["amount"] += (
account.amount * item.get(based_on_field) / total_item_cost
)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
"base_amount"
] += (account.base_amount * item.get(based_on_field) / total_item_cost)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["base_amount"] += (
account.base_amount * item.get(based_on_field) / total_item_cost
)
else:
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["amount"] += item.applicable_charges
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["base_amount"] += item.applicable_charges
return item_account_wise_cost

View File

@@ -1262,7 +1262,9 @@ class StockEntry(StockController):
and ret.get("has_batch_no")
and not args.get("batch_no")
):
args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"])
args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"]).get(
"batch_no", None
)
if (
self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code")
@@ -2227,11 +2229,11 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
def set_missing_values(self):
def set_missing_values(self, raise_error_if_no_rate=True):
"Updates rate and availability of all the items of mapped doc."
self.set_transfer_qty()
self.set_actual_qty()
self.calculate_rate_and_amount()
self.calculate_rate_and_amount(raise_error_if_no_rate=raise_error_if_no_rate)
@frappe.whitelist()

View File

@@ -2,7 +2,22 @@
// For license information, please see license.txt
frappe.ui.form.on('Stock Reposting Settings', {
// refresh: function(frm) {
refresh: function(frm) {
frm.trigger('convert_to_item_based_reposting');
},
// }
convert_to_item_based_reposting: function(frm) {
frm.add_custom_button(__('Convert to Item Based Reposting'), function() {
frm.call({
method: 'convert_to_item_wh_reposting',
frezz: true,
doc: frm.doc,
callback: function(r) {
if (!r.exc) {
frm.reload_doc();
}
}
})
})
}
});

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours
@@ -24,3 +26,62 @@ class StockRepostingSettings(Document):
if diff < 10:
self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True))
@frappe.whitelist()
def convert_to_item_wh_reposting(self):
"""Convert Transaction reposting to Item Warehouse based reposting if Item Based Reposting has enabled."""
reposting_data = get_reposting_entries()
vouchers = [d.voucher_no for d in reposting_data]
item_warehouses = {}
for ledger in get_stock_ledgers(vouchers):
key = (ledger.item_code, ledger.warehouse)
if key not in item_warehouses:
item_warehouses[key] = ledger.posting_date
elif frappe.utils.getdate(item_warehouses.get(key)) > frappe.utils.getdate(ledger.posting_date):
item_warehouses[key] = ledger.posting_date
for key, posting_date in item_warehouses.items():
item_code, warehouse = key
create_repost_item_valuation(item_code, warehouse, posting_date)
for row in reposting_data:
frappe.db.set_value("Repost Item Valuation", row.name, "status", "Skipped")
self.db_set("item_based_reposting", 1)
frappe.msgprint(_("Item Warehouse based reposting has been enabled."))
def get_reposting_entries():
return frappe.get_all(
"Repost Item Valuation",
fields=["voucher_no", "name"],
filters={"status": ("in", ["Queued", "In Progress"]), "docstatus": 1, "based_on": "Transaction"},
)
def get_stock_ledgers(vouchers):
return frappe.get_all(
"Stock Ledger Entry",
fields=["item_code", "warehouse", "posting_date"],
filters={"voucher_no": ("in", vouchers)},
)
def create_repost_item_valuation(item_code, warehouse, posting_date):
frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"company": frappe.get_cached_value("Warehouse", warehouse, "company"),
"posting_date": posting_date,
"based_on": "Item and Warehouse",
"posting_time": "00:00:01",
"item_code": item_code,
"warehouse": warehouse,
"allow_negative_stock": True,
"status": "Queued",
}
).submit()

View File

@@ -153,7 +153,7 @@ def update_stock(args, out):
):
if out.has_batch_no and not args.get("batch_no"):
out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty)
out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty).get("batch_no", None)
actual_batch_qty = get_batch_qty(out.batch_no, out.warehouse, out.item_code)
if actual_batch_qty:
out.update(actual_batch_qty)

View File

@@ -6,6 +6,7 @@ from operator import itemgetter
import frappe
from frappe import _
from frappe.query_builder.functions import Coalesce
from frappe.utils import cint, date_diff, flt, getdate
from six import iteritems
@@ -276,11 +277,39 @@ def get_stock_ledger_entries(filters, items):
)
def get_opening_vouchers(to_date):
opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []}
se = frappe.qb.DocType("Stock Entry")
sr = frappe.qb.DocType("Stock Reconciliation")
vouchers_data = (
frappe.qb.from_(
(
frappe.qb.from_(se)
.select(se.name, Coalesce("Stock Entry").as_("voucher_type"))
.where((se.docstatus == 1) & (se.posting_date <= to_date) & (se.is_opening == "Yes"))
)
+ (
frappe.qb.from_(sr)
.select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type"))
.where((sr.docstatus == 1) & (sr.posting_date <= to_date) & (sr.purpose == "Opening Stock"))
)
).select("voucher_type", "name")
).run(as_dict=True)
if vouchers_data:
for d in vouchers_data:
opening_vouchers[d.voucher_type].append(d.name)
return opening_vouchers
def get_item_warehouse_map(filters, sle):
iwb_map = {}
from_date = getdate(filters.get("from_date"))
to_date = getdate(filters.get("to_date"))
opening_vouchers = get_opening_vouchers(to_date)
float_precision = cint(frappe.db.get_default("float_precision")) or 3
for d in sle:
@@ -309,11 +338,7 @@ def get_item_warehouse_map(filters, sle):
value_diff = flt(d.stock_value_difference)
if d.posting_date < from_date or (
d.posting_date == from_date
and d.voucher_type == "Stock Reconciliation"
and frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock"
):
if d.posting_date < from_date or d.voucher_no in opening_vouchers.get(d.voucher_type, []):
qty_dict.opening_qty += qty_diff
qty_dict.opening_val += value_diff

View File

@@ -2007,30 +2007,27 @@ Please identify/create Account (Ledger) for type - {0},Bitte identifizieren / er
Please login as another user to register on Marketplace,"Bitte melden Sie sich als anderer Benutzer an, um sich auf dem Marktplatz zu registrieren",
Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.,"Bitte sicher stellen, dass wirklich alle Transaktionen dieses Unternehmens gelöscht werden sollen. Die Stammdaten bleiben bestehen. Diese Aktion kann nicht rückgängig gemacht werden.",
Please mention Basic and HRA component in Company,Bitte erwähnen Sie die Basis- und HRA-Komponente in der Firma,
Please mention Round Off Account in Company,Bitte Abschlusskonto in Unternehmen vermerken,
Please mention Round Off Cost Center in Company,Bitte Abschlusskostenstelle in Unternehmen vermerken,
Please mention no of visits required,"Bitte bei ""Besuche erforderlich"" NEIN angeben",
Please mention the Lead Name in Lead {0},Bitte erwähnen Sie den Lead Name in Lead {0},
Please pull items from Delivery Note,Bitte Artikel vom Lieferschein nehmen,
Please mention Round Off Account in Company,Bitte ein Standardkonto Konto für Rundungsdifferenzen in Unternehmen einstellen,
Please mention Round Off Cost Center in Company,Bitte eine Kostenstelle für Rundungsdifferenzen in Unternehmen einstellen,
Please mention no of visits required,Bitte die Anzahl der benötigten Wartungsbesuche angeben,
Please pull items from Delivery Note,Bitte Artikel aus dem Lieferschein ziehen,
Please register the SIREN number in the company information file,Bitte registrieren Sie die SIREN-Nummer in der Unternehmensinformationsdatei,
Please remove this Invoice {0} from C-Form {1},Bitte diese Rechnung {0} vom Kontaktformular {1} entfernen,
Please save the patient first,Bitte speichern Sie den Patienten zuerst,
Please save the report again to rebuild or update,"Speichern Sie den Bericht erneut, um ihn neu zu erstellen oder zu aktualisieren",
"Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row","Bitte zugewiesenen Betrag, Rechnungsart und Rechnungsnummer in mindestens einer Zeile auswählen",
Please select Apply Discount On,"Bitte ""Rabatt anwenden auf"" auswählen",
Please select BOM against item {0},Bitte wählen Sie Stückliste gegen Artikel {0},
Please select BOM for Item in Row {0},Bitte Stückliste für Artikel in Zeile {0} auswählen,
Please select BOM in BOM field for Item {0},Bitte aus dem Stücklistenfeld eine Stückliste für Artikel {0} auswählen,
Please select Category first,Bitte zuerst Kategorie auswählen,
Please select Charge Type first,Bitte zuerst Chargentyp auswählen,
Please select Company,Bitte Unternehmen auswählen,
Please select BOM against item {0},Bitte eine Stückliste für Artikel {0} auswählen,
Please select BOM for Item in Row {0},Bitte eine Stückliste für den Artikel in Zeile {0} auswählen,
Please select BOM in BOM field for Item {0},Bitte im Stücklistenfeld eine Stückliste für Artikel {0} auswählen,
Please select Category first,Bitte zuerst eine Kategorie auswählen,
Please select Charge Type first,Bitte zuerst einen Chargentyp auswählen,
Please select Company,Bitte ein Unternehmen auswählen,
Please select Company and Designation,Bitte wählen Sie Unternehmen und Position,
Please select Company and Posting Date to getting entries,"Bitte wählen Sie Unternehmen und Buchungsdatum, um Einträge zu erhalten",
Please select Company first,Bitte zuerst Unternehmen auswählen,
Please select Completion Date for Completed Asset Maintenance Log,Bitte wählen Sie Fertigstellungsdatum für das abgeschlossene Wartungsprotokoll für den Vermögenswert,
Please select Completion Date for Completed Repair,Bitte wählen Sie das Abschlussdatum für die abgeschlossene Reparatur,
Please select Course,Bitte wählen Sie Kurs,
Please select Drug,Bitte wählen Sie Arzneimittel,
Please select Employee,Bitte wählen Sie Mitarbeiter,
Please select Existing Company for creating Chart of Accounts,Bitte wählen Sie Bestehende Unternehmen für die Erstellung von Konten,
Please select Healthcare Service,Bitte wählen Sie Gesundheitsdienst,
@@ -4047,7 +4044,7 @@ Server Error,Serverfehler,
Service Level Agreement has been changed to {0}.,Service Level Agreement wurde in {0} geändert.,
Service Level Agreement was reset.,Service Level Agreement wurde zurückgesetzt.,
Service Level Agreement with Entity Type {0} and Entity {1} already exists.,Service Level Agreement mit Entitätstyp {0} und Entität {1} ist bereits vorhanden.,
Set,Menge,
Set Loyalty Program,Treueprogramm eintragen,
Set Meta Tags,Festlegen von Meta-Tags,
Set {0} in company {1},{0} in Firma {1} festlegen,
Setup,Einstellungen,
@@ -4227,10 +4224,8 @@ To date cannot be before From date,Bis-Datum kann nicht vor Von-Datum liegen,
Write Off,Abschreiben,
{0} Created,{0} Erstellt,
Email Id,E-Mail-ID,
No,Kein,
Reference Doctype,Referenz-DocType,
User Id,Benutzeridentifikation,
Yes,Ja,
Actual ,Tatsächlich,
Add to cart,In den Warenkorb legen,
Budget,Budget,
@@ -7799,7 +7794,7 @@ Default Employee Advance Account,Standardkonto für Vorschüsse an Arbeitnehmer,
Default Cost of Goods Sold Account,Standard-Herstellkosten,
Default Income Account,Standard-Ertragskonto,
Default Deferred Revenue Account,Standardkonto für passive Rechnungsabgrenzung,
Default Deferred Expense Account,Standard-Rechnungsabgrenzungsposten,
Default Deferred Expense Account,Standardkonto für aktive Rechnungsabgrenzung,
Default Payroll Payable Account,Standardkonto für Verbindlichkeiten aus Lohn und Gehalt,
Default Expense Claim Payable Account,Standard-Expense Claim Zahlbares Konto,
Stock Settings,Lager-Einstellungen,
@@ -8867,7 +8862,7 @@ Add Topic to Courses,Hinzufügen eines Themas zu Kursen,
This topic is already added to the existing courses,Dieses Thema wurde bereits zu den bestehenden Kursen hinzugefügt,
"If Shopify does not have a customer in the order, then while syncing the orders, the system will consider the default customer for the order","Wenn Shopify keinen Kunden in der Bestellung hat, berücksichtigt das System beim Synchronisieren der Bestellungen den Standardkunden für die Bestellung",
The accounts are set by the system automatically but do confirm these defaults,"Die Konten werden vom System automatisch festgelegt, bestätigen jedoch diese Standardeinstellungen",
Default Round Off Account,Standard-Rundungskonto,
Default Round Off Account,Standardkonto für Rundungsdifferenzen,
Failed Import Log,Importprotokoll fehlgeschlagen,
Fixed Error Log,Fehlerprotokoll behoben,
Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts,Firma {0} existiert bereits. Durch Fortfahren werden das Unternehmen und der Kontenplan überschrieben,
@@ -9899,3 +9894,5 @@ Total Asset,Aktiva,
Total Liability,Verbindlichkeiten,
Total Equity,Eigenkapital,
Warehouse wise Stock Value,Warenwert nach Lager,
Discount Validity,Frist für den Rabatt,
Discount Validity Based On,Frist für den Rabatt berechnet sich nach,
Can't render this file because it is too large.

View File

@@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei
Stock Levels,Niveaux du Stocks,
Stock Liabilities,Passif du Stock,
Stock Options,Options du Stock,
Stock Qty,Qté en Stock,
Stock Qty,Qté en unité de stock,
Stock Received But Not Billed,Stock Reçus Mais Non Facturés,
Stock Reports,Rapports de stock,
Stock Summary,Résumé du Stock,
Can't render this file because it is too large.