Compare commits

...

63 Commits

Author SHA1 Message Date
Frappe PR Bot
dd35cd1f84 chore(release): Bumped to Version 16.18.2
## [16.18.2](https://github.com/frappe/erpnext/compare/v16.18.1...v16.18.2) (2026-05-14)

### Bug Fixes

* posting date and time ([ab09029](ab090295d9))
2026-05-14 05:35:39 +00:00
rohitwaghchaure
77a6299e8b Merge pull request #54931 from frappe/mergify/bp/version-16/pr-54928
fix: posting date and time (backport #54905) (backport #54928)
2026-05-14 11:04:07 +05:30
rohitwaghchaure
b79ec7cbdd chore: fix linter issue
(cherry picked from commit 3c993377aa)
(cherry picked from commit 21ada7799c)
2026-05-14 02:04:52 +00:00
rohitwaghchaure
927360dd1d chore: fixed test case
(cherry picked from commit c740f77a6f)
(cherry picked from commit f4e66914c6)
2026-05-14 02:04:52 +00:00
Rohit Waghchaure
ab090295d9 fix: posting date and time
(cherry picked from commit fb6c05f186)
(cherry picked from commit 1c44c60dbd)
2026-05-14 02:04:51 +00:00
Frappe PR Bot
c4b7b15824 chore(release): Bumped to Version 16.18.1
## [16.18.1](https://github.com/frappe/erpnext/compare/v16.18.0...v16.18.1) (2026-05-13)

### Reverts

* Revert "fix: debit credit not equal in purchase transactions for mult… (backport [#54906](https://github.com/frappe/erpnext/issues/54906)) (backport [#54908](https://github.com/frappe/erpnext/issues/54908)) ([#54916](https://github.com/frappe/erpnext/issues/54916)) ([cfd3847](cfd3847255))
2026-05-13 10:16:21 +00:00
mergify[bot]
cfd3847255 Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (backport #54908) (#54916)
Revert "fix: debit credit not equal in purchase transactions for mult… (backport #54906) (#54908)

Revert "fix: debit credit not equal in purchase transactions for mult… (#54906)

* Revert "fix: debit credit not equal in purchase transactions for multi currency"

This reverts commit 75bcea57f4.

* Revert "test: add test case"

This reverts commit 1d30a202c3.

* Revert "fix: include rejected qty in tax (purchase receipt)"

This reverts commit 8c9a88abbe.

(cherry picked from commit cf5e8ce878)


(cherry picked from commit 0d07083299)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 10:14:46 +00:00
Frappe PR Bot
dc914adb62 chore(release): Bumped to Version 16.18.0
# [16.18.0](https://github.com/frappe/erpnext/compare/v16.17.0...v16.18.0) (2026-05-12)

### Bug Fixes

* added permission validation for `deactivate_sales_person` (backport [#54884](https://github.com/frappe/erpnext/issues/54884)) ([#54886](https://github.com/frappe/erpnext/issues/54886)) ([98de025](98de025a09))
* check if item is dropshipped before updating quantity (backport [#54825](https://github.com/frappe/erpnext/issues/54825)) ([#54827](https://github.com/frappe/erpnext/issues/54827)) ([0db7e1e](0db7e1e56b))
* **crm:** handle empty _assign in appointment auto assignment (backport [#54782](https://github.com/frappe/erpnext/issues/54782)) ([#54795](https://github.com/frappe/erpnext/issues/54795)) ([f36bdaa](f36bdaadae))
* decimal issue ([8b9b83a](8b9b83a9df))
* do not rely on client side to update quantities during partial d… (backport [#54804](https://github.com/frappe/erpnext/issues/54804)) ([#54821](https://github.com/frappe/erpnext/issues/54821)) ([f24b556](f24b556336))
* fetch get_item_tax_template while update items (backport [#53708](https://github.com/frappe/erpnext/issues/53708)) ([#54767](https://github.com/frappe/erpnext/issues/54767)) ([4fbaea1](4fbaea17f8))
* incorrect serial nos picked during disassemble (backport [#54757](https://github.com/frappe/erpnext/issues/54757)) ([#54760](https://github.com/frappe/erpnext/issues/54760)) ([66ae590](66ae590adc))
* incorrect validation thrown for drop shipped PI (backport [#54751](https://github.com/frappe/erpnext/issues/54751)) ([#54753](https://github.com/frappe/erpnext/issues/54753)) ([379ebbe](379ebbe8c4))
* raw material should not have target warehouse in manufacture entry (backport [#54849](https://github.com/frappe/erpnext/issues/54849)) ([#54861](https://github.com/frappe/erpnext/issues/54861)) ([3dbadfa](3dbadfadd5))
* rename supplier wise stock analytics report ([7086db1](7086db1e1c))
* **stock:** apply filters for rejected warehouse in pick list (backport [#54733](https://github.com/frappe/erpnext/issues/54733)) ([#54776](https://github.com/frappe/erpnext/issues/54776)) ([cf0d9df](cf0d9dfbfd))
* **stock:** ignore reserved qty for stock levels in batch (backport [#54790](https://github.com/frappe/erpnext/issues/54790)) ([#54797](https://github.com/frappe/erpnext/issues/54797)) ([338d190](338d1904c1))
* **stock:** priorities pick list parent warehouse (backport [#54788](https://github.com/frappe/erpnext/issues/54788)) ([#54793](https://github.com/frappe/erpnext/issues/54793)) ([d3bc629](d3bc629f68))
* **task:** update depends_on for closing date and review date [#54850](https://github.com/frappe/erpnext/issues/54850) (backport [#54852](https://github.com/frappe/erpnext/issues/54852)) ([#54863](https://github.com/frappe/erpnext/issues/54863)) ([b962a1a](b962a1a0cd))
* validate variant values (backport [#54831](https://github.com/frappe/erpnext/issues/54831)) ([#54839](https://github.com/frappe/erpnext/issues/54839)) ([87b798b](87b798b936))

### Features

* partial delivery in dropshipping (backport [#54787](https://github.com/frappe/erpnext/issues/54787)) ([#54800](https://github.com/frappe/erpnext/issues/54800)) ([f64f871](f64f871d45))
* Philippines chart of account (backport [#53918](https://github.com/frappe/erpnext/issues/53918)) ([#54888](https://github.com/frappe/erpnext/issues/54888)) ([8f03108](8f0310859d))
2026-05-12 18:49:27 +00:00
diptanilsaha
41bff45d7a Merge pull request #54865 from frappe/version-16-hotfix
chore: release v16
2026-05-13 00:17:54 +05:30
mergify[bot]
8f0310859d feat: Philippines chart of account (backport #53918) (#54888)
feat: Added Philippines chart of account json file (#53918)

* feat: Added philipinnes chart of account json file



* feat: made changes as per review comments and corrected indentation

* feat: made changes as per review comments

* feat: made changes as per review comments to resolve the issues

* fix: fixed changes as per review comments



* fix: fixed changes as per review comments on bank group account



---------




(cherry picked from commit 5560f6c270)

Signed-off-by: Soham-ambibuzz <soham.pawar@ambibuzz.com>
Signed-off-by: soham7117 <sohampawar626@gmail.com>
Co-authored-by: Soham-ambibuzz <soham.pawar@ambibuzz.com>
Co-authored-by: soham7117 <sohampawar626@gmail.com>
2026-05-12 16:46:05 +00:00
mergify[bot]
98de025a09 fix: added permission validation for deactivate_sales_person (backport #54884) (#54886)
* fix: added permission validation for `deactivate_sales_person` (#54884)

(cherry picked from commit 9134db9cd3)

# Conflicts:
#	erpnext/setup/doctype/employee/employee.py

* chore: resolved conflict

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-12 16:31:21 +00:00
mergify[bot]
b962a1a0cd fix(task): update depends_on for closing date and review date #54850 (backport #54852) (#54863)
fix(task): update depends_on for closing date and review date #54850 (#54852)

(cherry picked from commit 3532c1cc69)

Co-authored-by: Jaypal Lakum <96212547+jp-the-dev@users.noreply.github.com>
2026-05-12 10:13:49 +00:00
mergify[bot]
3dbadfadd5 fix: raw material should not have target warehouse in manufacture entry (backport #54849) (#54861)
fix: raw material should not have target warehouse in manufacture entry (#54849)

(cherry picked from commit b5527cf328)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-12 09:46:45 +00:00
Nishka Gosalia
67ad437dd3 Merge pull request #54854 from frappe/mergify/bp/version-16-hotfix/pr-54835
fix: rename supplier wise stock analytics report (backport #54835)
2026-05-12 14:08:34 +05:30
nishkagosalia
7086db1e1c fix: rename supplier wise stock analytics report
(cherry picked from commit 85206e0278)
2026-05-12 07:08:04 +00:00
mergify[bot]
87b798b936 fix: validate variant values (backport #54831) (#54839)
fix: validate variant values (#54831)

(cherry picked from commit 95705f18aa)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-11 21:23:34 +05:30
ruthra kumar
b894b02ebc Merge pull request #54832 from frappe/mergify/bp/version-16-hotfix/pr-54828
refactor(test): speed up payment reconciliation tests (backport #54828)
2026-05-11 14:28:02 +05:30
ruthra kumar
1d20469c99 refactor(test): speed up payment reconciliation tests
(cherry picked from commit f58242dca7)
2026-05-11 08:40:07 +00:00
mergify[bot]
0db7e1e56b fix: check if item is dropshipped before updating quantity (backport #54825) (#54827)
fix: check if item is dropshipped before updating quantity (#54825)

(cherry picked from commit 23e9ad3fd9)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-11 08:08:09 +00:00
mergify[bot]
f24b556336 fix: do not rely on client side to update quantities during partial d… (backport #54804) (#54821)
* fix: do not rely on client side to update quantities during partial d… (#54804)

fix: do not rely on client side to update quantities during partial dropship
(cherry picked from commit 03acbc3dc9)

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

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-11 07:40:14 +00:00
MochaMind
1fcd2837e8 chore: update POT file (#54814) 2026-05-10 13:56:48 +02:00
mergify[bot]
f64f871d45 feat: partial delivery in dropshipping (backport #54787) (#54800)
* feat: partial delivery in dropshipping (#54787)

(cherry picked from commit db74360396)

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

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-09 02:01:42 +00:00
mergify[bot]
f36bdaadae fix(crm): handle empty _assign in appointment auto assignment (backport #54782) (#54795)
fix(crm): handle empty _assign in appointment auto assignment (#54782)

(cherry picked from commit a4a389bd41)

Co-authored-by: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com>
2026-05-08 12:55:38 +00:00
mergify[bot]
d3bc629f68 fix(stock): priorities pick list parent warehouse (backport #54788) (#54793)
fix(stock): priorities pick list parent warehouse (#54788)

(cherry picked from commit 4e850f31d5)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-05-08 12:42:02 +00:00
mergify[bot]
338d1904c1 fix(stock): ignore reserved qty for stock levels in batch (backport #54790) (#54797)
fix(stock): ignore reserved qty for stock levels in batch (#54790)

(cherry picked from commit 0b6a372a52)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-08 12:35:22 +00:00
mergify[bot]
4fbaea17f8 fix: fetch get_item_tax_template while update items (backport #53708) (#54767)
* fix: fetch get_item_tax_template while update items

(cherry picked from commit 03c9d16ca6)

* fix: resolve item tax template from item group in update items

(cherry picked from commit 97e7916b66)

* fix: resolve item tax template from item group in update items

(cherry picked from commit ad22256b2d)

---------

Co-authored-by: ervishnucs <ervishnucs369@gmail.com>
2026-05-07 16:04:20 +05:30
mergify[bot]
cf0d9dfbfd fix(stock): apply filters for rejected warehouse in pick list (backport #54733) (#54776)
fix(stock): apply filters for rejected warehouse in pick list (#54733)

(cherry picked from commit 0fc96e8f7d)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-07 10:31:19 +00:00
mergify[bot]
66ae590adc fix: incorrect serial nos picked during disassemble (backport #54757) (#54760)
fix: incorrect serial nos picked during disassemble

(cherry picked from commit 25f7fa548d)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-06 16:06:03 +05:30
mergify[bot]
379ebbe8c4 fix: incorrect validation thrown for drop shipped PI (backport #54751) (#54753)
* fix: incorrect validation thrown for drop shipped PI (#54751)

(cherry picked from commit 907a809f3f)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-06 05:55:26 +00:00
Frappe PR Bot
7b494dc9e8 chore(release): Bumped to Version 16.17.0
# [16.17.0](https://github.com/frappe/erpnext/compare/v16.16.0...v16.17.0) (2026-05-05)

### Bug Fixes

* accounts and account types in German CoA "SKR 03" (backport [#54711](https://github.com/frappe/erpnext/issues/54711)) ([#54713](https://github.com/frappe/erpnext/issues/54713)) ([982810a](982810a700))
* add missing fields in set_currency_labels (backport [#54689](https://github.com/frappe/erpnext/issues/54689)) ([#54690](https://github.com/frappe/erpnext/issues/54690)) ([bca893a](bca893a508))
* Backfill `not_applicable` on Item Tax Template Details for German companies (backport [#54682](https://github.com/frappe/erpnext/issues/54682)) ([#54686](https://github.com/frappe/erpnext/issues/54686)) ([a22d773](a22d773341))
* copy project from first row to new rows (backport [#53295](https://github.com/frappe/erpnext/issues/53295)) ([#54620](https://github.com/frappe/erpnext/issues/54620)) ([e24ab72](e24ab72c0d))
* correct project filter in buying doctypes (backport [#54644](https://github.com/frappe/erpnext/issues/54644)) ([#54652](https://github.com/frappe/erpnext/issues/54652)) ([86cf256](86cf256358))
* correct titles set to {customer_name} or {supplier_name} text strings (backport [#54656](https://github.com/frappe/erpnext/issues/54656)) ([#54669](https://github.com/frappe/erpnext/issues/54669)) ([38cfeb1](38cfeb1bb7))
* dont show serial/batch button when PR is submitted (backport [#54642](https://github.com/frappe/erpnext/issues/54642)) ([#54646](https://github.com/frappe/erpnext/issues/54646)) ([6dbc17d](6dbc17d71a))
* error when creating quotation from CRM (backport [#54722](https://github.com/frappe/erpnext/issues/54722)) ([#54725](https://github.com/frappe/erpnext/issues/54725)) ([2cd4c1a](2cd4c1a052))
* hide payment and payment request buttons based on permissions in invoices and orders (backport [#53920](https://github.com/frappe/erpnext/issues/53920)) ([#54736](https://github.com/frappe/erpnext/issues/54736)) ([e60490d](e60490dceb))
* incorrect expense account book in purchase return (backport [#54681](https://github.com/frappe/erpnext/issues/54681)) ([#54693](https://github.com/frappe/erpnext/issues/54693)) ([0dade2c](0dade2c38c))
* mark item tax templates as not applicable (backport [#54673](https://github.com/frappe/erpnext/issues/54673)) ([#54677](https://github.com/frappe/erpnext/issues/54677)) ([126e13b](126e13be25))
* **payment_entry:** convert the date args to string type before escaping in `get_outstanding_reference_documents` (backport [#54639](https://github.com/frappe/erpnext/issues/54639)) ([#54648](https://github.com/frappe/erpnext/issues/54648)) ([19a8ebe](19a8ebe8a5))
* **project:** use user.email for invitations and skip disabled users. (backport [#54561](https://github.com/frappe/erpnext/issues/54561)) ([#54667](https://github.com/frappe/erpnext/issues/54667)) ([288cdf3](288cdf3bf0))
* py error on sales forecast doctype (backport [#54641](https://github.com/frappe/erpnext/issues/54641)) ([#54643](https://github.com/frappe/erpnext/issues/54643)) ([7bd360a](7bd360aa29))
* Remove bom stock report link from manufacturing workspace ([0f27881](0f27881fed))
* **selling:** blanket order ordered qty recalculation on sales order status change (backport [#54593](https://github.com/frappe/erpnext/issues/54593)) ([#54623](https://github.com/frappe/erpnext/issues/54623)) ([9db03bc](9db03bc520))
* set valid_from in created Item Price (backport [#54696](https://github.com/frappe/erpnext/issues/54696)) ([#54700](https://github.com/frappe/erpnext/issues/54700)) ([bbb4e79](bbb4e79d0a))
* show correct status in Serial No Ledger (backport [#54567](https://github.com/frappe/erpnext/issues/54567)) ([#54626](https://github.com/frappe/erpnext/issues/54626)) ([d6f2ff6](d6f2ff6b87))
* show in and out qty in the stock ledger report for stock recos ([d27cf48](d27cf48b19))
* skip depreciation rescheduling when asset is fully depreciated on sale ([d3c893d](d3c893d08b))
* skip rescheduling only for asset being disposed ([07a957c](07a957c164))
* use RecoverableErrors isinstance check for repost timeout status (backport [#54543](https://github.com/frappe/erpnext/issues/54543)) ([#54649](https://github.com/frappe/erpnext/issues/54649)) ([b300159](b3001595ab))

### Features

* copy terms attachments to transactions (backport [#53403](https://github.com/frappe/erpnext/issues/53403)) ([#54661](https://github.com/frappe/erpnext/issues/54661)) ([bd932da](bd932da08b))
* **ux:** Naming series dialog ([#54554](https://github.com/frappe/erpnext/issues/54554)) ([48ebb4c](48ebb4ca61))

### Performance Improvements

* max recursion depth error in serial no (backport [#54629](https://github.com/frappe/erpnext/issues/54629)) ([#54631](https://github.com/frappe/erpnext/issues/54631)) ([808214f](808214fd95))
2026-05-05 16:32:20 +00:00
rohitwaghchaure
2bc07f18a7 Merge pull request #54745 from frappe/mergify/bp/version-16-hotfix/pr-54723
fix: decimal issue in stock ageing report (backport #54723)
2026-05-05 22:02:16 +05:30
diptanilsaha
ed69dafbe8 Merge pull request #54740 from frappe/version-16-hotfix 2026-05-05 22:00:39 +05:30
Nishka Gosalia
c985f94009 Merge pull request #54743 from frappe/mergify/bp/version-16-hotfix/pr-54732
fix: Remove bom stock report link from manufacturing workspace (backport #54732)
2026-05-05 16:44:55 +05:30
Rohit Waghchaure
8b9b83a9df fix: decimal issue
(cherry picked from commit 542eb6aca4)
2026-05-05 11:13:04 +00:00
nishkagosalia
0f27881fed fix: Remove bom stock report link from manufacturing workspace
(cherry picked from commit f86568b078)
2026-05-05 10:51:13 +00:00
mergify[bot]
e60490dceb fix: hide payment and payment request buttons based on permissions in invoices and orders (backport #53920) (#54736)
Co-authored-by: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com>
Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
fix: hide payment and payment request buttons based on permissions in invoices and orders (#53920)
2026-05-05 12:17:57 +05:30
mergify[bot]
2cd4c1a052 fix: error when creating quotation from CRM (backport #54722) (#54725)
fix: error when creating quotation from CRM (#54722)

(cherry picked from commit 2d3190effb)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-04 16:04:03 +00:00
mergify[bot]
982810a700 fix: accounts and account types in German CoA "SKR 03" (backport #54711) (#54713)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: accounts and account types in German CoA "SKR 03" (#54711)
2026-05-03 17:49:02 +00:00
MochaMind
18006b978f chore: update POT file (#54710) 2026-05-03 14:24:28 +02:00
mergify[bot]
bbb4e79d0a fix: set valid_from in created Item Price (backport #54696) (#54700)
* fix: set valid_from in created Item Price (#54696)

Co-authored-by: Kaajal-Chhattani <kaajal.chhattani@aurigait.com>
(cherry picked from commit 6246a9aa6e)

# Conflicts:
#	erpnext/stock/get_item_details.py

* chore: resolve conflicts

---------

Co-authored-by: Kaajalchhattani <89331214+Kaajalchhattani@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-02 16:45:52 +00:00
mergify[bot]
bca893a508 fix: add missing fields in set_currency_labels (backport #54689) (#54690)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: add missing fields in set_currency_labels (#54689)
2026-05-01 14:39:39 +02:00
mergify[bot]
0dade2c38c fix: incorrect expense account book in purchase return (backport #54681) (#54693)
fix: incorrect expense account book in purchase return

(cherry picked from commit 2a720e7008)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-01 12:47:05 +05:30
mergify[bot]
a22d773341 fix: Backfill not_applicable on Item Tax Template Details for German companies (backport #54682) (#54686)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: Backfill `not_applicable` on Item Tax Template Details for German companies (#54682)
2026-05-01 04:29:06 +02:00
mergify[bot]
126e13be25 fix: mark item tax templates as not applicable (backport #54673) (#54677)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: mark item tax templates as not applicable (#54673)
2026-04-30 17:52:24 +02:00
mergify[bot]
288cdf3bf0 fix(project): use user.email for invitations and skip disabled users. (backport #54561) (#54667)
fix(project): use user.email for invitations and skip disabled users. (#54561)

* fix(project): use user.email for invitations and skip disabled users.

* Update erpnext/projects/doctype/project/project.py



* fix(project): remove duplicate loop causing indentation error

* fix(project): resolve pre-commit hook failure

---------


(cherry picked from commit 231dd1856f)

Co-authored-by: Hemil-Sangani <hemil@sanskartechnolab.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-30 14:35:12 +05:30
rohitwaghchaure
2422237c1a Merge pull request #54671 from frappe/mergify/bp/version-16-hotfix/pr-54664
fix: show in and out qty in the stock ledger report for stock recos (backport #54664)
2026-04-30 14:34:23 +05:30
mergify[bot]
38cfeb1bb7 fix: correct titles set to {customer_name} or {supplier_name} text strings (backport #54656) (#54669)
Co-authored-by: Trusted Computer <75872475+trustedcomputer@users.noreply.github.com>
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
fix: correct titles set to {customer_name} or {supplier_name} text strings (#54656)
2026-04-30 08:52:23 +00:00
Rohit Waghchaure
d27cf48b19 fix: show in and out qty in the stock ledger report for stock recos
(cherry picked from commit da081254a6)
2026-04-30 08:44:26 +00:00
Khushi Rawat
c232f1f450 Merge pull request #54659 from frappe/mergify/bp/version-16-hotfix/pr-54658
fix: skip depreciation rescheduling when asset is fully depreciated on sale (backport #54658)
2026-04-30 11:31:15 +05:30
mergify[bot]
bd932da08b feat: copy terms attachments to transactions (backport #53403) (#54661)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-04-29 23:43:28 +02:00
khushi8112
07a957c164 fix: skip rescheduling only for asset being disposed
(cherry picked from commit 88b82383f5)
2026-04-29 21:05:17 +00:00
khushi8112
d3c893d08b fix: skip depreciation rescheduling when asset is fully depreciated on sale
(cherry picked from commit c4155b6c81)
2026-04-29 21:05:17 +00:00
mergify[bot]
b3001595ab fix: use RecoverableErrors isinstance check for repost timeout status (backport #54543) (#54649)
fix: use RecoverableErrors isinstance check for repost timeout status

When a Repost Item Valuation job is killed by an RQ worker timeout
(JobTimeoutException raised via SIGALRM), the existing status detection
relied solely on traceback string matching for 'timeout' or 'Deadlock'.

This is unreliable because SIGALRM can interrupt a C-extension call
(e.g. inside pypika's copy.copy()) before Python records the exception
in the traceback. In that case the traceback shows only the interrupted
frame -- not JobTimeoutException -- so the job is permanently marked
'Failed' instead of 'In Progress', preventing the scheduler from
automatically retrying it.

RecoverableErrors = (JobTimeoutException, QueryDeadlockError,
QueryTimeoutError) is already defined at the top of this file and is
already used further down in the same except block to suppress email
notifications. Extend its use to also guard the status decision.

The traceback string fallback is kept as a secondary check for
forward compatibility with other timeout signals.

Fixes: jobs permanently stuck as 'Failed' after RQ worker timeout,
requiring manual re-queue to resume reposting.

(cherry picked from commit a49e2de866)

Co-authored-by: Assem Bahnasy <bahnasyassem@gmail.com>
2026-04-29 12:02:04 +00:00
mergify[bot]
86cf256358 fix: correct project filter in buying doctypes (backport #54644) (#54652)
fix: correct project filter in buying doctypes (#54644)

(cherry picked from commit a04c028522)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 17:28:12 +05:30
mergify[bot]
19a8ebe8a5 fix(payment_entry): convert the date args to string type before escaping in get_outstanding_reference_documents (backport #54639) (#54648)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): convert the date args to string type before escaping in `get_outstanding_reference_documents` (#54639)
2026-04-29 11:45:24 +00:00
mergify[bot]
6dbc17d71a fix: dont show serial/batch button when PR is submitted (backport #54642) (#54646)
fix: dont show serial/batch button when PR is submitted (#54642)

(cherry picked from commit 060defcc2b)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 11:32:37 +00:00
mergify[bot]
7bd360aa29 fix: py error on sales forecast doctype (backport #54641) (#54643)
fix: py error on sales forecast doctype (#54641)

fix: py error on sales forecase doctype
(cherry picked from commit d0d8cff48f)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 11:13:18 +00:00
Nishka Gosalia
2e438011da Merge pull request #54635 from frappe/mergify/bp/version-16-hotfix/pr-54554 2026-04-29 15:21:22 +05:30
Nishka Gosalia
48ebb4ca61 feat(ux): Naming series dialog (#54554)
(cherry picked from commit 844f3dbc0b)
2026-04-29 09:15:45 +00:00
mergify[bot]
808214fd95 perf: max recursion depth error in serial no (backport #54629) (#54631)
perf: max recursion depth error in serial no (#54629)

(cherry picked from commit 503b5bf140)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 08:53:07 +00:00
mergify[bot]
d6f2ff6b87 fix: show correct status in Serial No Ledger (backport #54567) (#54626)
* refactor: extract SN status logic

(cherry picked from commit cb2e6e1e2e)

* fix: show correct status in Serial No Ledger

(cherry picked from commit 2b3e047143)

---------

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2026-04-29 13:55:18 +05:30
mergify[bot]
9db03bc520 fix(selling): blanket order ordered qty recalculation on sales order status change (backport #54593) (#54623)
fix(selling): blanket order ordered qty recalculation on sales order status change (#54593)

(cherry picked from commit d68801e73a)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-04-29 06:47:55 +00:00
mergify[bot]
e24ab72c0d fix: copy project from first row to new rows (backport #53295) (#54620)
fix: copy project to new item row from parent

(cherry picked from commit 68cc518497)

Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
2026-04-29 11:55:46 +05:30
74 changed files with 3547 additions and 1571 deletions

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.16.0"
__version__ = "16.18.2"
def get_default_company(user=None):

View File

@@ -34,6 +34,13 @@
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Anlagen im Bau": {
"is_group": 1,
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
"account_number": "0498",
"account_type": "Capital Work in Progress"
}
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
@@ -317,13 +324,21 @@
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
"account_number": "3960",
"account_type": "Stock Adjustment"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
"account_number": "2310",
"account_type": "Expense Account"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Stock Adjustment"
"account_type": "Expense Account"
},
"Verwaltungskosten": {
"account_number": "4997",
@@ -340,7 +355,7 @@
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Accumulated Depreciation"
"account_type": "Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",

View File

@@ -0,0 +1,840 @@
{
"name": "Philippines",
"country": "Philippines",
"tree": {
"Asset": {
"account_number": "1000",
"is_group": 1,
"root_type": "Asset",
"Current Assets": {
"account_number": "1001",
"is_group": 1,
"root_type": "Asset",
"Cash": {
"account_number": "1100",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Cash on Hand": {
"account_number": "1101",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
},
"Petty Cash Fund": {
"account_number": "1200",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Petty Cash Fund": {
"account_number": "1201",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
}
}
},
"Bank Accounts": {
"account_number": "1102",
"is_group": 1,
"root_type": "Asset",
"account_type": "Bank"
},
"Advances to Officers & Employees": {
"account_number": "1290",
"is_group": 1,
"root_type": "Asset",
"Advances to Officers & Employees": {
"account_number": "1291",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable Trade": {
"account_number": "1300",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Trade": {
"account_number": "1301",
"is_group": 0,
"root_type": "Asset",
"account_type": "Receivable"
}
},
"Accounts Receivable - Affiliates": {
"account_number": "1310",
"is_group": 1,
"root_type": "Asset",
"Due from Company": {
"account_number": "1311",
"is_group": 0,
"root_type": "Asset"
}
},
"Accounts Receivable - Others": {
"account_number": "1400",
"is_group": 1,
"root_type": "Asset",
"Accounts Receivable - Others": {
"account_number": "1401",
"is_group": 0,
"root_type": "Asset"
}
},
"Parts, Materials and Supplies": {
"account_number": "1500",
"is_group": 1,
"root_type": "Asset",
"Parts, Materials and Supplies": {
"account_number": "1501",
"is_group": 0,
"root_type": "Asset"
},
"Raw Materials - Demo": {
"account_number": "1502",
"is_group": 0,
"root_type": "Asset"
}
},
"Project in Progress": {
"account_number": "1510",
"is_group": 1,
"root_type": "Asset",
"Project in Progress": {
"account_number": "1511",
"is_group": 0,
"root_type": "Asset"
},
"Factory Overhead Variance": {
"account_number": "1512",
"is_group": 0,
"root_type": "Asset"
}
},
"Finished Goods": {
"account_number": "1520",
"is_group": 1,
"root_type": "Asset",
"Finished Goods Inventory": {
"account_number": "1531",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock"
},
"Inventory in Transit": {
"account_number": "1532",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock Adjustment"
}
},
"Prepayments": {
"account_number": "1600",
"is_group": 1,
"root_type": "Asset",
"Prepaid Insurance & Bonds": {
"account_number": "1601",
"is_group": 0,
"root_type": "Asset"
},
"Prepaid Rent": {
"account_number": "1602",
"is_group": 0,
"root_type": "Asset"
}
},
"VAT Input Tax": {
"account_number": "1610",
"is_group": 1,
"root_type": "Asset",
"VAT Input Tax - Goods": {
"account_number": "1611",
"is_group": 0,
"root_type": "Asset",
"account_type": "Tax"
}
}
},
"Non - Current Assets": {
"account_number": "1002",
"is_group": 1,
"root_type": "Asset",
"Property, Plants And Equipments": {
"account_number": "1700",
"is_group": 1,
"root_type": "Asset",
"Land": {
"account_number": "1701",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Buildings & Improvements": {
"account_number": "1702",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Delivery & Trans Equipment": {
"account_number": "1703",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Furniture & Fixtures": {
"account_number": "1704",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
},
"Machinery & Equipment": {
"account_number": "1705",
"is_group": 0,
"root_type": "Asset",
"account_type": "Fixed Asset"
}
},
"Accum Depr. - Property, Plants and Equipment": {
"account_number": "1800",
"is_group": 1,
"root_type": "Asset",
"Accumulated Dep Bdgs & Improv": {
"account_number": "1801",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Delivery & Trans": {
"account_number": "1802",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Dep Furniture & Fixture": {
"account_number": "1803",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
},
"Accumulated Depreciation - Machinery & Equipment": {
"account_number": "1804",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
}
}
},
"Other Assets": {
"account_number": "1003",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1900",
"is_group": 1,
"root_type": "Asset",
"Advances To Supplier": {
"account_number": "1901",
"is_group": 0,
"root_type": "Asset"
}
},
"Miscellaneous Deposits": {
"account_number": "1910",
"is_group": 1,
"root_type": "Asset",
"Miscellaneous Deposits": {
"account_number": "1911",
"is_group": 0,
"root_type": "Asset"
}
},
"Retirement Fund": {
"account_number": "1920",
"is_group": 1,
"root_type": "Asset",
"Retirement Fund": {
"account_number": "1921",
"is_group": 0,
"root_type": "Asset"
}
},
"Investment": {
"account_number": "1930",
"is_group": 1,
"root_type": "Asset",
"Investment": {
"account_number": "1931",
"is_group": 0,
"root_type": "Asset"
}
},
"System Development": {
"account_number": "1940",
"is_group": 1,
"root_type": "Asset",
"System Development": {
"account_number": "1941",
"is_group": 0,
"root_type": "Asset"
}
}
}
},
"Liability": {
"account_number": "2000",
"is_group": 1,
"root_type": "Liability",
"Current Liabilities": {
"account_number": "2001",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable Trade": {
"account_number": "2100",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Trade": {
"account_number": "2101",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Accounts Payable Others": {
"account_number": "2110",
"is_group": 1,
"root_type": "Liability",
"Accounts Payable - Payroll": {
"account_number": "2111",
"is_group": 0,
"root_type": "Liability"
}
},
"VAT Output Tax": {
"account_number": "2200",
"is_group": 1,
"root_type": "Liability",
"VAT Output Tax": {
"account_number": "2201",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Wages": {
"account_number": "2210",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Wages": {
"account_number": "2211",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Withholding Taxes Payable Expanded": {
"account_number": "2220",
"is_group": 1,
"root_type": "Liability",
"Withholding Taxes Payable Expanded": {
"account_number": "2221",
"is_group": 0,
"root_type": "Liability",
"account_type": "Tax"
}
},
"Accruals And Other Current Payables": {
"account_number": "2300",
"is_group": 1,
"root_type": "Liability",
"Stock Received But Not Billed": {
"account_number": "2301",
"is_group": 0,
"root_type": "Liability",
"account_type": "Stock Received But Not Billed"
}
},
"Payable to Government and Other Institutions": {
"account_number": "2400",
"is_group": 1,
"root_type": "Liability",
"SSS Premium Payable": {
"account_number": "2401",
"is_group": 0,
"root_type": "Liability"
},
"SSS Salary Loan Payable": {
"account_number": "2402",
"is_group": 0,
"root_type": "Liability"
},
"PhilHealth Premium": {
"account_number": "2403",
"is_group": 0,
"root_type": "Liability"
},
"Pag-ibig Loan Payable": {
"account_number": "2404",
"is_group": 0,
"root_type": "Liability"
},
"Coop Loans": {
"account_number": "2405",
"is_group": 0,
"root_type": "Liability"
},
"Coop Contributions": {
"account_number": "2406",
"is_group": 0,
"root_type": "Liability"
},
"Canteen": {
"account_number": "2407",
"is_group": 0,
"root_type": "Liability"
},
"AUB Loan Payable": {
"account_number": "2408",
"is_group": 0,
"root_type": "Liability"
},
"HSBC Loan Payable": {
"account_number": "2409",
"is_group": 0,
"root_type": "Liability"
}
},
"Customer Deposits": {
"account_number": "2500",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
}
},
"Non Current Liabilities": {
"account_number": "2002",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2600",
"is_group": 1,
"root_type": "Liability",
"Due To Associated Company": {
"account_number": "2601",
"is_group": 0,
"root_type": "Liability"
}
},
"Deferred Income": {
"account_number": "2700",
"is_group": 1,
"root_type": "Liability",
"Deferred Income": {
"account_number": "2701",
"is_group": 0,
"root_type": "Liability"
}
},
"Notes Payable": {
"account_number": "2800",
"is_group": 1,
"root_type": "Liability",
"Notes Payable": {
"account_number": "2801",
"is_group": 0,
"root_type": "Liability"
}
},
"Dividends Payable": {
"account_number": "2900",
"is_group": 0,
"root_type": "Liability"
}
}
},
"Equity": {
"account_number": "3000",
"is_group": 1,
"root_type": "Equity",
"STOCKHOLDER'S EQUITY": {
"account_number": "3001",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3100",
"is_group": 1,
"root_type": "Equity",
"Capital Stocks": {
"account_number": "3101",
"is_group": 0,
"root_type": "Equity"
}
},
"Subscription Receivable": {
"account_number": "3200",
"is_group": 1,
"root_type": "Equity",
"Subscription Receivable": {
"account_number": "3201",
"is_group": 0,
"root_type": "Equity"
}
},
"Retained Earnings": {
"account_number": "3300",
"is_group": 1,
"root_type": "Equity",
"Retained Earnings": {
"account_number": "3301",
"is_group": 0,
"root_type": "Equity"
}
},
"Current Year (Profit/Loss)": {
"account_number": "3400",
"is_group": 0,
"root_type": "Equity"
},
"Drawings": {
"account_number": "3500",
"is_group": 1,
"root_type": "Equity",
"Drawings": {
"account_number": "3501",
"is_group": 0,
"root_type": "Equity"
}
}
}
},
"Income": {
"account_number": "4000",
"is_group": 1,
"root_type": "Income",
"Gross Sales": {
"account_number": "4100",
"is_group": 1,
"root_type": "Income",
"Sales": {
"account_number": "4101",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Adjustment": {
"account_number": "4200",
"is_group": 1,
"root_type": "Income",
"Sales Return And Allowance": {
"account_number": "4201",
"is_group": 0,
"root_type": "Income"
}
},
"Sales Discount": {
"account_number": "4300",
"is_group": 1,
"root_type": "Income",
"Sales Discount": {
"account_number": "4301",
"is_group": 0,
"root_type": "Income"
}
},
"Other Income": {
"account_number": "6000",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6010",
"is_group": 1,
"root_type": "Income",
"Interest Income Bank": {
"account_number": "6011",
"is_group": 0,
"root_type": "Income"
}
},
"Dividend Income": {
"account_number": "6020",
"is_group": 1,
"root_type": "Income",
"Dividend Income": {
"account_number": "6021",
"is_group": 0,
"root_type": "Income"
}
}
}
},
"Expense": {
"account_number": "5000",
"is_group": 1,
"root_type": "Expense",
"Operating Expenses": {
"account_number": "5100",
"is_group": 1,
"root_type": "Expense",
"Salaries, Wages": {
"account_number": "5101",
"is_group": 0,
"root_type": "Expense"
},
"13th Month Pay & Bonus": {
"account_number": "5102",
"is_group": 0,
"root_type": "Expense"
},
"Overtime & Night Diff": {
"account_number": "5103",
"is_group": 0,
"root_type": "Expense"
},
"Incentive/Performance Bonus": {
"account_number": "5104",
"is_group": 0,
"root_type": "Expense"
},
"Employees Benefits": {
"account_number": "5105",
"is_group": 0,
"root_type": "Expense"
},
"Advertising & Promotions": {
"account_number": "5106",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Leasehold Improvement": {
"account_number": "5107",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of Pre-Operating": {
"account_number": "5108",
"is_group": 0,
"root_type": "Expense"
},
"Amortization of System Development": {
"account_number": "5109",
"is_group": 0,
"root_type": "Expense"
},
"Audit & Legal Fee": {
"account_number": "5110",
"is_group": 0,
"root_type": "Expense"
},
"Bad Debts Expenses": {
"account_number": "5111",
"is_group": 0,
"root_type": "Expense"
},
"Client Service & Maintenance": {
"account_number": "5112",
"is_group": 0,
"root_type": "Expense"
},
"Commission Expenses": {
"account_number": "5113",
"is_group": 0,
"root_type": "Expense"
},
"Communications": {
"account_number": "5114",
"is_group": 0,
"root_type": "Expense"
},
"Contractual Services": {
"account_number": "5115",
"is_group": 0,
"root_type": "Expense"
},
"Depreciation Expenses": {
"account_number": "5116",
"is_group": 0,
"root_type": "Expense",
"account_type": "Depreciation"
},
"Donation & Contribution": {
"account_number": "5117",
"is_group": 0,
"root_type": "Expense"
},
"Dues & Subscription": {
"account_number": "5118",
"is_group": 0,
"root_type": "Expense"
},
"Employee Med/Dental/Hosp Expenses": {
"account_number": "5119",
"is_group": 0,
"root_type": "Expense"
},
"Employee Uniforms": {
"account_number": "5120",
"is_group": 0,
"root_type": "Expense"
},
"Equipage": {
"account_number": "5121",
"is_group": 0,
"root_type": "Expense"
},
"Expenses for Reclassification": {
"account_number": "5122",
"is_group": 0,
"root_type": "Expense"
},
"Gas & Oil": {
"account_number": "5123",
"is_group": 0,
"root_type": "Expense"
},
"Insurance Expenses": {
"account_number": "5124",
"is_group": 0,
"root_type": "Expense"
},
"Light & Water": {
"account_number": "5125",
"is_group": 0,
"root_type": "Expense"
},
"Local/Overseas Travel": {
"account_number": "5126",
"is_group": 0,
"root_type": "Expense"
},
"Meals & Transportation Expenses": {
"account_number": "5127",
"is_group": 0,
"root_type": "Expense"
},
"Meeting & Conferences": {
"account_number": "5128",
"is_group": 0,
"root_type": "Expense"
},
"Miscellaneous Expenses": {
"account_number": "5129",
"is_group": 0,
"root_type": "Expense"
},
"Mockup Expenses": {
"account_number": "5130",
"is_group": 0,
"root_type": "Expense"
},
"Obsolescence Expenses": {
"account_number": "5131",
"is_group": 0,
"root_type": "Expense"
},
"Other Support Cost": {
"account_number": "5132",
"is_group": 0,
"root_type": "Expense"
},
"Pag-ibig Contribution": {
"account_number": "5133",
"is_group": 0,
"root_type": "Expense"
},
"Performance Bonds": {
"account_number": "5134",
"is_group": 0,
"root_type": "Expense"
},
"Pre Employment Expenses": {
"account_number": "5135",
"is_group": 0,
"root_type": "Expense"
},
"Professional Fees": {
"account_number": "5136",
"is_group": 0,
"root_type": "Expense"
},
"Recruitment & Employment": {
"account_number": "5137",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses": {
"account_number": "5138",
"is_group": 0,
"root_type": "Expense"
},
"Rent Expenses Others": {
"account_number": "5139",
"is_group": 0,
"root_type": "Expense"
},
"Repairs & Maintenance": {
"account_number": "5140",
"is_group": 0,
"root_type": "Expense"
},
"Representation Expenses": {
"account_number": "5141",
"is_group": 0,
"root_type": "Expense"
},
"Research & Development": {
"account_number": "5142",
"is_group": 0,
"root_type": "Expense"
},
"Security Expenses": {
"account_number": "5143",
"is_group": 0,
"root_type": "Expense"
},
"Shared Services Fee": {
"account_number": "5144",
"is_group": 0,
"root_type": "Expense"
},
"SSS/Medicare/EC Contributions": {
"account_number": "5145",
"is_group": 0,
"root_type": "Expense"
},
"Stationery & Supplies": {
"account_number": "5146",
"is_group": 0,
"root_type": "Expense"
},
"Taxes & Licenses": {
"account_number": "5147",
"is_group": 0,
"root_type": "Expense",
"account_type": "Tax"
},
"Training & Seminar": {
"account_number": "5148",
"is_group": 0,
"root_type": "Expense"
}
},
"Stock Adjustment": {
"account_number": "5200",
"is_group": 0,
"root_type": "Expense",
"account_type": "Stock Adjustment"
},
"Round Off": {
"account_number": "5300",
"is_group": 0,
"root_type": "Expense",
"account_type": "Round Off"
},
"Expenses Included In Valuation": {
"account_number": "5400",
"is_group": 0,
"root_type": "Expense",
"account_type": "Expenses Included In Valuation"
}
}
}
}

View File

@@ -2328,16 +2328,19 @@ def get_outstanding_reference_documents(args, validate=False):
}
for fieldname, date_fields in date_fields_dict.items():
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += f" and {fieldname} between {frappe.db.escape(args.get(date_fields[0]))} and {frappe.db.escape(args.get(date_fields[1]))}"
condition += f" and {fieldname} between {from_date} and {to_date}"
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += f" and {fieldname} >= {frappe.db.escape(args.get(date_fields[0]))}"
condition += f" and {fieldname} >= {from_date}"
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += f" and {fieldname} <= {frappe.db.escape(args.get(date_fields[1]))}"
condition += f" and {fieldname} <= {to_date}"
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):

View File

@@ -21,122 +21,23 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPaymentReconciliation(ERPNextTestSuite):
def setUp(self):
self.create_company()
self.create_item()
self.create_customer()
self.create_account()
self.create_cost_center()
self.clear_old_entries()
def create_company(self):
company = None
if frappe.db.exists("Company", "_Test Payment Reconciliation"):
company = frappe.get_doc("Company", "_Test Payment Reconciliation")
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test Payment Reconciliation",
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses - _PR"
self.income_account = "Sales - _PR"
self.expense_account = "Cost of Goods Sold - _PR"
self.debit_to = "Debtors - _PR"
self.creditors = "Creditors - _PR"
self.cash = "Cash - _PR"
# create bank account
if frappe.db.exists("Account", "HDFC - _PR"):
self.bank = "HDFC - _PR"
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - _PR",
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name
def create_item(self):
item = create_item(
item_code="_Test PR Item", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
self.customer = make_customer("_Test PR Customer")
self.customer2 = make_customer("_Test PR Customer 2")
self.customer3 = make_customer("_Test PR Customer 3", "EUR")
self.customer4 = make_customer("_Test PR Customer 4", "EUR")
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
def create_account(self):
accounts = [
{
"attribute": "debtors_eur",
"account_name": "Debtors EUR",
"parent_account": "Accounts Receivable - _PR",
"account_currency": "EUR",
"account_type": "Receivable",
},
{
"attribute": "creditors_usd",
"account_name": "Payable USD",
"parent_account": "Accounts Payable - _PR",
"account_currency": "USD",
"account_type": "Payable",
},
# 'Payable' account for capturing advance paid, under 'Assets' group
{
"attribute": "advance_payable_account",
"account_name": "Advance Paid",
"parent_account": "Current Assets - _PR",
"account_currency": "INR",
"account_type": "Payable",
},
# 'Receivable' account for capturing advance received, under 'Liabilities' group
{
"attribute": "advance_receivable_account",
"account_name": "Advance Received",
"parent_account": "Current Liabilities - _PR",
"account_currency": "INR",
"account_type": "Receivable",
},
]
for x in accounts:
x = frappe._dict(x)
if not frappe.db.get_value(
"Account", filters={"account_name": x.account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = x.account_name
acc.parent_account = x.parent_account
acc.company = self.company
acc.account_currency = x.account_currency
acc.account_type = x.account_type
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": x.account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
setattr(self, x.attribute, acc.name)
self.company = "_Test Company"
self.debit_to = "Debtors - _TC"
self.creditors = "Creditors - _TC"
self.bank = "HDFC - _TC"
self.cash = "Cash - _TC"
self.item = "_Test Item"
self.cost_center = self.main_cc = "Main - _TC"
self.sub_cc = "Sub - _TC"
self.customer = "_Test Customer"
self.advance_receivable_account = "Advance Received - _TC"
self.advance_payable_account = "Advance Paid - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.warehouse = "All Warehouses - _TC"
self.customer_usd = "_Test Customer USD"
self.debtors_usd = "_Test Receivable USD - _TC"
self.creditors_usd = "_Test Payable USD - _TC"
def create_sales_invoice(
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
@@ -253,18 +154,6 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
return pord
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_payment_reconciliation(self, party_is_customer=True):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
@@ -300,22 +189,6 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
return je
def create_cost_center(self):
# Setup cost center
cc_name = "Sub"
self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company))
cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name})
if cc_exists:
self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name)
else:
sub_cc = frappe.new_doc("Cost Center")
sub_cc.cost_center_name = "Sub"
sub_cc.parent_cost_center = self.main_cc.parent_cost_center
sub_cc.company = self.main_cc.company
self.sub_cc = sub_cc.save()
def test_filter_min_max(self):
# check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300)
@@ -478,7 +351,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_payment_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
sales = "Sales - _TC"
amount = 921
# debit debtors account to record an invoice
je = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
@@ -513,7 +386,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
transaction_date = nowdate()
self.supplier = "_Test Supplier USD"
self.supplier2 = make_supplier("_Test Supplier2 USD", "USD")
self.supplier2 = "_Test Another Supplier USD"
amount = 100
exc_rate1 = 80
exc_rate2 = 83
@@ -666,7 +539,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_journal_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
sales = "Sales - _TC"
amount = 100
# debit debtors account to simulate a invoice
@@ -841,47 +714,49 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_pr_output_foreign_currency_and_amount(self):
# test for currency and amount invoices and payments
transaction_date = nowdate()
# In EUR
# In USD
amount = 100
exchange_rate = 80
si = self.create_sales_invoice(
qty=1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
si.customer = self.customer3
si.currency = "EUR"
si.customer = self.customer_usd
si.currency = "USD"
si.conversion_rate = exchange_rate
si.debit_to = self.debtors_eur
si.debit_to = self.debtors_usd
si = si.save().submit()
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.customer = self.customer3
cr_note.customer = self.customer_usd
cr_note.is_return = 1
cr_note.currency = "EUR"
cr_note.currency = "USD"
cr_note.conversion_rate = exchange_rate
cr_note.debit_to = self.debtors_eur
cr_note.debit_to = self.debtors_usd
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer3
pr.receivable_payable_account = self.debtors_eur
pr.party = self.customer_usd
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.invoices[0].amount, amount)
self.assertEqual(pr.invoices[0].currency, "EUR")
self.assertEqual(pr.invoices[0].currency, "USD")
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
self.assertEqual(pr.payments[0].currency, "USD")
cr_note.cancel()
pay = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=self.customer3)
pay.paid_from = self.debtors_eur
pay.paid_from_account_currency = "EUR"
pay = self.create_payment_entry(
amount=amount, posting_date=transaction_date, customer=self.customer_usd
)
pay.paid_from = self.debtors_usd
pay.paid_from_account_currency = "USD"
pay.source_exchange_rate = exchange_rate
pay.received_amount = exchange_rate * amount
pay = pay.save().submit()
@@ -890,21 +765,21 @@ class TestPaymentReconciliation(ERPNextTestSuite):
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
self.assertEqual(pr.payments[0].currency, "USD")
def test_difference_amount_via_journal_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.customer = self.customer_usd
si.currency = "USD"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.debit_to = self.debtors_usd
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1 = self.create_journal_entry("HDFC - _TC", self.debtors_usd, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = 0
@@ -912,7 +787,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.accounts[0].debit_in_account_currency = 8000
je1.accounts[0].debit = 8000
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].party = self.customer_usd
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
@@ -921,7 +796,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2 = self.create_journal_entry("HDFC - _TC", self.debtors_usd, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = 0
@@ -929,7 +804,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je2.accounts[0].debit_in_account_currency = 16000
je2.accounts[0].debit = 16000
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].party = self.customer_usd
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
@@ -939,8 +814,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.party = self.customer_usd
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
@@ -960,7 +835,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
pr.allocation[0].difference_account = "Exchange Gain/Loss - _TC"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
@@ -969,7 +844,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pr.reconcile()
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
{"account": self.debtors_usd, "docstatus": 1, "reference_name": si.name},
[{"SUM": "credit", "as": "amount"}],
group_by="reference_name",
)[0].amount
@@ -979,7 +854,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
filters={"account": self.debtors_usd, "docstatus": 1, "reference_name": si.name, "credit": 500},
fields=["parent"],
)[0]
self.assertEqual(
@@ -991,14 +866,14 @@ class TestPaymentReconciliation(ERPNextTestSuite):
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.customer = self.customer_usd
si.currency = "USD"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.debit_to = self.debtors_usd
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1 = self.create_journal_entry("HDFC - _TC", self.debtors_usd, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = -8000
@@ -1006,7 +881,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.accounts[0].debit_in_account_currency = 0
je1.accounts[0].debit = 0
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].party = self.customer_usd
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
@@ -1015,7 +890,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2 = self.create_journal_entry("HDFC - _TC", self.debtors_usd, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = -16000
@@ -1023,7 +898,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je2.accounts[0].debit_in_account_currency = 0
je2.accounts[0].debit = 0
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].party = self.customer_usd
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
@@ -1033,8 +908,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.party = self.customer_usd
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
@@ -1054,7 +929,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
pr.allocation[0].difference_account = "Exchange Gain/Loss - _TC"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
@@ -1063,7 +938,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pr.reconcile()
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
{"account": self.debtors_usd, "docstatus": 1, "reference_name": si.name},
[{"SUM": "credit", "as": "amount"}],
group_by="reference_name",
)[0].amount
@@ -1073,7 +948,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
filters={"account": self.debtors_usd, "docstatus": 1, "reference_name": si.name, "credit": 500},
fields=["parent"],
)[0]
self.assertEqual(
@@ -1085,10 +960,10 @@ class TestPaymentReconciliation(ERPNextTestSuite):
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer5
si.currency = "EUR"
si.customer = self.customer_usd
si.currency = "USD"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.debit_to = self.debtors_usd
si.save().submit()
# Make payment using Payment Entry
@@ -1096,8 +971,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
party=self.customer_usd,
paid_from=self.debtors_usd,
paid_to=self.bank,
paid_amount=100,
)
@@ -1111,8 +986,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
party=self.customer_usd,
paid_from=self.debtors_usd,
paid_to=self.bank,
paid_amount=200,
)
@@ -1123,8 +998,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pe2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer5
pr.receivable_payable_account = self.debtors_eur
pr.party = self.customer_usd
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
@@ -1152,14 +1027,14 @@ class TestPaymentReconciliation(ERPNextTestSuite):
"""
si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True)
si.cost_center = self.main_cc.name
si.cost_center = self.main_cc
si.submit()
pr = get_payment_entry(si.doctype, si.name)
pr.cost_center = self.sub_cc.name
pr.cost_center = self.sub_cc
pr = pr.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.cost_center = self.main_cc
pr.get_unreconciled_entries()
@@ -1176,38 +1051,38 @@ class TestPaymentReconciliation(ERPNextTestSuite):
# 'Main - PR' Cost Center
si1 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True)
si1.cost_center = self.main_cc.name
si1.cost_center = self.main_cc
si1.submit()
pe1 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe1.cost_center = self.main_cc.name
pe1.cost_center = self.main_cc
pe1 = pe1.save().submit()
je1 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
je1.accounts[0].cost_center = self.main_cc.name
je1.accounts[1].cost_center = self.main_cc.name
je1.accounts[0].cost_center = self.main_cc
je1.accounts[1].cost_center = self.main_cc
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer
je1 = je1.save().submit()
# 'Sub - PR' Cost Center
si2 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True)
si2.cost_center = self.sub_cc.name
si2.cost_center = self.sub_cc
si2.submit()
pe2 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe2.cost_center = self.sub_cc.name
pe2.cost_center = self.sub_cc
pe2 = pe2.save().submit()
je2 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
je2.accounts[0].cost_center = self.sub_cc.name
je2.accounts[1].cost_center = self.sub_cc.name
je2.accounts[0].cost_center = self.sub_cc
je2.accounts[1].cost_center = self.sub_cc
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer
je2 = je2.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.cost_center = self.main_cc
pr.get_unreconciled_entries()
@@ -1219,7 +1094,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
self.assertCountEqual(payment_vouchers, [pe1.name, je1.name])
# Change cost center
pr.cost_center = self.sub_cc.name
pr.cost_center = self.sub_cc
pr.get_unreconciled_entries()
@@ -1242,7 +1117,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer
si.currency = "EUR"
si.currency = "USD"
si.conversion_rate = 85
si.debit_to = self.debit_to
si.save().submit()
@@ -1624,7 +1499,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
"credit": 0.0,
},
{
"account": "Cash - _PR",
"account": "Cash - _TC",
"voucher_no": pe.name,
"against_voucher": None,
"debit": 0.0,
@@ -1782,10 +1657,10 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
amount = 200.0
je = self.create_journal_entry(self.debit_to, self.bank, amount)
je.accounts[0].cost_center = self.main_cc.name
je.accounts[0].cost_center = self.main_cc
je.accounts[0].party_type = "Customer"
je.accounts[0].party = self.customer
je.accounts[1].cost_center = self.main_cc.name
je.accounts[1].cost_center = self.main_cc
je = je.save().submit()
pe = self.create_payment_entry(amount=amount).save().submit()
@@ -1879,7 +1754,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
self.assertEqual(pl_entries, expected_ple)
def test_advance_payment_reconciliation_against_journal_for_supplier(self):
self.supplier = make_supplier("_Test Supplier")
self.supplier = "_Test Supplier"
frappe.db.set_value(
"Company",
self.company,
@@ -1891,10 +1766,10 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
amount = 200.0
je = self.create_journal_entry(self.creditors, self.bank, -amount)
je.accounts[0].cost_center = self.main_cc.name
je.accounts[0].cost_center = self.main_cc
je.accounts[0].party_type = "Supplier"
je.accounts[0].party = self.supplier
je.accounts[1].cost_center = self.main_cc.name
je.accounts[1].cost_center = self.main_cc
je = je.save().submit()
pe = self.create_payment_entry(amount=amount)
@@ -2062,13 +1937,13 @@ class TestPaymentReconciliation(ERPNextTestSuite):
},
{
"account": self.bank,
"cost_center": self.sub_cc.name,
"cost_center": self.sub_cc,
"credit_in_account_currency": 0,
"debit_in_account_currency": 500,
},
{
"account": self.cash,
"cost_center": self.sub_cc.name,
"cost_center": self.sub_cc,
"credit_in_account_currency": 0,
"debit_in_account_currency": 500,
},
@@ -2337,7 +2212,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_foreign_currency_reverse_payment_entry_against_payment_entry_for_customer(self):
transaction_date = nowdate()
customer = self.customer3
customer = self.customer_usd
amount = 1000
exchange_rate_at_payment = 100
exchange_rate_at_reverse_payment = 95
@@ -2345,8 +2220,8 @@ class TestPaymentReconciliation(ERPNextTestSuite):
# Receive amount from customer - 1,00,000
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=customer)
pe.payment_type = "Receive"
pe.paid_from = self.debtors_eur
pe.paid_from_account_currency = "EUR"
pe.paid_from = self.debtors_usd
pe.paid_from_account_currency = "USD"
pe.source_exchange_rate = exchange_rate_at_payment
pe.paid_amount = amount
pe.received_amount = exchange_rate_at_payment * amount
@@ -2364,14 +2239,14 @@ class TestPaymentReconciliation(ERPNextTestSuite):
reverse_pe.target_exchange_rate = exchange_rate_at_reverse_payment
reverse_pe.paid_amount = exchange_rate_at_reverse_payment * amount
reverse_pe.received_amount = amount
reverse_pe.paid_to = self.debtors_eur
reverse_pe.paid_to_account_currency = "EUR"
reverse_pe.paid_to = self.debtors_usd
reverse_pe.paid_to_account_currency = "USD"
reverse_pe.save().submit()
# Reconcile payments
pr = self.create_payment_reconciliation()
pr.party = customer
pr.receivable_payable_account = self.debtors_eur
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
invoices = [invoice.as_dict() for invoice in pr.invoices]
payments = [payment.as_dict() for payment in pr.payments]
@@ -2436,13 +2311,13 @@ class TestPaymentReconciliation(ERPNextTestSuite):
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_customer(self):
transaction_date = nowdate()
customer = self.customer3
customer = self.customer_usd
amount = 1000
exchange_rate_at_payment = 95
exchange_rate_at_reverse_payment = 100
# Receive amount from customer - 95,000
je1 = self.create_journal_entry(self.cash, self.debtors_eur, amount, transaction_date)
je1 = self.create_journal_entry(self.cash, self.debtors_usd, amount, transaction_date)
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].debit_in_account_currency = exchange_rate_at_payment * amount
@@ -2456,7 +2331,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
je1.submit()
# Pay amount to customer - 1,00,000
je2 = self.create_journal_entry(self.debtors_eur, self.cash, amount, transaction_date)
je2 = self.create_journal_entry(self.debtors_usd, self.cash, amount, transaction_date)
je2.multi_currency = 1
je2.accounts[0].party_type = "Customer"
je2.accounts[0].party = customer
@@ -2472,7 +2347,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
# Reconcile payments
pr = self.create_payment_reconciliation()
pr.party = customer
pr.receivable_payable_account = self.debtors_eur
pr.receivable_payable_account = self.debtors_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
@@ -2540,34 +2415,6 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pr.reconcile()
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
if currency:
customer.default_currency = currency
customer.save()
return customer.name
else:
return customer_name
def make_supplier(supplier_name, currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.type = "Individual"
if currency:
supplier.default_currency = currency
supplier.save()
return supplier.name
else:
return supplier_name
def create_fiscal_year(company, year_start_date, year_end_date):
fy_docname = frappe.db.exists(
"Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date}

View File

@@ -664,6 +664,7 @@
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"label": "Total Billing Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -1531,6 +1532,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1639,7 +1641,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-04-28 06:06:14.283612",
"modified": "2026-05-01 02:37:30.580568",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -115,7 +115,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount != 0 &&
!doc.on_hold &&
frappe.model.can_create("Payment Entry")
) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -130,7 +135,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount > 0 &&
!cint(doc.is_return) &&
!doc.on_hold &&
frappe.boot.user.in_create.includes("Payment Request")
) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -443,13 +454,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"discount_account",
"cost_center",
"project",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["expense_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
on_submit() {
@@ -558,12 +570,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
frappe.ui.form.on("Purchase Invoice", {
setup: function (frm) {
frm.custom_make_buttons = {

View File

@@ -110,6 +110,7 @@
"sales_invoice_item",
"material_request",
"material_request_item",
"delivered_by_supplier",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -1001,13 +1002,22 @@
"label": "Tax Withholding Category",
"options": "Tax Withholding Category",
"print_hide": 1
},
{
"default": "0",
"fieldname": "delivered_by_supplier",
"fieldtype": "Check",
"hidden": 1,
"label": "Delivered by Supplier",
"print_hide": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-04-07 15:40:45.687554",
"modified": "2026-05-06 08:08:40.782395",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -31,6 +31,7 @@ class PurchaseInvoiceItem(Document):
conversion_factor: DF.Float
cost_center: DF.Link | None
deferred_expense_account: DF.Link | None
delivered_by_supplier: DF.Check
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent

View File

@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -135,13 +135,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (doc.outstanding_amount > 0) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
},
__("Create")
);
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
},
__("Create")
);
}
this.frm.add_custom_button(
__("Invoice Discounting"),
this.make_invoice_discounting.bind(this),
@@ -552,12 +554,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"income_account",
"discount_account",
"cost_center",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["income_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
set_dynamic_labels() {

View File

@@ -1152,6 +1152,7 @@
"hide_seconds": 1,
"label": "Rounding Adjustment",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1164,6 +1165,7 @@
"label": "Rounded Total",
"oldfieldname": "rounded_total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -2365,7 +2367,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-04-28 13:08:19.849783",
"modified": "2026-05-01 02:37:29.742764",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -195,6 +195,9 @@ def reschedule_depreciation(asset_doc, notes, disposal_date=None):
for row in asset_doc.get("finance_books"):
current_schedule = get_asset_depr_schedule_doc(asset_doc.name, None, row.finance_book)
if disposal_date and flt(row.value_after_depreciation) <= flt(row.expected_value_after_useful_life):
continue
if current_schedule:
if current_schedule.docstatus == 1:
new_schedule = frappe.copy_doc(current_schedule)

View File

@@ -354,9 +354,9 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
}
if (is_drop_ship && doc.status != "Delivered") {
if (is_drop_ship && !["Completed", "Delivered"].includes(doc.status)) {
this.frm.add_custom_button(
__("Delivered"),
__("Deliver (Dropship)"),
this.delivered_by_supplier.bind(this),
__("Status")
);
@@ -374,7 +374,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
if (doc.status != "Closed") {
if (doc.status != "On Hold") {
if (flt(doc.per_received) < 100 && allow_receipt) {
if (
doc.items
.filter((item) => !item.delivered_by_supplier)
.some((item) => item.received_qty < item.qty) &&
allow_receipt
) {
this.frm.add_custom_button(
__("Purchase Receipt"),
() => {
@@ -416,7 +421,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
if (
frappe.model.can_create("Payment Entry") &&
flt(doc.per_billed) < 100 &&
doc.status != "Delivered"
) {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -424,7 +433,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed) < 100) {
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -660,12 +669,20 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (doc.schedule_date) {
row.schedule_date = doc.schedule_date;
refresh_field("schedule_date", cdn, "items");
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
field_copy.push("project");
}
if (doc.schedule_date) {
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
} else {
field_copy.push("schedule_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
}
@@ -718,7 +735,108 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
delivered_by_supplier() {
this.frm.cscript.update_status("Deliver", "Delivered");
const data = this.frm.doc.items
.filter((item) => item.delivered_by_supplier == 1)
.map((item) => {
return {
__checked: item.qty > item.received_qty,
name: item.name,
item_code: item.item_code,
item_name: item.item_name,
qty: item.qty,
uom: item.uom,
delivered_qty: item.received_qty || 0,
qty_change: item.qty - item.received_qty,
};
});
const dialog = new frappe.ui.Dialog({
title: __("Set Dropship Items Delivered Quantity"),
size: "extra-large",
fields: [
{
fieldname: "items",
fieldtype: "Table",
data: data,
cannot_add_rows: true,
cannot_delete_rows: true,
fields: [
{
fieldname: "name",
fieldtype: "Data",
read_only: true,
hidden: 1,
},
{
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
label: __("Item Code"),
in_list_view: 1,
read_only: true,
},
{
fieldname: "item_name",
fieldtype: "Data",
label: __("Item Name"),
in_list_view: 1,
read_only: true,
},
{
fieldname: "qty",
fieldtype: "Float",
label: __("Quantity"),
in_list_view: 1,
read_only: true,
},
{
fieldname: "uom",
fieldtype: "Data",
label: __("UOM"),
in_list_view: 1,
read_only: true,
},
{
fieldname: "delivered_qty",
fieldtype: "Float",
label: __("Delivered Qty"),
read_only: true,
in_list_view: 1,
},
{
fieldname: "qty_change",
fieldtype: "Float",
label: __("Qty Change"),
in_list_view: 1,
reqd: 1,
},
],
},
],
primary_action: (values) => {
const frm = this.frm;
frappe.call({
doc: frm.doc,
method: "update_dropship_received_qty",
args: {
data: values.items
.filter((item) => item.__checked)
.map((item) => ({
name: item.name,
current_qty: item.delivered_qty,
qty_change: item.qty_change,
})),
},
callback: function (r) {
if (!r.exc) {
frm.reload_doc();
frappe.toast(__("Quantities updated successfully."));
dialog.hide();
}
},
});
},
});
dialog.show();
}
items_on_form_rendered() {
@@ -740,12 +858,6 @@ cur_frm.cscript.update_status = function (label, status) {
});
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
if (cur_frm.doc.is_old_subcontracting_flow) {
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];

View File

@@ -218,7 +218,6 @@ class PurchaseOrder(BuyingController):
self.create_raw_materials_supplied()
self.validate_fg_item_for_subcontracting()
self.set_received_qty_for_drop_ship_items()
if not self.advance_payment_status:
self.advance_payment_status = "Not Initiated"
@@ -493,6 +492,8 @@ class PurchaseOrder(BuyingController):
if self.has_drop_ship_item():
self.update_delivered_qty_in_sales_order()
self.set_received_qty_to_zero_for_drop_ship_items()
self.update_receiving_percentage()
self.update_reserved_qty_for_subcontract()
self.check_on_hold_or_closed_status()
@@ -566,20 +567,72 @@ class PurchaseOrder(BuyingController):
so.set_status(update=True)
so.notify_update()
def set_received_qty_to_zero_for_drop_ship_items(self):
for item in self.items:
if item.delivered_by_supplier:
item.db_set("received_qty", 0)
def has_drop_ship_item(self):
return any(d.delivered_by_supplier for d in self.items)
@frappe.whitelist()
def update_dropship_received_qty(self, data: list[dict]):
if not data:
frappe.throw(_("Please select at least one item to update delivered quantity."))
for d in data:
item = next((item for item in self.items if item.name == d.get("name")), None)
if not item:
frappe.throw(
_("Item with name {0} not found in the Purchase Order").format(frappe.bold(d.get("name")))
)
if not item.delivered_by_supplier:
frappe.throw(
_(
"Item {0} is not a drop ship item. Only drop ship items can have Delivered Qty updated."
).format(frappe.bold(item.item_code))
)
if not item.has_permlevel_access_to("received_qty", permission_type="write"):
frappe.throw(
_("You don't have permission to update Received Qty DocField for item {0}").format(
frappe.bold(item.item_code)
)
)
if not d.get("qty_change"):
frappe.throw(
_(
"Item {0} has no changes in delivered quantity. Please unselect the row if you do not wish to update its quantity."
).format(frappe.bold(item.item_code))
)
if d.get("qty_change") < 0 and abs(d.get("qty_change")) > item.received_qty:
frappe.throw(
_("Delivered Qty cannot be reduced by more than {0} for item {1}").format(
item.received_qty, frappe.bold(item.item_code)
)
)
if d.get("qty_change") > 0 and item.received_qty + d.get("qty_change") > item.qty:
frappe.throw(
_("Delivered Qty cannot be increased by more than {0} for item {1}").format(
item.qty - item.received_qty, frappe.bold(item.item_code)
)
)
item.received_qty += d.get("qty_change")
self.update_receiving_percentage()
self.save()
def is_against_so(self):
return any(d.sales_order for d in self.items if d.sales_order)
def is_against_pp(self):
return any(d.production_plan for d in self.items if d.production_plan)
def set_received_qty_for_drop_ship_items(self):
for item in self.items:
if item.delivered_by_supplier == 1:
item.received_qty = item.qty
def update_reserved_qty_for_subcontract(self):
if self.is_old_subcontracting_flow:
for d in self.supplied_items:
@@ -592,7 +645,7 @@ class PurchaseOrder(BuyingController):
for item in self.items:
received_qty += min(item.received_qty, item.qty)
total_qty += item.qty
if total_qty:
if total_qty and received_qty:
self.db_set("per_received", flt(received_qty / total_qty) * 100, update_modified=False)
else:
self.db_set("per_received", 0, update_modified=False)

View File

@@ -622,11 +622,13 @@
"width": "100px"
},
{
"allow_on_submit": 1,
"depends_on": "received_qty",
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty",
"no_copy": 1,
"non_negative": 1,
"oldfieldname": "received_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
@@ -950,7 +952,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-11-30 16:51:57.761673",
"modified": "2026-05-08 20:40:10.683023",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",

View File

@@ -126,9 +126,3 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
// for backward compatibility: combine new and previous states
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};

View File

@@ -345,9 +345,9 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Supplier-Wise Sales Analytics",
"label": "Item Wise Consumption",
"link_count": 0,
"link_to": "Supplier-Wise Sales Analytics",
"link_to": "Item Wise Consumption",
"link_type": "Report",
"onboard": 1,
"type": "Link"

View File

@@ -71,6 +71,7 @@ from erpnext.stock.get_item_details import (
NOT_APPLICABLE_TAX,
ItemDetailsCtx,
_get_item_tax_template,
_get_item_tax_template_from_item_group,
get_conversion_factor,
get_item_details,
get_item_tax_map,
@@ -327,6 +328,7 @@ class AccountsController(TransactionBase):
# Determine if drop ship applies
is_drop_ship = self.doctype in {
"Purchase Order",
"Purchase Invoice",
"Sales Order",
"Sales Invoice",
} and self.is_drop_ship(self.items)
@@ -3672,7 +3674,12 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
}
)
child_item.item_tax_template = _get_item_tax_template(ctx, item.taxes)
item_tax_template = _get_item_tax_template(ctx, item.taxes)
if not item_tax_template:
item_tax_template = _get_item_tax_template_from_item_group(ctx, item.item_group)
child_item.item_tax_template = item_tax_template
child_item.item_tax_rate = get_item_tax_map(
doc=parent_doc,
tax_template=child_item.item_tax_template,
@@ -3901,8 +3908,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
)
qty_limits = {
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")),
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")),
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")),
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")),
}
if parent_doctype in qty_limits:

View File

@@ -461,17 +461,7 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
)
net_rate = (
flt(
(item.base_net_amount / item.received_qty) * item.qty,
item.precision("base_net_amount"),
)
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
net_rate = item.base_net_amount
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate

View File

@@ -169,10 +169,6 @@ class calculate_taxes_and_totals:
return
if not self.discount_amount_applied:
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
do_not_round_fields = ["valuation_rate", "incoming_rate"]
for item in self.doc.items:
@@ -236,13 +232,7 @@ class calculate_taxes_and_totals:
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
else:
qty = (
(item.qty + item.rejected_qty)
if bill_for_rejected_quantity_in_purchase_invoice
and self.doc.doctype == "Purchase Receipt"
else item.qty
)
item.amount = flt(item.rate * qty, item.precision("amount"))
item.amount = flt(item.rate * item.qty, item.precision("amount"))
item.net_amount = item.amount
@@ -402,16 +392,9 @@ class calculate_taxes_and_totals:
self.doc.total
) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
for item in self._items:
self.doc.total += item.amount
self.doc.total_qty += (
(item.qty + item.rejected_qty)
if bill_for_rejected_quantity_in_purchase_invoice and self.doc.doctype == "Purchase Receipt"
else item.qty
)
self.doc.total_qty += item.qty
self.doc.base_total += item.base_amount
self.doc.net_total += item.net_amount
self.doc.base_net_total += item.base_net_amount

View File

@@ -235,10 +235,13 @@ def _get_agents_sorted_by_asc_workload(date):
return agent_list
appointment_counter = Counter(agent_list)
for appointment in appointments:
assigned_to = frappe.parse_json(appointment._assign)
if not assigned_to:
assign_data = appointment._assign
if isinstance(assign_data, str):
assign_data = assign_data.strip()
if not assign_data:
continue
if (assigned_to[0] in agent_list) and getdate(appointment.scheduled_time) == date:
assigned_to = frappe.parse_json(assign_data)
if assigned_to and (assigned_to[0] in agent_list) and getdate(appointment.scheduled_time) == date:
appointment_counter[assigned_to[0]] += 1
sorted_agent_list = appointment_counter.most_common()
sorted_agent_list.reverse()

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ from frappe import _
def get_data():
return {
"fieldname": "demand_planning",
"fieldname": "sales_forecast",
"transactions": [
{
"label": _("MPS"),

View File

@@ -305,17 +305,6 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "BOM",
"hidden": 0,
"is_query_report": 1,
"label": "BOM Stock Report",
"link_count": 0,
"link_to": "BOM Stock Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Work Order",
"hidden": 0,
@@ -443,7 +432,7 @@
"type": "Link"
}
],
"modified": "2026-01-02 15:07:36.968043",
"modified": "2026-05-05 11:00:26.131777",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",

View File

@@ -479,4 +479,5 @@ erpnext.patches.v16_0.merge_repost_settings_to_accounts_settings
erpnext.patches.v16_0.set_root_type_in_account_categories
erpnext.patches.v16_0.scr_inv_dimension
erpnext.patches.v16_0.packed_item_inv_dimen
erpnext.patches.v16_0.correct_po_titles
erpnext.patches.v16_0.fix_titles
erpnext.patches.v16_0.set_not_applicable_on_german_item_tax_templates

View File

@@ -1,15 +0,0 @@
import frappe
def execute():
"""
This patch corrects the titles of purchase orders that were set to
the text string "{supplier_name}" instead of the actual supplier name.
"""
purchase_order = frappe.qb.DocType("Purchase Order")
(
frappe.qb.update(purchase_order)
.set(purchase_order.title, purchase_order.supplier_name)
.where(purchase_order.title == "{supplier_name}")
).run()

View File

@@ -0,0 +1,28 @@
import frappe
def execute():
"""
This patch corrects the titles of doctypes set to
the text strings "{customer_name}" or "{supplier_name}"
instead of the actual customer or supplier name.
"""
customer_doctypes = ["POS Invoice", "Sales Invoice", "Quotation", "Sales Order", "Delivery Note"]
supplier_doctypes = ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]
for doctype in customer_doctypes:
customer_doctype = frappe.qb.DocType(doctype)
(
frappe.qb.update(customer_doctype)
.set(customer_doctype.title, customer_doctype.customer_name)
.where(customer_doctype.title == "{customer_name}")
).run()
for doctype in supplier_doctypes:
supplier_doctype = frappe.qb.DocType(doctype)
(
frappe.qb.update(supplier_doctype)
.set(supplier_doctype.title, supplier_doctype.supplier_name)
.where(supplier_doctype.title == "{supplier_name}")
).run()

View File

@@ -0,0 +1,218 @@
import frappe
# Snapshot of the relevant German defaults when this migration was written.
# Migration patches must not read mutable setup data, otherwise future edits to
# country_wise_tax.json would change what this patch does on sites that have not
# run it yet.
#
# For numbered charts, compare account_number + root_type because Account.account_name
# is not unique within a company.
SKR04_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS = frozenset(
{
("3801", "Liability"),
("3802", "Liability"),
("3835", "Liability"),
("1401", "Asset"),
("1402", "Asset"),
("1541", "Asset"),
}
)
SKR04_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS = frozenset(
{
("3806", "Liability"),
("3804", "Liability"),
("3837", "Liability"),
("1406", "Asset"),
("1404", "Asset"),
("1540", "Asset"),
}
)
SKR03_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS = frozenset(
{
("1771", "Liability"),
("1772", "Liability"),
("1785", "Liability"),
("1571", "Asset"),
("1572", "Asset"),
("1541", "Asset"),
}
)
SKR03_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS = frozenset(
{
("1776", "Liability"),
("1774", "Liability"),
("1787", "Liability"),
("1576", "Asset"),
("1574", "Asset"),
("1540", "Asset"),
}
)
STANDARD_NOT_APPLICABLE_7_PERCENT_ACCOUNT_LABELS = frozenset(
{
("Umsatzsteuer 7 %", "Liability"),
("Umsatzsteuer aus innergemeinschaftlichem Erwerb", "Liability"),
("Umsatzsteuer nach § 13b UStG", "Liability"),
("Abziehbare Vorsteuer 7 %", "Asset"),
("Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb", "Asset"),
("Abziehbare Vorsteuer nach § 13b UStG", "Asset"),
}
)
STANDARD_NOT_APPLICABLE_19_PERCENT_ACCOUNT_LABELS = frozenset(
{
("Umsatzsteuer 19 %", "Liability"),
("Umsatzsteuer aus innergemeinschaftlichem Erwerb 19 %", "Liability"),
("Umsatzsteuer nach § 13b UStG 19 %", "Liability"),
("Abziehbare Vorsteuer 19 %", "Asset"),
("Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19 %", "Asset"),
("Abziehbare Vorsteuer nach § 13b UStG 19 %", "Asset"),
}
)
STANDARD_WITH_NUMBERS_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS = frozenset(
{
("2321", "Liability"),
("2331", "Liability"),
("2341", "Liability"),
("1521", "Asset"),
("1531", "Asset"),
("1541", "Asset"),
}
)
STANDARD_WITH_NUMBERS_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS = frozenset(
{
("2320", "Liability"),
("2330", "Liability"),
("2340", "Liability"),
("1520", "Asset"),
("1530", "Asset"),
("1540", "Asset"),
}
)
GERMAN_ITEM_TAX_TEMPLATE_NOT_APPLICABLE_ACCOUNTS = {
"SKR03 mit Kontonummern": {
"identifier_field": "account_number",
"templates": {
"19 %": SKR03_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS,
"7 %": SKR03_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS,
"0 %": SKR03_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS
| SKR03_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS
| frozenset({("1588", "Asset")}),
},
},
"SKR04 mit Kontonummern": {
"identifier_field": "account_number",
"templates": {
"19 %": SKR04_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS,
"7 %": SKR04_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS,
"0 %": SKR04_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS
| SKR04_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS
| frozenset({("1433", "Asset")}),
},
},
"Standard": {
"identifier_field": "account_name",
"templates": {
"19 %": STANDARD_NOT_APPLICABLE_7_PERCENT_ACCOUNT_LABELS,
"7 %": STANDARD_NOT_APPLICABLE_19_PERCENT_ACCOUNT_LABELS,
"0%": STANDARD_NOT_APPLICABLE_7_PERCENT_ACCOUNT_LABELS
| STANDARD_NOT_APPLICABLE_19_PERCENT_ACCOUNT_LABELS
| frozenset({("Entstandene Einfuhrumsatzsteuer", "Asset")}),
},
},
"Standard with Numbers": {
"identifier_field": "account_number",
"templates": {
"19%": STANDARD_WITH_NUMBERS_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS,
"7%": STANDARD_WITH_NUMBERS_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS,
"0 %": STANDARD_WITH_NUMBERS_NOT_APPLICABLE_7_PERCENT_ACCOUNT_IDS
| STANDARD_WITH_NUMBERS_NOT_APPLICABLE_19_PERCENT_ACCOUNT_IDS
| frozenset({("1550", "Asset")}),
},
},
}
def update_account_cache(accounts, account_cache):
missing_accounts = set(accounts) - set(account_cache)
if not missing_accounts:
return
for account in frappe.get_all(
"Account",
filters={"name": ("in", tuple(sorted(missing_accounts)))},
fields=["name", "account_name", "account_number", "root_type"],
):
account_cache[account.name] = account
def get_account_identifier(account, identifier_field, account_cache):
cached_account = account_cache.get(account)
if not cached_account:
return None
return cached_account.get(identifier_field), cached_account.root_type
def execute():
"""Backfill `not_applicable` on Item Tax Template Details for German companies.
Before the `not_applicable` flag existed, German default templates used
`tax_rate: 0` to mean "this tax does not apply to the item" (as opposed to
an explicit 0% rate). For each German company, this patch looks up the
historical defaults for its Chart of Accounts and sets
`not_applicable = 1` on detail rows that still match those defaults
(same template title, same zero-rate tax account identifier set, flag still unset),
leaving any user-customised rows untouched.
"""
companies = frappe.get_all(
"Company",
filters={"country": "Germany"},
fields=["name", "chart_of_accounts"],
)
account_cache = {}
for company in companies:
chart = GERMAN_ITEM_TAX_TEMPLATE_NOT_APPLICABLE_ACCOUNTS.get(company.chart_of_accounts)
if not chart:
continue
identifier_field = chart["identifier_field"]
for template_title, target_accounts in chart["templates"].items():
itt_names = frappe.get_all(
"Item Tax Template",
filters={"company": company.name, "title": template_title},
pluck="name",
)
for itt_name in itt_names:
zero_rate_details = frappe.get_all(
"Item Tax Template Detail",
filters={"parent": itt_name, "tax_rate": 0},
fields=["name", "tax_type", "not_applicable"],
)
update_account_cache((d.tax_type for d in zero_rate_details), account_cache)
zero_rate_accounts_by_detail = {
d.name: get_account_identifier(d.tax_type, identifier_field, account_cache)
for d in zero_rate_details
}
if any(identifier is None for identifier in zero_rate_accounts_by_detail.values()):
continue
if set(zero_rate_accounts_by_detail.values()) != target_accounts:
continue
for d in zero_rate_details:
if not d.not_applicable:
frappe.db.set_value(
"Item Tax Template Detail",
d.name,
"not_applicable",
1,
update_modified=False,
)

View File

@@ -364,13 +364,18 @@ class Project(Document):
)
for user in self.users:
# process only users who haven't received the welcome email yet
if user.welcome_email_sent == 0:
frappe.sendmail(
user.user,
subject=_("Project Collaboration Invitation"),
content=content,
)
user.welcome_email_sent = 1
# fetch canonical User data (enabled status + latest email)
user_info = frappe.db.get_value("User", user.user, ["enabled", "email"], as_dict=True)
# send email only if user is enabled and has a valid email
if user_info and user_info.enabled and user_info.email:
frappe.sendmail(
recipients=[user_info.email],
subject=_("Project Collaboration Invitation"),
content=content,
)
user.welcome_email_sent = 1
def get_timeline_data(doctype: str, name: str) -> dict[int, int]:

View File

@@ -305,7 +305,7 @@
"label": "Additional Info"
},
{
"depends_on": "eval:doc.status == \"Closed\" || doc.status == \"Pending Review\"",
"depends_on": "eval:doc.status == \"Completed\" || doc.status == \"Pending Review\"",
"fieldname": "review_date",
"fieldtype": "Date",
"label": "Review Date",
@@ -313,7 +313,7 @@
"oldfieldtype": "Date"
},
{
"depends_on": "eval:doc.status == \"Closed\"",
"depends_on": "eval:doc.status == \"Completed\"",
"fieldname": "closing_date",
"fieldtype": "Date",
"label": "Closing Date",

View File

@@ -25,14 +25,16 @@ erpnext.buying = {
};
});
this.frm.set_query("project", function (doc) {
return {
filters: {
company: doc.company,
},
};
const get_project_filters = () => ({
query: "erpnext.controllers.queries.get_project_name",
filters: {
company: this.frm.doc.company,
},
});
this.frm.set_query("project", get_project_filters);
this.frm.set_query("project", "items", get_project_filters);
if (
this.frm.doc.__islocal &&
frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")
@@ -176,9 +178,14 @@ erpnext.buying = {
callback: (r) => {
if (!r.message) return;
this.frm.set_value("billing_address", r.message.primary_address || "");
if (!this.frm.doc.billing_address) {
this.frm.set_value("billing_address", r.message.primary_address || "");
}
if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) {
if (
frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") &&
!this.frm.doc.shipping_address
) {
this.frm.set_value("shipping_address", r.message.shipping_address || "");
}
},

View File

@@ -870,10 +870,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
me.apply_rule_on_other_items({ key: item });
}
},
() => {
var company_currency = me.get_company_currency();
me.update_item_grid_labels(company_currency);
},
]);
}
},
@@ -1296,7 +1292,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) &&
!this.frm.doc.shipping_address
) {
let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
const is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
if (!is_drop_ship) {
erpnext.utils.get_shipping_address(this.frm, function () {
@@ -1824,63 +1820,51 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (
this._last_currency === this.frm.doc.currency &&
this._last_price_list_currency === this.frm.doc.price_list_currency
this._last_price_list_currency === this.frm.doc.price_list_currency &&
this._last_party_account_currency === this.frm.doc.party_account_currency &&
this._last_company_currency === company_currency
) {
return;
}
this._last_currency = this.frm.doc.currency;
this._last_price_list_currency = this.frm.doc.price_list_currency;
this._last_party_account_currency = this.frm.doc.party_account_currency;
this._last_company_currency = company_currency;
this.change_form_labels(company_currency);
this.change_grid_labels(company_currency);
this.frm.refresh_fields();
}
get_currency_label_options(company_currency) {
return {
currency: this.frm.doc.currency,
"Company:company:default_currency": company_currency,
party_account_currency: this.frm.doc.party_account_currency,
};
}
set_currency_labels_from_options(currency_options, parentfield) {
const doctype = parentfield ? this.frm.fields_dict[parentfield].grid.doctype : this.frm.doc.doctype;
const docfields = frappe.meta.get_docfields(doctype);
Object.entries(currency_options).forEach(([options, currency]) => {
const fields = docfields
.filter((df) => df.fieldtype === "Currency" && df.options === options)
.map((df) => df.fieldname);
this.frm.set_currency_labels(fields, currency, parentfield);
});
}
change_form_labels(company_currency) {
let me = this;
const currency_options = this.get_currency_label_options(company_currency);
this.frm.set_currency_labels(
[
"advance_paid",
"base_total",
"base_net_total",
"base_total_taxes_and_charges",
"base_discount_amount",
"base_taxes_and_charges_added",
"base_taxes_and_charges_deducted",
"total_amount_to_pay",
"base_paid_amount",
"base_write_off_amount",
"base_change_amount",
"base_operating_cost",
"base_raw_material_cost",
"base_total_cost",
"base_secondary_items_cost",
"base_totals_section",
],
company_currency
);
this.frm.set_currency_labels(
[
"total",
"net_total",
"total_taxes_and_charges",
"discount_amount",
"taxes_and_charges_added",
"taxes_and_charges_deducted",
"tax_withholding_net_total",
"paid_amount",
"write_off_amount",
"operating_cost",
"secondary_items_cost",
"raw_material_cost",
"total_cost",
"totals_section",
],
this.frm.doc.currency
);
this.set_currency_labels_from_options(currency_options);
this.frm.set_currency_labels(["totals_section"], this.frm.doc.currency);
this.frm.set_currency_labels(["base_totals_section"], company_currency);
this.frm.set_currency_labels(
["outstanding_amount", "total_advance"],
@@ -1961,23 +1945,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
change_grid_labels(company_currency) {
var me = this;
this.update_item_grid_labels(company_currency);
const currency_options = this.get_currency_label_options(company_currency);
this.toggle_item_grid_columns(company_currency);
if (this.frm.doc.operations && this.frm.doc.operations.length > 0) {
this.frm.set_currency_labels(
["operating_cost", "hour_rate"],
this.frm.doc.currency,
"operations"
);
this.frm.set_currency_labels(
["base_operating_cost", "base_hour_rate"],
company_currency,
"operations"
);
for (const child_table of [
"items",
"operations",
"secondary_items",
"taxes",
"advances",
"payment_schedule",
"sales_team",
]) {
if (this.frm.fields_dict[child_table]) {
this.set_currency_labels_from_options(currency_options, child_table);
}
}
if (this.frm.doc.operations && this.frm.doc.operations.length > 0) {
var item_grid = this.frm.fields_dict["operations"].grid;
$.each(["base_operating_cost", "base_hour_rate"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
@@ -1986,9 +1972,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
if (this.frm.doc.secondary_items && this.frm.doc.secondary_items.length > 0) {
this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "secondary_items");
this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "secondary_items");
var item_grid = this.frm.fields_dict["secondary_items"].grid;
$.each(["base_rate", "base_amount"], function (i, fname) {
if (frappe.meta.get_docfield(item_grid.doctype, fname))
@@ -1996,74 +1979,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
});
}
if (this.frm.doc.taxes && this.frm.doc.taxes.length > 0) {
this.frm.set_currency_labels(
["tax_amount", "total", "tax_amount_after_discount"],
this.frm.doc.currency,
"taxes"
);
this.frm.set_currency_labels(
["base_tax_amount", "base_total", "base_tax_amount_after_discount"],
company_currency,
"taxes"
);
}
if (this.frm.doc.advances && this.frm.doc.advances.length > 0) {
this.frm.set_currency_labels(
["advance_amount", "allocated_amount"],
this.frm.doc.party_account_currency,
"advances"
);
}
this.update_payment_schedule_grid_labels(company_currency);
}
update_item_grid_labels(company_currency) {
this.frm.set_currency_labels(
[
"base_rate",
"base_net_rate",
"base_price_list_rate",
"base_amount",
"base_net_amount",
"base_rate_with_margin",
],
company_currency,
"items"
);
this.frm.set_currency_labels(
[
"rate",
"net_rate",
"price_list_rate",
"amount",
"net_amount",
"stock_uom_rate",
"rate_with_margin",
],
this.frm.doc.currency,
"items"
);
}
update_payment_schedule_grid_labels(company_currency) {
const me = this;
if (this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length > 0) {
this.frm.set_currency_labels(
["base_payment_amount", "base_outstanding", "base_paid_amount"],
company_currency,
"payment_schedule"
);
this.frm.set_currency_labels(
["payment_amount", "outstanding", "paid_amount"],
this.frm.doc.currency,
"payment_schedule"
);
var schedule_grid = this.frm.fields_dict["payment_schedule"].grid;
$.each(["base_payment_amount", "base_outstanding", "base_paid_amount"], function (i, fname) {
if (frappe.meta.get_docfield(schedule_grid.doctype, fname))

View File

@@ -37,5 +37,6 @@ import "./utils/demo.js";
import "./financial_statements.js";
import "./sales_trends_filters.js";
import "./purchase_trends_filters.js";
import "./utils/naming_series_dialog.js";
// import { sum } from 'frappe/public/utils/util.js'

View File

@@ -106,15 +106,19 @@ $.extend(erpnext.queries, {
});
}
let filters = { link_doctype: "Company", link_name: doc.company || "" };
const is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
if (is_drop_ship) filters = {};
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: { link_doctype: "Company", link_name: doc.company },
filters: filters,
};
},
dispatch_address_query: function (doc) {
var filters = { link_doctype: "Company", link_name: doc.company || "" };
var is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
let filters = { link_doctype: "Company", link_name: doc.company || "" };
const is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
if (is_drop_ship) filters = {};
return {
query: "frappe.contacts.doctype.address.address.address_query",

View File

@@ -0,0 +1,312 @@
frappe.provide("erpnext");
erpnext.NamingSeriesDialog = class NamingSeriesDialog {
constructor(opts = {}) {
this.opts = Object.assign(
{
title: __("Document Naming"),
single_doctype: "Document Naming Settings",
},
opts
);
this.current_doctype = null;
this.loaded = false;
this.make_dialog();
}
make_dialog() {
this.dialog = new frappe.ui.Dialog({
title: this.opts.title,
size: "medium",
fields: [
{
fieldtype: "Table",
fieldname: "naming_series_options",
label: __("Add Series Prefix"),
reqd: 1,
in_place_edit: true,
data: [],
fields: [
{
fieldtype: "Data",
fieldname: "series",
label: __("Series"),
in_list_view: 1,
change: async function () {
const preview = await this.grid_row.grid._naming_dialog.get_series_preview(
this.doc.series
);
this.doc.preview = preview;
this.grid_row.refresh_field("preview");
},
},
{
fieldtype: "Data",
fieldname: "preview",
label: __("Preview"),
in_list_view: 1,
placeholder: " ",
read_only: 1,
},
],
},
{ fieldtype: "Section Break", label: __("Rules for configuring series"), collapsible: 1 },
{
fieldtype: "HTML",
fieldname: "naming_series_description",
},
],
primary_action_label: __("Update"),
primary_action: () => this.save(),
});
this.dialog.fields_dict.naming_series_options.grid._naming_dialog = this;
}
async show() {
this.dialog.show();
this.render_help();
if (this.opts.doctype && !this.loaded) {
await this.get_transaction(this.opts.doctype);
this.loaded = true;
return;
}
}
render_help() {
this.dialog.get_field("naming_series_description").$wrapper.html(`
<ul>
<li>${__("Allowed special characters are '/' and '-'")}</li>
<li>
${__(
"Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, '.####' means that the series will have four digits. Default is five digits."
)}
</li>
<li> ${__("You can also use variables in the series name by putting them between (.) dots")}
<br>
${__("Supported Variables:")}
<ul>
<li><code>.YYYY.</code> - ${__("Year in 4 digits")}</li>
<li><code>.YY.</code> - ${__("Year in 2 digits")}</li>
<li><code>.MM.</code> - ${__("Month")}</li>
<li><code>.DD.</code> - ${__("Day of month")}</li>
<li><code>.WW.</code> - ${__("Week of the year")}</li>
<li>
<code>.{fieldname}.</code> - ${__("fieldname on the document e.g.")}
<code>branch</code>
</li>
<li><code>.FY.</code> - ${__("Fiscal Year (requires ERPNext to be installed)")}</li>
<li><code>.ABBR.</code> - ${__("Company Abbreviation (requires ERPNext to be installed)")}</li>
</ul>
</li>
</ul>
Examples:
<ul>
<li>INV-</li>
<li>INV-10-</li>
<li>INVK-</li>
<li>INV-.YYYY.-._{branch}.-.MM.-.####</li>
</ul>
<br>`);
}
get_series_preview(series) {
if (!series) return "";
return this.get_document_naming_doc().then((doc) => {
doc.try_naming_series = series;
doc.transaction_type = this.current_doctype;
return frappe
.call({
doc: doc,
method: "preview_series",
freeze: true,
})
.then((r) => (r.message || "").split("\n")[0] || "");
});
}
get_document_naming_doc() {
const dt = this.opts.single_doctype;
return frappe.model.with_doc(dt, dt).then(() => {
return frappe.model.get_doc(dt, dt);
});
}
async get_transaction(doctype) {
this.current_doctype = doctype;
await frappe.model.with_doctype(doctype, async () => {
const meta = frappe.get_meta(doctype);
const naming_df = (meta?.fields || []).find((df) => df.fieldname === "naming_series");
const series_list = (naming_df?.options || "").split("\n").filter(Boolean);
const rows = await Promise.all(
series_list.map(async (series) => ({
series: series,
preview: await this.get_series_preview(series),
}))
);
this.dialog.fields_dict.naming_series_options.df.data = rows;
this.dialog.fields_dict.naming_series_options.grid.refresh();
});
}
save() {
const rows = this.dialog.fields_dict.naming_series_options.grid.get_data();
const naming_series_options = rows
.map((r) => (r.series || "").trim())
.filter(Boolean)
.join("\n");
if (!this.current_doctype) {
frappe.msgprint(__("Please select a transaction."));
return;
}
if (!naming_series_options) {
frappe.msgprint(__("Please add at least one naming series."));
return;
}
this.get_document_naming_doc().then((doc) => {
doc.transaction_type = this.current_doctype;
doc.naming_series_options = naming_series_options;
frappe.call({
doc: doc,
method: "update_series",
freeze: true,
callback: async () => {
const updated_rows = await Promise.all(
naming_series_options
.split("\n")
.filter(Boolean)
.map(async (series) => ({
series: series,
preview: await this.get_series_preview(series),
}))
);
this.dialog.fields_dict.naming_series_options.df.data = updated_rows;
this.dialog.fields_dict.naming_series_options.grid.refresh();
frappe.show_alert({ message: __("Naming Series updated"), indicator: "green" });
this.dialog.hide();
this.opts.on_update?.({ doctype: this.current_doctype, naming_series_options });
},
});
});
}
};
erpnext.NamingSeriesTable = class NamingSeriesTable {
constructor(opts = {}) {
this.frm = opts.frm;
this.transactions = opts.transactions || [];
this.$wrapper = opts.frm.get_field(opts.fieldname).$wrapper;
}
render() {
this.$wrapper.html(`
<div class="form-grid" style="margin-bottom: 24px;">
<table class="table" style="margin: 0;">
<thead class="grid-heading-row" style="background-color: var(--subtle-fg);">
<tr>
<td style="width: 25%; padding: 8px 12px; text-align: left;">
${__("Transaction")}
</td>
<td colspan="2"
style="width: 75%; padding: 8px 12px; text-align: left; border-left: 1px solid var(--border-color);">
${__("Current Series")}
</td>
</tr>
</thead>
<tbody class="naming-series-table-rows"></tbody>
</table>
</div>
`);
const $rows = this.$wrapper.find(".naming-series-table-rows");
this.map_configure_button($rows);
this.get_row_data($rows);
}
map_configure_button($rows) {
$rows.on("click", ".configure-btn", (e) => {
const $btn = $(e.currentTarget);
const doctype = $btn.data("doctype");
const label = $btn.data("label");
if (!this.frm._naming_dialogs) this.frm._naming_dialogs = {};
if (!this.frm._naming_dialogs[doctype]) {
this.frm._naming_dialogs[doctype] = new erpnext.NamingSeriesDialog({
doctype: doctype,
title: __("{0} Naming Series", [__(label)]),
on_update: ({ naming_series_options }) => {
const series = naming_series_options.split("\n").filter(Boolean);
this.$wrapper
.find(`.series-cell-${frappe.scrub(doctype)}`)
.html(this.series_list_background(series));
},
});
}
this.frm._naming_dialogs[doctype].show();
});
}
get_row_data($rows) {
this.transactions.forEach((t) => {
frappe.model.with_doctype(t.doctype, () => {
const meta = frappe.get_meta(t.doctype);
const naming_df = (meta?.fields || []).find((df) => df.fieldname === "naming_series");
const series = (naming_df?.options || "")
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
$rows.append(this.make_row(t, series));
});
});
}
make_row(t, series) {
return $(`
<tr>
<td style="width: 25%; padding: 8px 12px; vertical-align: top; background-color: var(--card-bg);">
${frappe.utils.escape_html(t.label)}
</td>
<td class="series-cell-${frappe.scrub(t.doctype)}"
style="width: 70%; padding: 8px 12px; border-left: 1px solid var(--border-color); white-space: normal; vertical-align: top; background-color: var(--card-bg);">
${this.series_list_background(series)}
</td>
<td class="text-center"
style="width: 5%; padding: 8px 12px; border-left: 1px solid var(--border-color); vertical-align: middle; background-color: var(--card-bg);">
<a class="btn-link configure-btn"
data-doctype="${frappe.utils.escape_html(t.doctype)}"
data-label="${frappe.utils.escape_html(t.label)}"
style="cursor: pointer; color: var(--text-muted);">
${frappe.utils.icon("edit", "sm")}
</a>
</td>
</tr>
`);
}
series_list_background(series_list) {
if (!series_list.length) {
return `<span class="text-muted">${__("Not configured")}</span>`;
}
return series_list
.map(
(s) => `<span class="badge badge-light"
style="margin: 2px; font-family: monospace; font-weight: normal;">
${frappe.utils.escape_html(s)}
</span>`
)
.join("");
}
};

View File

@@ -565,7 +565,9 @@ def _make_customer(source_name, ignore_permissions=False):
if quotation.quotation_to == "Customer":
return frappe.get_doc("Customer", quotation.party_name)
elif quotation.quotation_to == "CRM Deal":
return frappe.get_doc("Customer", {"crm_deal": quotation.party_name})
customer_name = frappe.get_value("Customer", {"crm_deal": quotation.party_name})
if customer_name:
return frappe.get_doc("Customer", customer_name)
# Check if a Customer already exists for the Lead or Prospect.
existing_customer = None

View File

@@ -183,6 +183,61 @@ class TestQuotation(ERPNextTestSuite):
self.assertTrue(quotation.payment_schedule)
def test_terms_attachments_are_copied_to_quotation(self):
terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
first_attachment = make_file_attachment(
"Terms and Conditions",
terms.name,
content="First terms attachment",
)
quotation = make_quotation(do_not_save=1)
quotation.tc_name = terms.name
quotation.insert()
self.assertEqual(get_attachment_urls("Quotation", quotation.name), {first_attachment.file_url})
second_attachment = make_file_attachment(
"Terms and Conditions",
terms.name,
content="Second terms attachment",
)
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
quotation.save()
quotation_attachments = get_attachment_urls("Quotation", quotation.name)
self.assertEqual(quotation_attachments, {first_attachment.file_url})
self.assertNotIn(second_attachment.file_url, quotation_attachments)
new_terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
new_terms_attachment = make_file_attachment(
"Terms and Conditions",
new_terms.name,
content="Attachment from updated terms",
)
quotation.tc_name = new_terms.name
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
quotation.save()
self.assertEqual(
get_attachment_urls("Quotation", quotation.name),
{first_attachment.file_url, new_terms_attachment.file_url},
)
def test_terms_attachments_are_not_copied_when_disabled(self):
terms = make_terms_and_conditions(copy_attachments_to_transaction=False)
make_file_attachment(
"Terms and Conditions",
terms.name,
content="Terms attachment should stay on the template",
)
quotation = make_quotation(do_not_save=1)
quotation.tc_name = terms.name
quotation.insert()
self.assertFalse(get_attachment_urls("Quotation", quotation.name))
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"automatically_fetch_payment_terms": 1},
@@ -1148,6 +1203,42 @@ def get_quotation_dict(party_name=None, item_code=None):
}
def make_terms_and_conditions(copy_attachments_to_transaction=False):
return frappe.get_doc(
{
"doctype": "Terms and Conditions",
"title": f"_Test Terms and Conditions {frappe.generate_hash(length=8)}",
"selling": 1,
"terms": "Test terms",
"copy_attachments_to_transaction": 1 if copy_attachments_to_transaction else 0,
}
).insert()
def make_file_attachment(doctype, docname, content):
return frappe.get_doc(
{
"doctype": "File",
"file_name": f"terms-attachment-{frappe.generate_hash(length=8)}.txt",
"attached_to_doctype": doctype,
"attached_to_name": docname,
"content": content,
}
).insert()
def get_attachment_urls(doctype, docname):
return {
file.file_url
for file in frappe.get_all(
"File",
filters={"attached_to_doctype": doctype, "attached_to_name": docname},
fields=["file_url"],
)
if file.file_url
}
def make_quotation(**args):
qo = frappe.new_doc("Quotation")
args = frappe._dict(args)

View File

@@ -1162,11 +1162,13 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// payment request
if (flt(doc.per_billed) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
this.frm.add_custom_button(
__("Payment Request"),
() => this.make_payment_request_with_schedule(),
__("Create")
);
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
() => this.make_payment_request_with_schedule(),
__("Create")
);
}
if (frappe.model.can_create("Payment Entry")) {
this.frm.add_custom_button(
@@ -1218,6 +1220,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
this.order_type(doc);
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
if (doc.delivery_date) {
frappe.model.set_value(cdt, cdn, "delivery_date", doc.delivery_date);
} else {
field_copy.push("delivery_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
}
create_pick_list() {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",

View File

@@ -847,6 +847,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Loyalty Amount",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1480,6 +1481,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1763,7 +1765,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-04-28 06:30:35.902868",
"modified": "2026-05-01 02:37:30.937916",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -610,6 +610,7 @@ class SalesOrder(SellingController):
self.update_subcontracting_order_status()
self.notify_update()
clear_doctype_notifications(self)
self.update_blanket_order()
def update_subcontracting_order_status(self):
from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import (
@@ -1627,7 +1628,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
if default_payment_terms:
target.payment_terms_template = default_payment_terms
if any(item.delivered_by_supplier == 1 for item in source.items):
if any(item.delivered_by_supplier for item in target.items):
if source.shipping_address_name:
target.shipping_address = source.shipping_address_name
target.shipping_address_display = source.shipping_address

View File

@@ -1619,7 +1619,7 @@ class TestSalesOrder(ERPNextTestSuite):
make_item( # template item
"Test-WO-Tshirt",
{
"has_variant": 1,
"has_variants": 1,
"variant_based_on": "Item Attribute",
"attributes": [{"attribute": "Test Colour"}],
},

View File

@@ -2,7 +2,80 @@
// For license information, please see license.txt
frappe.ui.form.on("Selling Settings", {
refresh(frm) {
const display = frm.doc.cust_master_name === "Naming Series";
frm.set_df_property("naming_series_details", "hidden", !display);
frm.set_df_property("configure", "hidden", !display);
if (display) {
find_naming_series("Customer", "naming_series_details", frm);
}
load_default_naming_series(frm);
},
cust_master_name(frm) {
const display = frm.doc.cust_master_name === "Naming Series";
frm.set_df_property("naming_series_details", "hidden", !display);
frm.set_df_property("configure", "hidden", !display);
if (display) {
find_naming_series("Customer", "naming_series_details", frm);
} else {
frm.set_value("naming_series_details", "");
}
},
configure(frm) {
show_naming_series_dialog("Customer", frm);
},
after_save(frm) {
frappe.boot.user.defaults.editable_price_list_rate = frm.doc.editable_price_list_rate;
},
});
function show_naming_series_dialog(doctype, frm) {
if (!frm._naming_series_dialog) {
frm._naming_series_dialog = new erpnext.NamingSeriesDialog({
doctype: doctype,
title: __("Naming Series for {0}", [__(doctype)]),
on_update: ({ naming_series_options }) => {
frm.set_value("naming_series_details", naming_series_options);
},
});
}
frm._naming_series_dialog.show();
}
function find_naming_series(doctype, field, frm) {
frappe.model.with_doctype(doctype, () => {
const meta = frappe.get_meta(doctype);
const naming_df = (meta?.fields || []).find((df) => df.fieldname === "naming_series");
const options = naming_df?.options || "";
const series_list = options
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
frm.doc[field] = series_list.length ? series_list.join("\n") : __("No naming series defined");
frm.refresh_field(field);
});
}
function load_default_naming_series(frm) {
let transactions = [
{ label: __("Customer"), doctype: "Customer" },
{ label: __("Quotation"), doctype: "Quotation" },
{ label: __("Sales Order"), doctype: "Sales Order" },
{ label: __("Sales Invoice"), doctype: "Sales Invoice" },
{ label: __("Delivery Note"), doctype: "Delivery Note" },
{ label: __("Payment Entry"), doctype: "Payment Entry" },
{ label: __("POS Invoice"), doctype: "POS Invoice" },
];
if (frm.doc.cust_master_name !== "Naming Series") {
transactions = transactions.filter((t) => t.doctype !== "Customer");
}
new erpnext.NamingSeriesTable({
frm: frm,
fieldname: "transaction_naming_html",
transactions: transactions,
}).render();
}

View File

@@ -9,8 +9,10 @@
"customer_defaults_tab",
"customer_defaults_section",
"cust_master_name",
"customer_group",
"naming_series_details",
"configure",
"column_break_4",
"customer_group",
"territory",
"item_price_tab",
"item_price_settings_section",
@@ -57,7 +59,9 @@
"section_break_zwh6",
"allow_delivery_of_overproduced_qty",
"column_break_mla9",
"deliver_secondary_items"
"deliver_secondary_items",
"default_naming_tab",
"transaction_naming_html"
],
"fields": [
{
@@ -279,7 +283,7 @@
{
"fieldname": "item_price_tab",
"fieldtype": "Tab Break",
"label": "Item Price"
"label": "Pricing"
},
{
"fieldname": "transaction_tab",
@@ -377,6 +381,29 @@
"fieldname": "blanket_orders_section",
"fieldtype": "Section Break",
"label": "Blanket Orders"
},
{
"fieldname": "configure",
"fieldtype": "Button",
"hidden": 1,
"label": "Configure Series"
},
{
"fieldname": "naming_series_details",
"fieldtype": "Small Text",
"hidden": 1,
"is_virtual": 1,
"label": "Naming Series options",
"read_only": 1
},
{
"fieldname": "default_naming_tab",
"fieldtype": "Tab Break",
"label": "Document Naming"
},
{
"fieldname": "transaction_naming_html",
"fieldtype": "HTML"
}
],
"grid_page_length": 50,
@@ -385,7 +412,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-04-21 21:29:32.890098",
"modified": "2026-04-29 11:05:48.836362",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -417,6 +417,7 @@ def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False,
@frappe.whitelist()
def deactivate_sales_person(status=None, employee=None):
frappe.has_permission("Employee", doc=employee, ptype="write", throw=True)
if status == "Left":
sales_person = frappe.db.get_value("Sales Person", {"Employee": employee})
if sales_person:

View File

@@ -11,6 +11,8 @@
"field_order": [
"title",
"disabled",
"column_break_ofhb",
"copy_attachments_to_transaction",
"applicable_modules_section",
"selling",
"buying",
@@ -72,12 +74,22 @@
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ofhb",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "copy_attachments_to_transaction",
"fieldtype": "Check",
"label": "Copy Attachments to Transaction"
}
],
"icon": "icon-legal",
"idx": 1,
"links": [],
"modified": "2026-04-14 18:22:49.285298",
"modified": "2026-04-29 22:51:49.285298",
"modified_by": "Administrator",
"module": "Setup",
"name": "Terms and Conditions",

View File

@@ -21,6 +21,7 @@ class TermsandConditions(Document):
from frappe.types import DF
buying: DF.Check
copy_attachments_to_transaction: DF.Check
disabled: DF.Check
selling: DF.Check
terms: DF.TextEditor | None

View File

@@ -1387,6 +1387,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1405,6 +1406,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1423,6 +1425,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1441,6 +1444,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1459,6 +1463,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1477,6 +1482,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1499,6 +1505,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1517,6 +1524,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1535,6 +1543,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1553,6 +1562,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1571,6 +1581,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1589,6 +1600,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1620,6 +1632,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1629,6 +1642,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1638,6 +1652,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1647,6 +1662,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1656,6 +1672,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1665,6 +1682,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1674,6 +1692,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1683,6 +1702,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1692,6 +1712,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1701,6 +1722,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1710,6 +1732,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1719,6 +1742,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -1727,6 +1751,7 @@
"account_number": "1433",
"root_type": "Asset"
},
"not_applicable": 1,
"tax_rate": 0.00
}
]
@@ -2150,6 +2175,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2168,6 +2194,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2186,6 +2213,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2204,6 +2232,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2222,6 +2251,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2240,6 +2270,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2262,6 +2293,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2280,6 +2312,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2298,6 +2331,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2316,6 +2350,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2334,6 +2369,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2352,6 +2388,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2383,6 +2420,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2392,6 +2430,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2401,6 +2440,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2410,6 +2450,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2419,6 +2460,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2428,6 +2470,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2437,6 +2480,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2446,6 +2490,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2455,6 +2500,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2464,6 +2510,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2473,6 +2520,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2482,6 +2530,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2490,6 +2539,7 @@
"account_number": "1588",
"root_type": "Asset"
},
"not_applicable": 1,
"tax_rate": 0.00
}
]
@@ -2913,6 +2963,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2931,6 +2982,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2949,6 +3001,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2967,6 +3020,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -2985,6 +3039,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3003,6 +3058,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3025,6 +3081,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3043,6 +3100,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3061,7 +3119,8 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"tax_rate": 19.00
"not_applicable": 1,
"tax_rate": 0.00
},
{
"tax_type": {
@@ -3079,6 +3138,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3097,6 +3157,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3115,6 +3176,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3146,6 +3208,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3155,6 +3218,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3164,6 +3228,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3173,6 +3238,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3182,6 +3248,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3191,6 +3258,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3200,6 +3268,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3209,6 +3278,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3218,6 +3288,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3227,6 +3298,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3236,6 +3308,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3245,6 +3318,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3253,6 +3327,7 @@
"account_number": "1550",
"root_type": "Asset"
},
"not_applicable": 1,
"tax_rate": 0.00
}
]
@@ -3645,6 +3720,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3661,6 +3737,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3677,6 +3754,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3693,6 +3771,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3709,6 +3788,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3725,6 +3805,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3745,6 +3826,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3761,6 +3843,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3777,6 +3860,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3793,6 +3877,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3809,6 +3894,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3825,6 +3911,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3853,6 +3940,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3861,6 +3949,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3869,6 +3958,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3877,6 +3967,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3885,6 +3976,7 @@
"root_type": "Liability",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3893,6 +3985,7 @@
"root_type": "Liability",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3901,6 +3994,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3909,6 +4003,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3917,6 +4012,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3925,6 +4021,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3933,6 +4030,7 @@
"root_type": "Asset",
"tax_rate": 19.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3941,6 +4039,7 @@
"root_type": "Asset",
"tax_rate": 7.00
},
"not_applicable": 1,
"tax_rate": 0.00
},
{
@@ -3948,6 +4047,7 @@
"account_name": "Entstandene Einfuhrumsatzsteuer",
"root_type": "Asset"
},
"not_applicable": 1,
"tax_rate": 0.00
}
]

View File

@@ -70,6 +70,7 @@ frappe.ui.form.on("Batch", {
item_code: frm.doc.item,
for_stock_levels: for_stock_levels,
consider_negative_batches: 1,
ignore_reserved_stock: 1,
},
callback: (r) => {
if (!r.message || r.message.length === 0) {

View File

@@ -372,6 +372,15 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
});
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
this.frm.script_manager.copy_from_first_row("items", row, ["project"]);
}
}
make_sales_invoice() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",

View File

@@ -1265,6 +1265,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1465,7 +1466,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2026-04-28 06:37:33.600775",
"modified": "2026-05-01 02:37:31.430649",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -202,6 +202,7 @@ class Item(Document):
self.validate_warehouse_for_reorder()
self.update_bom_item_desc()
self.validate_variant()
self.validate_has_variants()
self.validate_attributes_in_variants()
self.validate_stock_exists_for_template_item()
@@ -811,6 +812,43 @@ class Item(Document):
enqueue_after_commit=True,
)
def validate_variant(self):
if self.variant_of:
has_variants, based_on = frappe.get_value(
"Item", self.variant_of, ["has_variants", "variant_based_on"]
)
if not has_variants:
frappe.throw(_("Item {0} is not a template item.").format(frappe.bold(self.variant_of)))
if based_on == "Item Attribute":
for d in self.attributes:
if not frappe.db.exists(
"Item Variant Attribute", {"attribute": d.attribute, "parent": self.variant_of}
):
frappe.throw(
_("Attribute {0} is not valid for the selected template.").format(
frappe.bold(d.attribute)
)
)
numeric_values, disabled = frappe.get_value(
"Item Variant Attribute",
{"attribute": d.attribute, "parent": self.variant_of},
["numeric_values", "disabled"],
)
if disabled:
frappe.throw(_("Attribute {0} is disabled.").format(frappe.bold(d.attribute)))
if not numeric_values and not frappe.db.exists(
"Item Attribute Value", {"parent": d.attribute, "attribute_value": d.attribute_value}
):
frappe.throw(
_("Attribute Value {0} is not valid for the selected attribute {1}.").format(
frappe.bold(d.attribute_value), frappe.bold(d.attribute)
)
)
def validate_has_variants(self):
if self.is_new():
return

View File

@@ -9,6 +9,22 @@ frappe.ui.form.on("Pick List", {
}, 500);
},
set_warehouse_query: function (frm, fieldname, parentfield = null) {
const query = () => {
let filters = { company: frm.doc.company };
frm.doc.consider_rejected_warehouses ? null : (filters.is_rejected_warehouse = 0);
return { filters };
};
if (parentfield) {
frm.set_query(fieldname, parentfield, query);
} else {
frm.set_query(fieldname, query);
}
},
setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
@@ -21,21 +37,8 @@ frappe.ui.form.on("Pick List", {
"Stock Entry": "Stock Entry",
};
frm.set_query("warehouse", "locations", () => {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("parent_warehouse", () => {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.events.set_warehouse_query(frm, "warehouse", "locations");
frm.events.set_warehouse_query(frm, "parent_warehouse");
frm.set_query("work_order", () => {
return {

View File

@@ -522,9 +522,11 @@ class PickList(TransactionBase):
picked_items_details = self.get_picked_items_details(items)
self.item_location_map = frappe._dict()
from_warehouses = [self.parent_warehouse] if self.parent_warehouse else []
from_warehouses = []
if self.parent_warehouse:
from_warehouses = [self.parent_warehouse]
if self.work_order:
elif self.work_order:
root_warehouse = frappe.db.get_value(
"Warehouse", {"company": self.company, "parent_warehouse": ["IS", "NOT SET"], "is_group": 1}
)

View File

@@ -368,11 +368,13 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"cost_center",
"project",
]);
const field_copy = ["expense_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
};

View File

@@ -561,14 +561,7 @@ class PurchaseReceipt(BuyingController):
else flt(item.net_amount, item.precision("net_amount"))
)
outgoing_amount = (
flt((item.base_net_amount / item.received_qty) * item.qty, item.precision("base_net_amount"))
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
outgoing_amount = item.base_net_amount
if self.is_internal_transfer() and item.valuation_rate:
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
credit_amount = outgoing_amount
@@ -724,6 +717,9 @@ class PurchaseReceipt(BuyingController):
or stock_asset_rbnb
)
if self.is_return and item.expense_account:
loss_account = item.expense_account
cost_center = item.cost_center or frappe.get_cached_value(
"Company", self.company, "cost_center"
)

View File

@@ -4610,7 +4610,7 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(srbnb_cost, 1500)
def test_valuation_rate_for_rejected_materials_without_accepted_materials(self):
def test_valuation_rate_for_rejected_materials_withoout_accepted_materials(self):
item = make_item("Test Item with Rej Material Valuation WO Accepted", {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"
@@ -5423,33 +5423,6 @@ class TestPurchaseReceipt(ERPNextTestSuite):
self.assertEqual(row.warehouse, "_Test Warehouse 1 - _TC")
self.assertEqual(row.incoming_rate, 100)
def test_bill_for_rejected_quantity_in_purchase_invoice(self):
item_code = make_item("Test Rejected Qty", {"is_stock_item": 1}).name
with self.change_settings("Buying Settings", {"bill_for_rejected_quantity_in_purchase_invoice": 0}):
pr = make_purchase_receipt(
item_code=item_code,
qty=10,
rejected_qty=2,
rate=10,
warehouse="_Test Warehouse - _TC",
)
self.assertEqual(pr.total_qty, 10)
self.assertEqual(pr.total, 100)
with self.change_settings("Buying Settings", {"bill_for_rejected_quantity_in_purchase_invoice": 1}):
pr = make_purchase_receipt(
item_code=item_code,
qty=10,
rejected_qty=2,
rate=10,
warehouse="_Test Warehouse - _TC",
)
self.assertEqual(pr.total_qty, 12)
self.assertEqual(pr.total, 120)
def test_different_exchange_rate_in_pr_and_pi(self):
from erpnext.accounts.doctype.account.test_account import create_account

View File

@@ -1049,7 +1049,7 @@
"search_index": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
@@ -1064,7 +1064,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
@@ -1149,7 +1149,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-04-07 15:40:47.032889",
"modified": "2026-04-29 16:01:34.154697",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -411,8 +411,15 @@ def repost(doc):
message = message.get("message")
status = "Failed"
# If failed because of timeout, set status to In Progress
if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback):
# If failed because of a recoverable error (timeout, deadlock), set status to In Progress
# so the scheduler automatically retries instead of leaving it permanently failed.
# NOTE: isinstance check comes first because the traceback string matching is unreliable
# when SIGALRM kills the process mid-C-extension (JobTimeoutException may not appear
# in the traceback if the exception handler itself was interrupted).
traceback_lower = traceback.lower() if traceback else ""
if isinstance(e, RecoverableErrors) or (
traceback_lower and ("timeout" in traceback_lower or "deadlock found" in traceback_lower)
):
status = "In Progress"
if traceback:

View File

@@ -8,11 +8,10 @@ from collections import Counter, defaultdict
import frappe
import frappe.query_builder
import frappe.query_builder.functions
from frappe import _, _dict, bold
from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.query_builder.functions import Concat_ws, Locate, Sum
from frappe.query_builder.functions import Concat_ws, Sum
from frappe.utils import (
cint,
cstr,
@@ -3358,22 +3357,21 @@ def get_stock_ledgers_for_serial_nos(kwargs):
serial_nos = [serial_nos]
if serial_nos:
import re
escaped_serial_nos = [re.escape(sn) for sn in serial_nos if sn]
regex_pattern = r"\n(" + "|".join(escaped_serial_nos) + r")\n"
query = (
query.left_join(serial_batch_entry)
.on(stock_ledger_entry.serial_and_batch_bundle == serial_batch_entry.parent)
.where(
serial_batch_entry.serial_no.isin(serial_nos)
| Concat_ws("", "\n", stock_ledger_entry.serial_no, "\n").regexp(regex_pattern)
)
.distinct()
)
bundle_match = serial_batch_entry.serial_no.isin(serial_nos)
padded_serial_no = Concat_ws("", "\n", stock_ledger_entry.serial_no, "\n")
direct_match = None
for sn in serial_nos:
cond = Locate(f"\n{sn}\n", padded_serial_no) > 0
direct_match = cond if direct_match is None else (direct_match | cond)
query = query.where(bundle_match | direct_match)
if kwargs.ignore_voucher_detail_no:
query = query.where(stock_ledger_entry.voucher_detail_no != kwargs.ignore_voucher_detail_no)

View File

@@ -243,7 +243,6 @@ class StockEntry(StockController, SubcontractingInwardController):
self.set_transfer_qty()
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "transfer_qty")
self.validate_warehouse()
self.validate_warehouse_of_sabb()
self.validate_work_order()
self.validate_source_stock_entry()
@@ -259,6 +258,7 @@ class StockEntry(StockController, SubcontractingInwardController):
else:
self.validate_job_card_fg_item()
self.validate_warehouse()
self.validate_with_material_request()
self.validate_batch()
self.validate_inspection()
@@ -383,6 +383,9 @@ class StockEntry(StockController, SubcontractingInwardController):
def _set_serial_batch_for_disassembly_from_available_materials(self):
available_materials = get_available_materials(self.work_order, self)
for row in self.items:
if row.serial_no or row.batch_no or row.serial_and_batch_bundle:
continue
warehouse = row.s_warehouse or row.t_warehouse
materials = available_materials.get((row.item_code, warehouse))
if not materials:
@@ -850,15 +853,14 @@ class StockEntry(StockController, SubcontractingInwardController):
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Manufacture":
if has_bom:
if d.is_finished_item or d.type or d.is_legacy_scrap_item:
d.s_warehouse = None
if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
else:
d.t_warehouse = None
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
if d.is_finished_item or d.type or d.is_legacy_scrap_item:
d.s_warehouse = None
if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
else:
d.t_warehouse = None
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Disassemble":
if has_bom:

View File

@@ -726,11 +726,7 @@ def get_item_tax_template(ctx, item=None, out: ItemDetails | None = None):
item_tax_template = _get_item_tax_template(ctx, item.taxes, out)
if not item_tax_template:
item_group = item.item_group
while item_group and not item_tax_template:
item_group_doc = frappe.get_cached_doc("Item Group", item_group)
item_tax_template = _get_item_tax_template(ctx, item_group_doc.taxes, out)
item_group = item_group_doc.parent_item_group
item_tax_template = _get_item_tax_template_from_item_group(ctx, item.item_group, out)
if out and ctx.get("child_doctype") and item_tax_template:
out.update(get_fetch_values(ctx.get("child_doctype"), "item_tax_template", item_tax_template))
@@ -738,6 +734,18 @@ def get_item_tax_template(ctx, item=None, out: ItemDetails | None = None):
return item_tax_template
def _get_item_tax_template_from_item_group(ctx, item_group, out=None):
from frappe.utils.nestedset import get_ancestors_of
ancestors = get_ancestors_of("Item Group", item_group)
for group in [item_group, *ancestors]:
group_doc = frappe.get_cached_doc("Item Group", group)
item_tax_template = _get_item_tax_template(ctx, group_doc.taxes, out)
if item_tax_template:
return item_tax_template
return None
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def _get_item_tax_template(
ctx: ItemDetailsCtx, taxes, out: ItemDetails | None = None, for_validate=False
@@ -1117,6 +1125,7 @@ def insert_item_price(ctx: ItemDetailsCtx):
currency=ctx.currency,
uom=ctx.stock_uom,
price_list=ctx.price_list,
valid_from=transaction_date,
)
item_price.insert()
frappe.msgprint(
@@ -1139,6 +1148,7 @@ def insert_item_price(ctx: ItemDetailsCtx):
"currency": ctx.currency,
"price_list_rate": price_list_rate,
"uom": ctx.stock_uom,
"valid_from": transaction_date,
}
)
item_price.insert()

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Supplier-Wise Sales Analytics"] = {
frappe.query_reports["Item Wise Consumption"] = {
filters: [
{
fieldname: "supplier",

View File

@@ -10,10 +10,10 @@
"modified": "2017-02-24 20:13:38.914651",
"modified_by": "Administrator",
"module": "Stock",
"name": "Supplier-Wise Sales Analytics",
"name": "Item Wise Consumption",
"owner": "Administrator",
"ref_doctype": "Stock Ledger Entry",
"report_name": "Supplier-Wise Sales Analytics",
"report_name": "Item Wise Consumption",
"report_type": "Script Report",
"roles": [
{

View File

@@ -7,6 +7,7 @@ import frappe
from frappe import _
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle
from erpnext.stock.serial_batch_bundle import get_serial_no_status
from erpnext.stock.stock_ledger import get_stock_ledger_entries
BUYING_VOUCHER_TYPES = ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]
@@ -111,7 +112,7 @@ def get_data(filters):
"posting_time": row.posting_time,
"voucher_type": row.voucher_type,
"voucher_no": row.voucher_no,
"status": "Active" if row.actual_qty > 0 else "Delivered",
"status": get_serial_no_status(row),
"company": row.company,
"warehouse": row.warehouse,
"qty": 1 if row.actual_qty > 0 else -1,

View File

@@ -11,6 +11,7 @@ from frappe.query_builder.functions import Count
from frappe.utils import cint, date_diff, flt, get_datetime
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.valuation import round_off_if_near_zero
Filters = frappe._dict
@@ -117,10 +118,14 @@ def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: d
i *= 2
range_values[i] = flt(range_values[i] + qty, precision)
range_values[i + 1] = flt(range_values[i + 1] + stock_value, precision)
if range_values[i] == 0.0 and round_off_if_near_zero(range_values[i + 1], 2) == 0:
range_values[i + 1] = 0.0
break
else:
range_values[-2] = flt(range_values[-2] + qty, precision)
range_values[-1] = flt(range_values[-1] + stock_value, precision)
if range_values[-2] == 0.0 and round_off_if_near_zero(range_values[-1], 2) == 0:
range_values[-1] = 0.0
return range_values

View File

@@ -73,6 +73,7 @@ def execute(filters=None):
inv_dimension_wise_dict, filters, inv_dimension_key=inv_dimension_key, opening_row=opening_row
)
item_wh_wise_prev_sle = {}
for sle in sl_entries:
item_detail = item_details[sle.item_code]
@@ -114,6 +115,21 @@ def execute(filters=None):
elif sle.voucher_type == "Stock Reconciliation":
sle["in_out_rate"] = sle.valuation_rate
if (
sle.voucher_type == "Stock Reconciliation"
and not sle.in_qty
and not sle.out_qty
and not sle.actual_qty
):
if prev_sle := item_wh_wise_prev_sle.get((sle.item_code, sle.warehouse)):
bal_qty = prev_sle.get("qty_after_transaction", 0)
qty = sle.qty_after_transaction - bal_qty
if qty > 0:
sle.in_qty = qty
elif qty < 0:
sle.out_qty = qty
item_wh_wise_prev_sle[(sle.item_code, sle.warehouse)] = sle
data.append(sle)
if include_uom:

View File

@@ -63,7 +63,7 @@ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("Incorrect Stock Value Report", {"company": "_Test Company with perpetual inventory"}),
("Incorrect Serial No Valuation", {}),
("Incorrect Balance Qty After Transaction", {}),
("Supplier-Wise Sales Analytics", {}),
("Item Wise Consumption", {}),
("Item Prices", {"items": "Enabled Items only"}),
("Delayed Item Report", {"based_on": "Sales Invoice"}),
("Delayed Item Report", {"based_on": "Delivery Note"}),

View File

@@ -15,6 +15,45 @@ from erpnext.stock.deprecated_serial_batch import (
)
from erpnext.stock.valuation import round_off_if_near_zero
CONSUMED_SERIAL_NO_STOCK_ENTRY_PURPOSES = (
"Manufacture",
"Material Issue",
"Repack",
"Material Consumption for Manufacture",
)
INACTIVE_SERIAL_NO_STOCK_ENTRY_PURPOSES = ("Disassemble", "Material Receipt")
def get_serial_no_status(sle):
warehouse = sle.warehouse if sle.actual_qty > 0 else None
if warehouse:
return "Active"
status = get_status_for_serial_nos(sle)
if sle.voucher_type == "Stock Entry" and sle.actual_qty < 0:
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
if purpose in INACTIVE_SERIAL_NO_STOCK_ENTRY_PURPOSES:
status = "Inactive"
return status
def get_status_for_serial_nos(sle):
status = "Inactive"
if sle.actual_qty < 0:
status = "Delivered"
if sle.voucher_type == "Stock Entry":
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
if purpose in CONSUMED_SERIAL_NO_STOCK_ENTRY_PURPOSES:
status = "Consumed"
if sle.is_cancelled == 1 and (
sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed"
):
status = "Inactive"
return status
class SerialBatchBundle:
def __init__(self, **kwargs):
@@ -429,25 +468,7 @@ class SerialBatchBundle:
self.update_serial_no_status_warehouse(self.sle, serial_nos)
def get_status_for_serial_nos(self, sle):
status = "Inactive"
if sle.actual_qty < 0:
status = "Delivered"
if sle.voucher_type == "Stock Entry":
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
if purpose in [
"Manufacture",
"Material Issue",
"Repack",
"Material Consumption for Manufacture",
]:
status = "Consumed"
if sle.is_cancelled == 1 and (
sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed"
):
status = "Inactive"
return status
return get_status_for_serial_nos(sle)
def update_serial_no_status_warehouse(self, sle, serial_nos):
warehouse = sle.warehouse if sle.actual_qty > 0 else None
@@ -455,19 +476,12 @@ class SerialBatchBundle:
if isinstance(serial_nos, str):
serial_nos = [serial_nos]
status = "Active"
if not warehouse:
status = self.get_status_for_serial_nos(sle)
status = get_serial_no_status(sle)
customer = None
if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0:
customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")
if sle.voucher_type in ["Stock Entry"] and sle.actual_qty < 0:
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
if purpose in ["Disassemble", "Material Receipt"]:
status = "Inactive"
sn_table = frappe.qb.DocType("Serial No")
query = (
@@ -1055,6 +1069,12 @@ class SerialBatchCreation:
self.__dict__.update(item_details)
def set_other_details(self):
from erpnext.stock.utils import get_combine_datetime
if not self.get("posting_datetime"):
if self.get("posting_date") and self.get("posting_time"):
self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time)
if not self.get("posting_datetime"):
self.posting_datetime = now()
self.__dict__["posting_datetime"] = self.posting_datetime

View File

@@ -900,6 +900,13 @@ class BootStrapTestData:
"default_currency": "USD",
"accounts": [{"company": "_Test Company", "account": "_Test Payable USD - _TC"}],
},
{
"doctype": "Supplier",
"supplier_name": "_Test Another Supplier USD",
"supplier_group": "_Test Supplier Group",
"default_currency": "USD",
"accounts": [{"company": "_Test Company", "account": "_Test Payable USD - _TC"}],
},
{
"doctype": "Supplier",
"supplier_name": "_Test Supplier With Tax Category",
@@ -951,6 +958,13 @@ class BootStrapTestData:
"is_group": 0,
"parent_cost_center": "_Test Company - _TC",
},
{
"company": "_Test Company",
"cost_center_name": "Sub",
"doctype": "Cost Center",
"is_group": 0,
"parent_cost_center": "_Test Company - _TC",
},
]
self.make_records(["cost_center_name", "company"], records)

View File

@@ -18,6 +18,14 @@ class UOMMustBeIntegerError(frappe.ValidationError):
class TransactionBase(StatusUpdater):
def on_change(self):
# `on_change` also fires for `db_set()`, so only run during an actual insert/save.
is_real_save = self.flags.in_insert or (self.doctype, self.name) in frappe.flags.currently_saving
if not is_real_save:
return
self.copy_terms_and_conditions_attachments()
def validate_posting_time(self):
# set Edit Posting Date and Time to 1 while data import and restore
if (frappe.flags.in_import or self.flags.from_restore) and self.posting_date:
@@ -36,6 +44,56 @@ class TransactionBase(StatusUpdater):
def validate_uom_is_integer(self, uom_field, qty_fields, child_dt=None):
validate_uom_is_integer(self, uom_field, qty_fields, child_dt)
def copy_terms_and_conditions_attachments(self):
if (
not self.name
or not self.meta.has_field("tc_name")
or not self.tc_name
or not self.has_value_changed("tc_name")
):
return
copy_attachments_to_transaction = frappe.db.get_value(
"Terms and Conditions", self.tc_name, "copy_attachments_to_transaction"
)
if not cint(copy_attachments_to_transaction):
return
source_attachments = frappe.get_all(
"File",
filters={
"attached_to_doctype": "Terms and Conditions",
"attached_to_name": self.tc_name,
},
fields=["name", "file_url"],
)
if not source_attachments:
return
existing_file_urls = {
attachment.file_url
for attachment in frappe.get_all(
"File",
filters={
"attached_to_doctype": self.doctype,
"attached_to_name": self.name,
},
fields=["file_url"],
)
if attachment.file_url
}
for source_attachment in source_attachments:
if not source_attachment.file_url or source_attachment.file_url in existing_file_urls:
continue
# Reuse the existing file metadata so the same on-disk blob is shared.
new_attachment = frappe.get_doc("File", source_attachment.name).create_attachment_copy(
attached_to_doctype=self.doctype,
attached_to_name=self.name,
)
existing_file_urls.add(new_attachment.file_url)
def validate_with_previous_doc(self, ref):
self.exclude_fields = ["conversion_factor", "uom"] if self.get("is_return") else []

View File

@@ -328,8 +328,8 @@
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "Supplier-Wise Sales Analytics",
"link_to": "Supplier-Wise Sales Analytics",
"label": "Item Wise Consumption",
"link_to": "Item Wise Consumption",
"link_type": "Report",
"show_arrow": 0,
"type": "Link"

View File

@@ -289,17 +289,6 @@
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
"indent": 0,
"keep_closed": 0,
"label": "BOM Stock Report",
"link_to": "BOM Stock Report",
"link_type": "Report",
"show_arrow": 0,
"type": "Link"
},
{
"child": 1,
"collapsible": 1,
@@ -437,7 +426,7 @@
"type": "Link"
}
],
"modified": "2026-02-20 16:45:00.399936",
"modified": "2026-05-05 11:01:50.260118",
"modified_by": "Administrator",
"module": "Manufacturing",
"module_onboarding": "Manufacturing Onboarding",