Compare commits

..

161 Commits

Author SHA1 Message Date
Nishka Gosalia
9a9b330af6 Merge pull request #54910 from frappe/mergify/bp/version-16-hotfix/pr-54731
fix(UX): Buying settings form cleanup (backport #54731)
2026-05-14 11:56:28 +05:30
rohitwaghchaure
76204b920d Merge pull request #54928 from frappe/mergify/bp/version-16-hotfix/pr-54905
fix: posting date and time (backport #54905)
2026-05-14 07:33:26 +05:30
rohitwaghchaure
21ada7799c chore: fix linter issue
(cherry picked from commit 3c993377aa)
2026-05-13 18:19:17 +00:00
rohitwaghchaure
f4e66914c6 chore: fixed test case
(cherry picked from commit c740f77a6f)
2026-05-13 18:19:17 +00:00
Rohit Waghchaure
1c44c60dbd fix: posting date and time
(cherry picked from commit fb6c05f186)
2026-05-13 18:19:17 +00:00
mergify[bot]
596c2571f6 fix: add warehouse vaildation for repack entry (backport #54866) (#54901)
fix: add warehouse vaildation for repack entry (#54866)

(cherry picked from commit bc07b2d3e5)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-13 15:24:51 +00:00
mergify[bot]
c4994548c3 fix(stock): add whole number quantity validation in Stock Reconciliation (backport #54922) (#54925)
fix(stock): add whole number quantity validation in Stock Reconciliation (#54922)

(cherry picked from commit f9dec73042)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-05-13 15:03:45 +00:00
Khushi Rawat
604f80f043 Merge pull request #54913 from frappe/mergify/bp/version-16-hotfix/pr-53843
feat: Accounts Payable print template revamp and print format introduction (backport #53843)
2026-05-13 16:07:00 +05:30
Khushi Rawat
4a4757dbc6 Merge pull request #54915 from frappe/mergify/bp/version-16-hotfix/pr-53762
feat: General ledger print template revamp and print format introduction (backport #53762)
2026-05-13 16:06:27 +05:30
Khushi Rawat
436e0269f8 Merge pull request #54914 from frappe/mergify/bp/version-16-hotfix/pr-53822
feat: Accounts Receivable print template revamp and print format introduction (backport #53822)
2026-05-13 16:05:47 +05:30
Khushi Rawat
5d73da5a85 Merge pull request #54912 from frappe/mergify/bp/version-16-hotfix/pr-53870
feat: AR and AP summary reports print template revamp and print format introduction (backport #53870)
2026-05-13 15:57:07 +05:30
Khushi Rawat
886c7cc5a3 Merge pull request #54911 from frappe/mergify/bp/version-16-hotfix/pr-53934
feat: Financial Statements print format introduction (backport #53934)
2026-05-13 15:56:45 +05:30
Khushi Rawat
d430736177 Merge pull request #54634 from frappe/mergify/bp/version-16-hotfix/pr-54538
fix(UX): Item master form cleanup (backport #54538)
2026-05-13 15:11:31 +05:30
Shllokkk
caa524f661 fix: changes to gl print template
(cherry picked from commit e8d08df044)
2026-05-13 09:41:22 +00:00
Shllokkk
cd69b66761 refactor: table body data rendering cleanup
(cherry picked from commit 0d4f56bf84)
2026-05-13 09:41:22 +00:00
Shllokkk
040b31d3a7 fix: improve filter details render logic to avoid showing duplicate information
(cherry picked from commit 9660debe28)
2026-05-13 09:41:21 +00:00
Shllokkk
04893ae0e3 refactor: clean and standardize print template for general ledger report
(cherry picked from commit 3ba36212b0)
2026-05-13 09:41:21 +00:00
Shllokkk
0ead2296e6 fix: minor changes in print template
(cherry picked from commit e3019c827c)
2026-05-13 09:40:53 +00:00
Shllokkk
09b19f7a2a fix: minor bug fixes for ar print template
(cherry picked from commit 4228885f1e)
2026-05-13 09:40:52 +00:00
Shllokkk
4e7f2eeaa0 feat: introduce print format for Accounts Receivable report
(cherry picked from commit e6a32a9d02)
2026-05-13 09:40:52 +00:00
Shllokkk
059372add5 fix: improve design and refactor ar print template
(cherry picked from commit ffc59ebc9c)
2026-05-13 09:40:52 +00:00
Shllokkk
16bc28bd70 fix: minor changes in print template
(cherry picked from commit 915fcc0166)
2026-05-13 09:40:24 +00:00
Shllokkk
1c6dc80b70 feat: add print format for accounts payable report
(cherry picked from commit 2bf9d41797)
2026-05-13 09:40:24 +00:00
Shllokkk
748a3d72a3 refactor: revamp print template for accounts payable report
(cherry picked from commit c051536182)
2026-05-13 09:40:23 +00:00
Shllokkk
0d50e03595 fix: minor changes in print templates
(cherry picked from commit 44e0b36093)
2026-05-13 09:39:56 +00:00
Shllokkk
e1446fc6f4 fix: minor bugs in print templates
(cherry picked from commit 86ee9959a2)
2026-05-13 09:39:56 +00:00
Shllokkk
928fab6f7e fix: revamp print formats for accounts receivable summary and accounts payable summary reports
(cherry picked from commit 5bbcb73808)
2026-05-13 09:39:55 +00:00
mergify[bot]
0d07083299 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)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-13 15:09:38 +05:30
Shllokkk
c4037daca8 fix: add filter subtitle in print formats
(cherry picked from commit e82b4d9ca7)
2026-05-13 09:39:05 +00:00
Shllokkk
9a18d318d9 fix: styling in trial_balance.html and print format
(cherry picked from commit 5858b14071)
2026-05-13 09:39:05 +00:00
Shllokkk
2ff9f00ce0 refactor: print templates for financial statements
(cherry picked from commit e8777a1e34)
2026-05-13 09:39:05 +00:00
Shllokkk
daaa4ca0c8 fix: minor text issues in print
(cherry picked from commit fa0a9085ca)
2026-05-13 09:39:04 +00:00
Shllokkk
1d08448d1a feat: print format for report trial balance
(cherry picked from commit ac7e5271b0)
2026-05-13 09:39:04 +00:00
Shllokkk
3283c461f1 feat: introduce print formats for financial statements
(cherry picked from commit 82cac9c40f)
2026-05-13 09:39:04 +00:00
Nishka Gosalia
e7ae296614 fix(UX): Buying settings form cleanup (#54731)
* fix(UX): Buying settings form cleanup

* fix: controller approach modification

* fix: dark mode support

(cherry picked from commit 45f05fbeaa)
2026-05-13 09:24:12 +00:00
mergify[bot]
c041cd27b5 fix(general-ledger): show raw GL entries when categorize_by is empty (backport #54816) (#54830)
fix(general-ledger): show raw GL entries when categorize_by is empty (#54816)

(cherry picked from commit dfbe847307)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-05-13 05:42:11 +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
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
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
Khushi Rawat
0eb049cd85 fix(UX): Item master form cleanup (#54538)
* fix: UI improvements for item form

* fix: add descriptions and tooltips to all checkboxes

* feat: show toast notification when item price is created

* fix: do not use selling rate for opening stock entry

* fix: add descriptions and tooltips to item default fields

* fix(test): give valuation rate for opening stock entry creation

* fix: moving naming series toggle before the return

* refactor: more changes in the form UI

(cherry picked from commit 43937acd8b)
2026-04-29 09:15:20 +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
mergify[bot]
5de4b013ea fix: avoid double reduction of pe reference outstanding (backport #54193) (#54613)
* fix: avoid double reduction of pe reference outstanding (#54193)

Co-authored-by: diptanilsaha <diptanil@frappe.io>
(cherry picked from commit d1a80d40c4)

# Conflicts:
#	erpnext/accounts/utils.py

* chore: remove type hints for function parameters

---------

Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 20:41:42 +00:00
mergify[bot]
8f8bf13b41 fix: filter overdue purchase order items by company (backport #54099) (#54611)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
fix: filter overdue purchase order items by company (#54099)
2026-04-29 01:25:31 +05:30
mergify[bot]
11117710d3 fix: duplicate entries being shown in batch exists in future transact… (backport #54604) (#54606)
fix: duplicate entries being shown in batch exists in future transact… (#54604)

fix: duplicate entries being shown in batch exists in future transactions msg
(cherry picked from commit 54f20de7e3)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-28 22:23:13 +05:30
diptanilsaha
90b07b3db5 Merge branch 'version-16' into version-16-hotfix 2026-04-28 21:49:53 +05:30
Trusted Computer
0d498baa10 refactor: bring back titles on sales transactions and make them optional and visible on purchase transactions (backport #52633) (#54601)
* refactor: bring back titles on sales transactions and make them optional and visible on purchase transactions

* fix: update timestamp in json to UTC
2026-04-28 21:12:41 +05:30
mergify[bot]
deef1696d6 refactor(sms_center): replaced raw SQL queries with Query Builder (backport #54600) (#54603)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 15:41:59 +00:00
Lakshit Jain
c6ee18b4d4 Merge pull request #54599 from frappe/mergify/bp/version-16-hotfix/pr-54362
fix: filter opening entries after closing voucher (backport #54362)
2026-04-28 19:04:45 +05:30
Lakshit Jain
2f88fa6731 Merge pull request #54598 from frappe/mergify/bp/version-16-hotfix/pr-54517
fix: always exclude pcv entries except for closing account head (backport #54517)
2026-04-28 19:03:09 +05:30
Lakshit Jain
9c5c87b354 Merge pull request #54594 from frappe/mergify/bp/version-16-hotfix/pr-54479
fix:  Handle mandantory filters for financial statements report (backport #54479)
2026-04-28 18:45:05 +05:30
Smit Vora
64a724baea test: include both accounts to test sum = 0
(cherry picked from commit 590f2ffe28)
2026-04-28 13:13:46 +00:00
Smit Vora
7f32c3aca7 test: opening entries after period closing
(cherry picked from commit 5fc3ca1d4b)
2026-04-28 13:13:46 +00:00
Smit Vora
4675921077 chore: comment
(cherry picked from commit c94b8c41f3)
2026-04-28 13:13:46 +00:00
Smit Vora
d51ce66cb2 fix: summing of values could be zero even if values exist
(cherry picked from commit 7ae91cac01)
2026-04-28 13:13:45 +00:00
vorasmit
6bd6e62c8c fix: filter opening entries in first year in custom financial statement
(cherry picked from commit 3c8a066484)
2026-04-28 13:13:45 +00:00
Smit Vora
fa901946ce test: pcv is excluded from PL accounts
(cherry picked from commit 84aa54c540)
2026-04-28 13:13:18 +00:00
Smit Vora
446c111653 fix: always exclude pcv entries except for closing account head
(cherry picked from commit 0349e7a0b8)
2026-04-28 13:13:18 +00:00
Abdeali Chharchhoda
859b24dd95 chore: minor fix
(cherry picked from commit 3854d2cbf6)
2026-04-28 12:01:21 +00:00
Abdeali Chharchhoda
7b60ec8457 fix: update account identification to avoid using name_field in financial statements
(cherry picked from commit 1fd6c3ba1a)
2026-04-28 12:01:21 +00:00
Abdeali Chharchhoda
e6f0bb66e2 fix: add filter labels and required filters for financial report validation
(cherry picked from commit 4274c2aba3)
2026-04-28 12:01:21 +00:00
Abdeali Chharchhoda
6570796fba fix: update fiscal year filter to use mandatory_depends_on instead of reqd
(cherry picked from commit 79d6a51e1e)
2026-04-28 12:01:20 +00:00
Abdeali Chharchhoda
fba78711cc fix: ensure fiscal year is checked before validating date filters in financial statements
(cherry picked from commit 5a915cb45e)
2026-04-28 12:01:20 +00:00
mergify[bot]
9f04fcc190 fix(get_stock_balance): validate inventory dimension fieldnames (backport #54587) (#54589)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(`get_stock_balance`): validate inventory dimension fieldnames (#54587)
2026-04-28 11:35:16 +00:00
mergify[bot]
5289aa0ab3 fix(payment_entry): escape arguments on invoice and order fetching sql queries (backport #54582) (#54586)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): escape arguments on invoice and order fetching sql queries (#54582)
2026-04-28 10:48:41 +00:00
ruthra kumar
185ef4e273 Merge pull request #54553 from frappe/mergify/bp/version-16-hotfix/pr-54509
fix: hide feature flag controlled fields on install (backport #54509)
2026-04-28 16:04:42 +05:30
ruthra kumar
45dc2c40fd fix: hide feature flag controlled fields on install
(cherry picked from commit 889fdf2f11)
2026-04-28 15:44:28 +05:30
mergify[bot]
386a373c9b chore(sidebar): moved Inactive Customers from CRM to Selling Workspace Sidebar (backport #54578) (#54581)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 15:41:36 +05:30
mergify[bot]
134e4b7446 fix: update status of quotation in patch (backport #54577) (#54580)
fix: update status of quotation in patch (#54577)

(cherry picked from commit 2088a01c19)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-28 09:44:16 +00:00
Jatin3128
15b6633fc3 feat: add setting to hide Subscription references across doctypes (#54576) 2026-04-28 13:09:50 +05:30
mergify[bot]
f14751d538 fix(manufacturing): remove conversion factor for stock qty (backport #54525) (#54573)
fix(manufacturing): remove conversion factor for stock qty (#54525)

(cherry picked from commit 6f9089dd5b)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-04-28 05:38:29 +00:00
mergify[bot]
f7fa394aea fix: negative quantity check in validate_item_qty (backport #54559) (#54572)
fix: negative quantity check in validate_item_qty (#54559)

Fix negative quantity check in validate_item_qty

When saving a Blanket Order with a blank qty field in the items table, the following error is raised:

TypeError: '<' not supported between instances of 'NoneType' and 'int'

Root cause: The validate_item_qty method compares d.qty < 0 directly. When the qty field is left empty, its value is None, and Python cannot compare None with an integer.

Fix
Wrap d.qty with flt(), which safely converts None (and any non-numeric value) to 0.0 before the comparison.

# Before
if d.qty < 0:

# After
if flt(d.qty) < 0:

(cherry picked from commit 63edd5ddc6)

Co-authored-by: Vinay Mishra <39999379+vinaymishraofficial@users.noreply.github.com>
2026-04-28 05:33:55 +00:00
mergify[bot]
d9a9a5bcde fix: debit credit not equal in purchase transactions for multi currency (backport #54456) (#54564)
fix: debit credit not equal in purchase transactions for multi currency (#54456)

(cherry picked from commit 601581d6f8)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-27 15:23:05 +00:00
Mihir Kandoi
0df38a841e fix: correct display depends on condition (#54556) 2026-04-27 10:08:47 +00:00
mergify[bot]
d56df96f73 fix: make inv dimen reqd only in delivery note (backport #54546) (#54552)
fix: make inv dimen reqd only in delivery note (#54546)

(cherry picked from commit 0aadd1e3a5)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-27 15:10:44 +05:30
mergify[bot]
cc85370d54 fix(stock): remove validation for transfer_qty field (backport #54542) (#54545)
fix(stock): remove validation for transfer_qty field (#54542)

(cherry picked from commit 60a6b38c31)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-04-27 07:20:16 +00:00
mergify[bot]
ac9aa7f154 refactor: quality inspection item query (backport #54511) (#54540)
* refactor: quality inspection item query (#54511)

(cherry picked from commit be2a4b7b2a)

# Conflicts:
#	erpnext/stock/doctype/quality_inspection/quality_inspection.py

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-27 05:50:37 +00:00
MochaMind
23cac0df83 chore: update POT file (#54535) 2026-04-26 18:55:31 +02:00
mergify[bot]
5c0d2cb474 feat: danish_bosnian_address_template (backport #54093) (#54516)
feat: danish_bosnian_address_template (#54093)

(cherry picked from commit e517eeaaa2)

Co-authored-by: mahsem <137205921+mahsem@users.noreply.github.com>
2026-04-26 21:06:52 +05:30
mergify[bot]
cabea2f288 fix(stock): set incoming rate as zero for outward sle (backport #54514) (#54533)
fix(stock): set incoming rate as zero for outward sle

(cherry picked from commit ce37530e70)

Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
2026-04-26 20:24:52 +05:30
rohitwaghchaure
4c95daaca5 Merge pull request #54531 from frappe/mergify/bp/version-16-hotfix/pr-54530
fix(stock): show item code in serial and batch selector dialog (backport #54530)
2026-04-26 10:25:51 +05:30
Sudharsanan11
62bbe28a72 fix(stock): add stock entry in batch master connection
(cherry picked from commit fee5bcadb2)
2026-04-26 04:35:42 +00:00
Sudharsanan11
85d1eb8379 fix(stock): show item code in serial and batch selector dialog
(cherry picked from commit f572bc51e1)
2026-04-26 04:35:42 +00:00
mergify[bot]
8de9ac4e34 refactor(UX): selling settings form (backport #54412) (#54527) 2026-04-25 18:01:07 +05:30
mergify[bot]
f3996fb971 fix(PCV): set correct filters of from_date and to_date on General Ledger Report on clicking Ledger button (backport #54522) (#54524)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(PCV): set correct filters of `from_date` and `to_date` on General Ledger Report on clicking `Ledger` button (#54522)
2026-04-25 00:08:27 +05:30
mergify[bot]
764c775e19 refactor: tax witholding report (backport #54449) (backport #54477) (#54519)
* refactor: use consistent report column names

(cherry picked from commit 6dca96b423)
(cherry picked from commit 9276cd7343)

* refactor: how data is built

(cherry picked from commit c3e7f7f02f)
(cherry picked from commit be0e58fb23)

* refactor: better label for entity type

(cherry picked from commit 53666974a3)
(cherry picked from commit fffaf834fd)

* refactor: updated key for withholding_date

(cherry picked from commit 07b023a934)
(cherry picked from commit e6cfdb8e4d)

* test: None is better than zero, as no values exist

(cherry picked from commit b5550f747e)
(cherry picked from commit 40466be9ef)

* refactor: make report extensible by regional apps

(cherry picked from commit f0ea20e579)
(cherry picked from commit 6392126ca5)

* fix: add party type for dynamic link support

(cherry picked from commit b925469c4d)
(cherry picked from commit c6d4802857)

---------

Co-authored-by: Smit Vora <smitvora203@gmail.com>
2026-04-24 14:39:42 +00:00
Frappe PR Bot
66ec6a4d20 chore(release): Bumped to Version 16.15.1
## [16.15.1](https://github.com/frappe/erpnext/compare/v16.15.0...v16.15.1) (2026-04-24)

### Bug Fixes

* preserve inventory dimensions when raw materials are reset (backport [#54440](https://github.com/frappe/erpnext/issues/54440)) (backport [#54493](https://github.com/frappe/erpnext/issues/54493)) ([#54513](https://github.com/frappe/erpnext/issues/54513)) ([610735d](610735d1c5))
2026-04-24 12:35:07 +00:00
mergify[bot]
610735d1c5 fix: preserve inventory dimensions when raw materials are reset (backport #54440) (backport #54493) (#54513)
* fix: preserve inventory dimensions when raw materials are reset (backport #54440) (#54493)

fix: preserve inventory dimensions when raw materials are reset (#54440)

* fix: preserve inventory dimensions when raw materials are reset

* test: add test case

(cherry picked from commit 0e20e35842)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit 456e99b352)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflicts

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-24 12:33:34 +00:00
Khushi Rawat
8a5fa64e1d Merge pull request #54504 from frappe/mergify/bp/version-16-hotfix/pr-53314
fix: skip budget validation when cancelling GL entries (backport #53314)
2026-04-24 17:39:05 +05:30
Smit Vora
aecf2c1c0e Merge pull request #54505 from frappe/mergify/bp/version-16-hotfix/pr-54476
fix: ensure tax withholding entries respect date range of category (backport #54476)
2026-04-24 13:39:54 +05:30
ljain112
719d982a07 fix: ensure tax withholding entries respect date range of category
(cherry picked from commit 9ead8d4e3f)
2026-04-24 07:48:53 +00:00
nareshkannasln
1b146738c4 fix: skip BudgetValidation when cancelling GL entries
(cherry picked from commit fa34ebea94)
2026-04-24 06:45:16 +00:00
mergify[bot]
c4010b0581 ci: fix timezone for python mariadb tests (backport #54464) (#54465)
ci: fix timezone for python mariadb tests (#54464)

(cherry picked from commit 0d2da6d86c)

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-23 22:31:16 +00:00
mergify[bot]
456e99b352 fix: preserve inventory dimensions when raw materials are reset (backport #54440) (#54493)
fix: preserve inventory dimensions when raw materials are reset (#54440)

* fix: preserve inventory dimensions when raw materials are reset

* test: add test case

(cherry picked from commit 0e20e35842)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-23 17:37:04 +00:00
mergify[bot]
2a244d162b fix(edi): restrict Code List imports to files and trusted backend URLs (backport #54137) (#54266)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(edi): restrict Code List imports to files and trusted backend URLs (#54137)
fix(edi): hardcode "Code List" DocType in importer (#54488)
2026-04-23 15:36:35 +00:00
mergify[bot]
ddc9ea16cc ci: test correctness pattern (backport #54186) (#54473)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-04-23 13:29:15 +00:00
mergify[bot]
f7b87ed0e3 fix(stock): show available qty in warehouse link field (backport #54474) (#54484)
fix(stock): show available qty in warehouse link field (#54474)

(cherry picked from commit ab19b16fe2)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-04-23 18:19:28 +05:30
Smit Vora
01b22254e7 Merge pull request #54486 from frappe/mergify/bp/version-16-hotfix/pr-53190 2026-04-23 17:54:28 +05:30
Smit Vora
df3fbeded2 feat: Add XLSX styling support to custom financial report templates (backport #52612) (#54485)
Co-authored-by: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com>
2026-04-23 17:50:52 +05:30
Abdeali Chharchhodawala
96bab08ae0 feat: enhance account category with root type (#53190)
(cherry picked from commit f6639db0e9)
2026-04-23 12:05:07 +00:00
Abdeali Chharchhodawala
055ff56ce4 feat: Add XLSX styling support to custom financial report templates (#52612)
(cherry picked from commit c35221852a)
2026-04-23 11:46:01 +00:00
Smit Vora
4f8184ec70 Merge pull request #54477 from frappe/mergify/bp/version-16-hotfix/pr-54449 2026-04-23 15:18:46 +05:30
Smit Vora
c6d4802857 fix: add party type for dynamic link support
(cherry picked from commit b925469c4d)
2026-04-23 09:27:53 +00:00
Smit Vora
6392126ca5 refactor: make report extensible by regional apps
(cherry picked from commit f0ea20e579)
2026-04-23 09:27:53 +00:00
Smit Vora
40466be9ef test: None is better than zero, as no values exist
(cherry picked from commit b5550f747e)
2026-04-23 09:27:53 +00:00
Smit Vora
e6cfdb8e4d refactor: updated key for withholding_date
(cherry picked from commit 07b023a934)
2026-04-23 09:27:53 +00:00
Smit Vora
fffaf834fd refactor: better label for entity type
(cherry picked from commit 53666974a3)
2026-04-23 09:27:52 +00:00
Smit Vora
be0e58fb23 refactor: how data is built
(cherry picked from commit c3e7f7f02f)
2026-04-23 09:27:52 +00:00
Smit Vora
9276cd7343 refactor: use consistent report column names
(cherry picked from commit 6dca96b423)
2026-04-23 09:27:52 +00:00
rohitwaghchaure
dd2763aabc Merge pull request #54472 from frappe/mergify/bp/version-16-hotfix/pr-54471
fix: delivery schedule in the sales order (backport #54471)
2026-04-22 22:11:01 +05:30
Rohit Waghchaure
386f49978e fix: delivery schedule in the sales order
(cherry picked from commit 435db260ee)
2026-04-22 16:33:12 +00:00
mergify[bot]
090aab33fb fix: py error on stock ageing report (backport #54467) (#54469)
fix: py error on stock ageing report (#54467)

(cherry picked from commit f5357c233d)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-22 14:41:13 +00:00
ruthra kumar
99bc2c174b Merge pull request #54463 from frappe/mergify/bp/version-16-hotfix/pr-54447
refactor(test): remove explicit sql delete calls (backport #54447)
2026-04-22 11:34:10 +05:30
ruthra kumar
91a748d9bf refactor(test): remove explicit sql delete calls
(cherry picked from commit b16dd3f2dd)
2026-04-22 05:43:48 +00:00
206 changed files with 9562 additions and 3842 deletions

View File

@@ -41,6 +41,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
@@ -56,6 +57,7 @@ jobs:
mysql:
image: mariadb:10.6
env:
TZ: 'Asia/Kolkata'
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306

View File

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

View File

@@ -5,8 +5,7 @@ frappe.ui.form.on("Account", {
setup: function (frm) {
frm.add_fetch("parent_account", "report_type", "report_type");
frm.add_fetch("parent_account", "root_type", "root_type");
},
onload: function (frm) {
frm.set_query("parent_account", function (doc) {
return {
filters: {
@@ -15,7 +14,18 @@ frappe.ui.form.on("Account", {
},
};
});
frm.set_query("account_category", function () {
if (!frm.doc.root_type) return;
return {
filters: {
root_type: ["in", [frm.doc.root_type, ""]],
},
};
});
},
refresh: function (frm) {
frm.toggle_display("account_name", frm.is_new());
@@ -58,12 +68,20 @@ frappe.ui.form.on("Account", {
}
}
},
account_type: function (frm) {
if (frm.doc.is_group == 0) {
frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
}
},
root_type: function (frm) {
if (frm.doc.account_category) {
frm.set_value("account_category", "");
}
},
add_toolbar_buttons: function (frm) {
frm.add_custom_button(
__("Chart of Accounts"),

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

@@ -7,6 +7,8 @@
"engine": "InnoDB",
"field_order": [
"account_category_name",
"root_type",
"column_break_qluu",
"description"
],
"fields": [
@@ -14,6 +16,7 @@
"fieldname": "account_category_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account Category Name",
"reqd": 1,
"unique": 1
@@ -22,6 +25,18 @@
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "column_break_qluu",
"fieldtype": "Column Break"
},
{
"fieldname": "root_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity"
}
],
"grid_page_length": 50,
@@ -32,7 +47,7 @@
"link_fieldname": "account_category"
}
],
"modified": "2026-02-23 01:19:49.589393",
"modified": "2026-03-05 06:49:34.430723",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Category",
@@ -69,7 +84,7 @@
}
],
"row_format": "Dynamic",
"search_fields": "account_category_name, description",
"search_fields": "account_category_name, root_type",
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -21,6 +21,7 @@ class AccountCategory(Document):
account_category_name: DF.Data
description: DF.SmallText | None
root_type: DF.Literal["", "Asset", "Liability", "Income", "Expense", "Equity"]
# end: auto-generated types
def after_rename(self, old_name, new_name, merge):

View File

@@ -16,6 +16,7 @@
"invoicing_features_section",
"check_supplier_invoice_uniqueness",
"automatically_fetch_payment_terms",
"enable_subscription",
"column_break_17",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",

View File

@@ -77,6 +77,7 @@ class AccountsSettings(Document):
enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_party_matching: DF.Check
enable_subscription: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_payment_schedule_in_payment_request: DF.Check
fetch_valuation_rate_for_internal_transaction: DF.Check
@@ -142,6 +143,10 @@ class AccountsSettings(Document):
toggle_loyalty_point_program_section(not self.enable_loyalty_point_program)
clear_cache = True
if old_doc.enable_subscription != self.enable_subscription:
toggle_subscription_sections(not self.enable_subscription)
clear_cache = True
if clear_cache:
frappe.clear_cache()
@@ -234,6 +239,12 @@ def toggle_loyalty_point_program_section(hide):
create_property_setter_for_hiding_field(doctype, "loyalty_points_redemption", hide)
def toggle_subscription_sections(hide):
subscription_doctypes = frappe.get_hooks("subscription_doctypes")
for doctype in subscription_doctypes:
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,

View File

@@ -6,7 +6,7 @@ import json
import math
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from functools import reduce
from functools import cache, reduce
from typing import Any, Union
import frappe
@@ -15,6 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
from frappe.query_builder import Case
from frappe.query_builder.functions import Sum
from frappe.utils import cstr, date_diff, flt, getdate
from frappe.utils.xlsxutils import XLSXMetadata, XLSXStyleBuilder
from pypika.terms import Bracket, LiteralValue
from erpnext import get_company_currency
@@ -38,6 +39,9 @@ from erpnext.accounts.report.financial_statements import (
)
from erpnext.accounts.utils import get_children, get_currency_precision
DEFAULT_BULLET_PREFIX = ""
SEGMENT_PREFIX = "seg_"
# ============================================================================
# DATA MODELS
# ============================================================================
@@ -141,7 +145,7 @@ class SegmentData:
@property
def id(self) -> str:
return f"seg_{self.index}"
return f"{SEGMENT_PREFIX}{self.index}"
@dataclass
@@ -222,14 +226,38 @@ class FinancialReportEngine:
return context.get_result()
def _validate_filters(self, filters: dict[str, Any]) -> None:
required_filters = ["report_template", "period_start_date", "period_end_date"]
filter_labels = {
"report_template": _("Report Template"),
"filter_based_on": _("Filter Based On"),
"period_start_date": _("Start Date"),
"period_end_date": _("End Date"),
"from_fiscal_year": _("Start Year"),
"to_fiscal_year": _("End Year"),
}
required_filters_by_basis = {
"Date Range": ("period_start_date", "period_end_date"),
"Fiscal Year": ("from_fiscal_year", "to_fiscal_year"),
}
required_filters = ["report_template", "filter_based_on"]
required_filters.extend(required_filters_by_basis.get(filters.get("filter_based_on"), ()))
for filter_key in required_filters:
if not filters.get(filter_key):
frappe.throw(_("Missing required filter: {0}").format(filter_key))
frappe.throw(
title=_("Missing Required Filter"),
msg=_("Missing required filter: {0}").format(
frappe.bold(filter_labels.get(filter_key, filter_key))
),
)
if filters.get("presentation_currency"):
frappe.msgprint(_("Currency filters are currently unsupported in Custom Financial Report."))
frappe.msgprint(
title=_("Unsupported Feature"),
msg=_("Currency filters are currently unsupported in Custom Financial Report."),
indicator="orange",
)
# Margin view is dependent on first row being an income account. Hence not supported.
# Way to implement this would be using calculated rows with formulas.
@@ -464,6 +492,7 @@ class FinancialQueryBuilder:
self.periods = periods
self.company = filters.get("company")
self.account_meta = {} # {name: {account_name, account_number}}
self.ignore_opening_entries = False
def fetch_account_balances(self, accounts: list[dict]) -> dict[str, AccountData]:
"""
@@ -501,6 +530,8 @@ class FinancialQueryBuilder:
"""
Return opening balances for *all accounts* defaulting to zero.
"""
self.ignore_opening_entries = False
if frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance"):
return self._get_opening_balances_from_gl(accounts)
@@ -520,9 +551,9 @@ class FinancialQueryBuilder:
if last_closing_voucher:
closing_voucher = last_closing_voucher[0]
closing_data = self._get_closing_balances(accounts, closing_voucher.name)
self.ignore_opening_entries = True # Else it will double count
if sum(closing_data.values()) != 0.0:
return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date)
return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date)
return self._get_opening_balances_from_gl(accounts)
@@ -616,7 +647,12 @@ class FinancialQueryBuilder:
.groupby(gl_table.account)
)
if not frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting"):
ignore_is_opening = frappe.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
if self.ignore_opening_entries and not ignore_is_opening:
# This filter here applies to all accounts (BS & PL)
# However, in legacy query, this filter only applies to BS accounts
query = query.where(gl_table.is_opening == "No")
# Add period-specific columns
@@ -680,11 +716,18 @@ class FinancialQueryBuilder:
account_data.unaccumulate_values()
def _apply_standard_filters(self, query, table, doctype: str = "GL Entry"):
if self.filters.get("ignore_closing_entries"):
if doctype == "GL Entry":
query = query.where(table.voucher_type != "Period Closing Voucher")
else:
query = query.where(table.is_period_closing_voucher_entry == 0)
# Exclude PCV-generated entries except those posted to a closing-account-head
# so BS retained earnings survive while P&L reversal entries are filtered out
pcv = frappe.qb.DocType("Period Closing Voucher")
closing_heads = frappe.qb.from_(pcv).select(pcv.closing_account_head).where(pcv.docstatus == 1)
if doctype == "GL Entry":
is_pcv = table.voucher_type == "Period Closing Voucher"
else:
# Account Closing Balance
is_pcv = table.is_period_closing_voucher_entry == 1
query = query.where(~is_pcv | table.account.isin(closing_heads))
if self.filters.get("project"):
projects = self.filters.get("project")
@@ -1392,7 +1435,8 @@ class FormattingEngine:
condition=lambda rd: getattr(rd.row, "italic_text", False), format_properties={"italic": True}
),
FormattingRule(
condition=lambda rd: rd.is_detail_row, format_properties={"is_detail": True, "prefix": ""}
condition=lambda rd: rd.is_detail_row,
format_properties={"is_detail": True, "prefix": DEFAULT_BULLET_PREFIX},
),
FormattingRule(
condition=lambda rd: getattr(rd.row, "warn_if_negative", False),
@@ -1838,3 +1882,124 @@ class GrowthViewTransformer:
return 0.0
else:
return flt(((current_value - previous_value) / abs(previous_value)) * 100, 2)
# ============================================================================
# XLSX EXPORT STYLING
# ============================================================================
def get_xlsx_styles(metadata: XLSXMetadata) -> dict | None:
"""
Generate XLSX styles for financial report templates.
NOTE: Currently only custom report generated with "Report Template" filter will have styles applied.
"""
# skip styling
if not metadata.filters.get("report_template"):
return
builder = XLSXStyleBuilder(metadata, default_styling=False)
builder.apply_default_styles(currency_formatting=False)
# currency is fixed for all columns (only if report template filter is applied)
currency = get_company_currency(metadata.filters.get("company"))
styles = {
"bold": builder.register_style({"bold": True}),
"italic": builder.register_style({"italic": True}),
"warning": builder.register_style({"font_color": "#dc3545"}), # text-danger
}
fieldtype_formats = {
"Int": builder.register_style({"num_format": "General"}),
"Float": builder.register_style({"num_format": builder.get_number_format("Float")}),
"Percent": builder.register_style({"num_format": builder.get_number_format("Percent")}),
"Currency": builder.register_style({"num_format": builder.get_number_format("Currency", currency)}),
}
# quick access for hot loop
style_cell = builder.style_cell
@cache
def get_color_style(color: str) -> int:
return builder.register_style({"font_color": color})
@cache
def get_prefix_style(prefix: str) -> int:
prefix = f"{prefix or DEFAULT_BULLET_PREFIX}@"
return builder.register_style({"num_format": prefix})
@cache
def get_indent_style(indent: int) -> int:
return builder.register_style({"align": "left", "indent": indent})
# column level styling of currency columns
for col_idx, col in metadata.column_map.items():
if col.get("fieldtype") != "Currency":
continue
builder.style_column(col_idx, fieldtype_formats["Currency"])
# cell level styling
for row_idx, row in metadata.row_map.items():
# skip total row
if metadata.has_total_row and row_idx == builder.last_row_index:
continue
is_segmented = (row.get("_segment_info", {}).get("total_segments", 1) or 1) > 1
segment_values = row.get("segment_values", {}) or {}
for col_idx, col in metadata.column_map.items():
fieldname = col.get("fieldname")
is_account = fieldname == "account"
# determine formatting bucket
if is_segmented and fieldname.startswith(SEGMENT_PREFIX):
formatting = row.copy()
_, seg_idx, seg_fieldname = fieldname.split("_", 2)
is_account = seg_fieldname == "account"
formatting.update(segment_values.get(f"{SEGMENT_PREFIX}{seg_idx}", {}) or {})
else:
formatting = row # default formatting bucket.
if not is_account and formatting.get("is_blank_line"):
continue
col_fieldtype = col.get("fieldtype")
cell_fieldtype = formatting.get("fieldtype") or col_fieldtype
cell_value = row.get(fieldname)
if cell_value in (None, ""):
continue
# account column and other fieldtype styling
if is_account:
if formatting.get("is_detail") or (prefix := formatting.get("prefix")):
style_cell(row_idx, col_idx, get_prefix_style(prefix))
# custom indentation (different segment might have different indentation levels)
if is_segmented and (indent := formatting.get("indent")) and indent > 0:
style_cell(row_idx, col_idx, get_indent_style(indent))
else:
if col_fieldtype != cell_fieldtype and cell_fieldtype in fieldtype_formats:
style_cell(row_idx, col_idx, fieldtype_formats[cell_fieldtype])
# text styles
for style_key in ("bold", "italic"):
if formatting.get(style_key):
style_cell(row_idx, col_idx, styles[style_key])
# color styles
if (
formatting.get("warn_if_negative")
and cell_fieldtype in frappe.model.numeric_fieldtypes
and flt(cell_value) < 0
):
style_cell(row_idx, col_idx, styles["warning"])
elif color := formatting.get("color"):
style_cell(row_idx, col_idx, get_color_style(color))
return builder.result

View File

@@ -10,6 +10,7 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_engine
DependencyResolver,
FilterExpressionParser,
FinancialQueryBuilder,
FinancialReportEngine,
FormulaCalculator,
PeriodValue,
)
@@ -1952,6 +1953,159 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
jv_2023.cancel()
def test_opening_entries_roll_into_opening_after_period_closing(self):
"""
Sequence:
1. is_opening JV of 3000 in current year (FY 2024)
2. is_opening JV of 5000 in next year (FY 2025)
3. Period Closing Voucher for previous year (FY 2023)
Expected (BS report for FY 2024):
opening of FY 2024 = 3000 + 5000 = 8000
(all is_opening entries roll into opening irrespective of fiscal year,
on top of the PCV carry-forward — here PCV closing for cash is 0).
"""
company = "_Test Company"
cash_account = "_Test Cash - _TC"
# Opening JVs cannot post against P&L accounts; use a Balance Sheet offset.
opening_offset_account = "Temporary Opening - _TC"
pcv = None
jv_current_year = None
jv_next_year = None
original_pcv_setting = frappe.db.get_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv"
)
try:
# Step 1: opening JV in current year (FY 2024) — must be posted before PCV
# exists, else `validate_against_pcv` rejects it.
jv_current_year = make_journal_entry(
account1=cash_account,
account2=opening_offset_account,
amount=3000,
posting_date="2024-06-15",
company=company,
save=False,
)
jv_current_year.is_opening = "Yes"
jv_current_year.insert()
jv_current_year.submit()
# Step 2: opening JV in next year (FY 2025)
jv_next_year = make_journal_entry(
account1=cash_account,
account2=opening_offset_account,
amount=5000,
posting_date="2025-06-15",
company=company,
save=False,
)
jv_next_year.is_opening = "Yes"
jv_next_year.insert()
jv_next_year.submit()
# Step 3: book Period Closing Voucher for previous year (FY 2023)
closing_account = frappe.db.get_value(
"Account",
{
"company": company,
"root_type": "Liability",
"is_group": 0,
"account_type": ["not in", ["Payable", "Receivable"]],
},
"name",
)
fy_2023 = get_fiscal_year("2023-06-15", company=company)
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
pcv = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": "2023-12-31",
"period_start_date": fy_2023[1],
"period_end_date": fy_2023[2],
"company": company,
"fiscal_year": fy_2023[0],
"cost_center": "_Test Cost Center - _TC",
"closing_account_head": closing_account,
"remarks": "Test Period Closing",
}
)
pcv.insert()
pcv.submit()
pcv.reload()
# Run BS report for FY 2024
filters = {
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-01-01",
"period_end_date": "2024-12-31",
"filter_based_on": "Date Range",
"periodicity": "Yearly",
"ignore_closing_entries": True,
}
periods = [{"key": "2024", "from_date": "2024-01-01", "to_date": "2024-12-31"}]
query_builder = FinancialQueryBuilder(filters, periods)
accounts = [
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
frappe._dict(
{
"name": opening_offset_account,
"account_name": "Temporary Opening",
"account_number": "1900",
}
),
]
balances_data = query_builder.fetch_account_balances(accounts)
cash_data = balances_data.get(cash_account)
offset_data = balances_data.get(opening_offset_account)
self.assertIsNotNone(cash_data, "Cash account should exist in results")
self.assertIsNotNone(offset_data, "Offset account should exist in results")
year_2024_cash = cash_data.get_period("2024")
year_2024_offset = offset_data.get_period("2024")
self.assertIsNotNone(year_2024_cash, "FY 2024 period should exist for cash")
self.assertIsNotNone(year_2024_offset, "FY 2024 period should exist for offset")
# All is_opening JVs (current + next year) roll into FY 2024 opening
self.assertEqual(
year_2024_cash.opening,
8000.0,
"FY 2024 cash opening must combine is_opening JVs from current and next year",
)
self.assertEqual(
year_2024_offset.opening,
-8000.0,
"FY 2024 offset opening must combine is_opening JVs from current and next year",
)
self.assertEqual(
year_2024_cash.movement, 0.0, "Opening JVs must not be counted as period movement"
)
self.assertEqual(year_2024_cash.closing, 8000.0, "Closing = opening when no non-opening movement")
finally:
frappe.db.set_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
)
if pcv:
pcv.reload()
if pcv.docstatus == 1:
pcv.cancel()
if jv_next_year and jv_next_year.docstatus == 1:
jv_next_year.cancel()
if jv_current_year and jv_current_year.docstatus == 1:
jv_current_year.cancel()
def test_account_with_gl_entries_but_no_prior_closing_balance(self):
company = "_Test Company"
cash_account = "_Test Cash - _TC"
@@ -2025,3 +2179,210 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
finally:
jv.cancel()
def test_pl_pcv_exclusion_and_growth_view_year_over_year(self):
"""
Sequence:
1. Expense JV 2000 in FY 2024, PCV for FY 2024
→ assert FY 2024 movement = 2000 via FinancialQueryBuilder
2. Expense JV 3000 in FY 2025, PCV for FY 2025
3. Run FinancialReportEngine with selected_view="Growth"
→ assert col_2024 = 2000 (raw), col_2025 = 50.0 (% growth)
"""
company = "_Test Company"
expense_account = "Administrative Expenses - _TC"
bank_account = "_Test Bank - _TC"
template = None
pcv_2024 = None
pcv_2025 = None
jv_2024 = None
jv_2025 = None
original_pcv_setting = frappe.db.get_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv"
)
try:
closing_account = frappe.db.get_value(
"Account",
{
"company": company,
"root_type": "Liability",
"is_group": 0,
"account_type": ["not in", ["Payable", "Receivable"]],
},
"name",
)
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
accounts = [
frappe._dict(
{
"name": expense_account,
"account_name": "Administrative Expenses",
"account_number": "5001",
}
),
]
# --- Step 1: FY 2024 expense + PCV, assert PCV reversal excluded ---
jv_2024 = make_journal_entry(
account1=expense_account,
account2=bank_account,
amount=2000,
posting_date="2024-06-15",
company=company,
submit=True,
)
fy_2024 = get_fiscal_year("2024-06-15", company=company)
pcv_2024 = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": "2024-12-31",
"period_start_date": fy_2024[1],
"period_end_date": fy_2024[2],
"company": company,
"fiscal_year": fy_2024[0],
"cost_center": "_Test Cost Center - _TC",
"closing_account_head": closing_account,
"remarks": "Test PCV FY 2024",
}
)
pcv_2024.insert()
pcv_2024.submit()
pcv_2024.reload()
builder_2024 = FinancialQueryBuilder(
{
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-01-01",
"period_end_date": "2024-12-31",
"filter_based_on": "Date Range",
"periodicity": "Yearly",
},
[{"key": "2024", "from_date": "2024-01-01", "to_date": "2024-12-31"}],
)
data_2024 = builder_2024.fetch_account_balances(accounts)
expense_2024 = data_2024.get(expense_account)
self.assertIsNotNone(expense_2024, "Expense account must appear in FY 2024 results")
year_2024 = expense_2024.get_period("2024")
self.assertEqual(
year_2024.movement,
2000.0,
"FY 2024 expense movement must equal real expense (PCV reversal excluded)",
)
# --- Step 2: FY 2025 expense + PCV ---
jv_2025 = make_journal_entry(
account1=expense_account,
account2=bank_account,
amount=3000,
posting_date="2025-06-15",
company=company,
submit=True,
)
fy_2025 = get_fiscal_year("2025-06-15", company=company)
pcv_2025 = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": "2025-12-31",
"period_start_date": fy_2025[1],
"period_end_date": fy_2025[2],
"company": company,
"fiscal_year": fy_2025[0],
"cost_center": "_Test Cost Center - _TC",
"closing_account_head": closing_account,
"remarks": "Test PCV FY 2025",
}
)
pcv_2025.insert()
pcv_2025.submit()
pcv_2025.reload()
# --- Step 3: full pipeline with Growth view across both years ---
template_name = f"Test Growth Template {frappe.generate_hash()[:8]}"
template = frappe.get_doc(
{
"doctype": "Financial Report Template",
"template_name": template_name,
"report_type": "Profit and Loss Statement",
"rows": [
{
"reference_code": "EXP_ADMIN",
"display_name": "Administrative Expenses",
"indentation_level": 0,
"data_source": "Account Data",
"balance_type": "Closing Balance",
"calculation_formula": f'["name", "=", "{expense_account}"]',
},
],
}
)
template.insert()
filters = frappe._dict(
{
"company": company,
"report_template": template_name,
"from_fiscal_year": fy_2024[0],
"to_fiscal_year": fy_2025[0],
"period_start_date": "2024-01-01",
"period_end_date": "2025-12-31",
"filter_based_on": "Date Range",
"periodicity": "Yearly",
"accumulated_values": 0,
"selected_view": "Growth",
}
)
_columns, formatted_data, _msg, _chart = FinancialReportEngine().execute(filters)
expense_row = next(
(row for row in formatted_data if row.get("account_name") == "Administrative Expenses"),
None,
)
self.assertIsNotNone(expense_row, "Administrative Expenses row must appear in growth view")
period_keys = expense_row.get("_segment_info", {}).get("period_keys", [])
self.assertEqual(len(period_keys), 2, "Yearly view must yield exactly two periods")
first_period_key, second_period_key = period_keys
# First column: raw absolute value (FY 2024 expense)
self.assertEqual(
flt(expense_row[first_period_key]),
2000.0,
"First column in growth view must keep raw FY 2024 expense value",
)
# Second column: ((3000 - 2000) / 2000) * 100 = 50.0
self.assertEqual(
flt(expense_row[second_period_key]),
50.0,
"Second column must be % growth FY 2024 → FY 2025",
)
finally:
frappe.db.set_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
)
if pcv_2025:
pcv_2025.reload()
if pcv_2025.docstatus == 1:
pcv_2025.cancel()
if jv_2025 and jv_2025.docstatus == 1:
jv_2025.cancel()
if pcv_2024:
pcv_2024.reload()
if pcv_2024.docstatus == 1:
pcv_2024.cancel()
if jv_2024 and jv_2024.docstatus == 1:
jv_2024.cancel()
if template and frappe.db.exists("Financial Report Template", template.name):
frappe.delete_doc("Financial Report Template", template.name, force=1)

View File

@@ -39,7 +39,7 @@
"clearance_date",
"column_break_oizh",
"user_remark",
"subscription_section",
"auto_repeat_section",
"auto_repeat",
"tax_withholding_tab",
"section_tax_withholding_entry",
@@ -477,11 +477,6 @@
"options": "Stock Entry",
"read_only": 1
},
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription"
},
{
"allow_on_submit": 1,
"fieldname": "auto_repeat",

View File

@@ -89,6 +89,7 @@
"remarks",
"base_in_words",
"is_opening",
"title",
"column_break_16",
"letter_head",
"print_heading",
@@ -96,10 +97,9 @@
"bank_account_no",
"payment_order",
"in_words",
"subscription_section",
"auto_repeat",
"amended_from",
"title"
"auto_repeat_section",
"auto_repeat"
],
"fields": [
{
@@ -503,11 +503,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
},
{
"allow_on_submit": 1,
"fieldname": "auto_repeat",
@@ -781,6 +776,11 @@
"fieldname": "override_tax_withholding_entries",
"fieldtype": "Check",
"label": "Edit Tax Withholding Entries"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
}
],
"grid_page_length": 50,

View File

@@ -2306,22 +2306,20 @@ def get_outstanding_reference_documents(args, validate=False):
# Get positive outstanding sales /purchase invoices
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
condition = " and voucher_type={} and voucher_no={}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
)
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
common_filter.append(ple.voucher_type == args["voucher_type"])
common_filter.append(ple.voucher_no == args["voucher_no"])
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = {
@@ -2330,18 +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 += " and {} between '{}' and '{}'".format(
fieldname, args.get(date_fields[0]), 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} >= '{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} <= '{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"):
@@ -2561,7 +2560,7 @@ def get_orders_to_be_billed(
active_dimensions = get_dimensions(True)[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"

View File

@@ -195,6 +195,30 @@ class TestPaymentEntry(ERPNextTestSuite):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
def test_reference_outstanding_amount_on_advance_pull(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order(qty=1, rate=1000)
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_amount = pe.received_amount = 500
pe.references[0].allocated_amount = 500
pe.insert()
pe.submit()
so.reload()
self.assertEqual(so.advance_paid, 500)
si = make_sales_invoice(so.name)
si.allocate_advances_automatically = 1
si.save()
self.assertEqual(si.get("advances")[0].allocated_amount, 500)
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
si.submit()
pe.load_from_db()
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(pe.references[0].outstanding_amount, si.outstanding_amount)
def test_payment_entry_against_pi(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",

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

@@ -183,7 +183,7 @@
"depends_on": "eval:doc.is_a_subscription",
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
"label": "Subscription"
},
{
"fieldname": "subscription_plans",
@@ -478,7 +478,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-01-13 12:53:00.963274",
"modified": "2026-02-27 19:11:03.308896",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",

View File

@@ -46,8 +46,8 @@ frappe.ui.form.on("Period Closing Voucher", {
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,

View File

@@ -18,9 +18,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
def test_closing_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
@@ -70,9 +67,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(pcv_gle, expected_gle)
def test_cost_center_wise_posting(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
@@ -136,9 +130,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
)
def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
@@ -190,9 +181,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertSequenceEqual(pcv_gle, expected_gle)
def test_gl_entries_restrictions(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
@@ -213,10 +201,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions_and_test_reposting_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
company = create_company()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")

View File

@@ -201,7 +201,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
)
from erpnext.stock.doctype.batch.batch import get_batch_qty
frappe.db.sql("delete from `tabPOS Invoice`")
item_doc = make_item(
"_Test Item With Batch FOR POS Merge Test",
properties={

View File

@@ -26,6 +26,8 @@
"due_date",
"amended_from",
"return_against",
"section_break_abck",
"title",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -187,7 +189,7 @@
"subscription_section",
"from_date",
"to_date",
"column_break_140",
"auto_repeat_section",
"auto_repeat",
"update_auto_repeat_reference",
"against_income_account"
@@ -662,6 +664,7 @@
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"label": "Total Billing Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -1462,7 +1465,7 @@
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
"label": "Subscription"
},
{
"allow_on_submit": 1,
@@ -1480,10 +1483,6 @@
"no_copy": 1,
"print_hide": 1
},
{
"fieldname": "column_break_140",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "auto_repeat",
@@ -1533,6 +1532,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1619,12 +1619,29 @@
{
"fieldname": "column_break_bhao",
"fieldtype": "Column Break"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_abck",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-02-10 14:23:07.181782",
"modified": "2026-05-01 02:37:30.580568",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -172,6 +172,7 @@ class POSInvoice(SalesInvoice):
terms: DF.TextEditor | None
territory: DF.Link | None
timesheets: DF.Table[SalesInvoiceTimesheet]
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency

View File

@@ -37,7 +37,6 @@ class POSInvoiceTestMixin(ERPNextTestSuite):
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice")
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
frappe.db.sql("delete from `tabTax Rule`")
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")

View File

@@ -34,7 +34,6 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
consolidate_pos_invoices,
)
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
@@ -64,7 +63,6 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
consolidate_pos_invoices,
)
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
@@ -123,7 +121,7 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
item = "Test Selling Price Validation"
make_item(item, {"is_stock_item": 1})
make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})

View File

@@ -17,7 +17,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPricingRule(ERPNextTestSuite):
def setUp(self):
delete_existing_pricing_rules()
setup_pricing_rule_data()
self.enterClassContext(self.change_settings("Selling Settings", validate_selling_price=0))
@@ -1586,16 +1585,6 @@ def setup_pricing_rule_data():
).insert()
def delete_existing_pricing_rules():
for doctype in [
"Pricing Rule",
"Pricing Rule Item Code",
"Pricing Rule Item Group",
"Pricing Rule Brand",
]:
frappe.db.sql(f"delete from `tab{doctype}`")
def make_item_price(item, price_list_name, item_price):
frappe.get_doc(
{

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

@@ -27,6 +27,8 @@
"update_billed_amount_in_purchase_receipt",
"apply_tds",
"amended_from",
"section_break_hzux",
"title",
"supplier_invoice_details",
"bill_no",
"column_break_15",
@@ -180,11 +182,12 @@
"unrealized_profit_loss_account",
"subscription_section",
"subscription",
"auto_repeat",
"update_auto_repeat_reference",
"column_break_114",
"from_date",
"to_date",
"automation_section",
"auto_repeat",
"update_auto_repeat_reference",
"printing_settings",
"letter_head",
"group_same_items",
@@ -1675,6 +1678,24 @@
"fieldname": "totals_section",
"fieldtype": "Section Break",
"label": "Totals"
},
{
"collapsible": 1,
"fieldname": "automation_section",
"fieldtype": "Section Break",
"label": "Automation"
},
{
"fieldname": "section_break_hzux",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -1682,7 +1703,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-03-25 11:45:38.696888",
"modified": "2026-04-28 07:15:31.062404",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -203,6 +203,7 @@ class PurchaseInvoice(BuyingController):
taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency

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

@@ -33,6 +33,8 @@
"is_created_using_pos",
"pos_closing_entry",
"has_subcontracted",
"section_break_qllv",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -91,9 +93,9 @@
"column_break_xjag",
"base_rounding_adjustment",
"base_rounded_total",
"section_break_vacb",
"section_break_pxwz",
"total_advance",
"column_break_rdks",
"column_break_iaso",
"outstanding_amount",
"section_tax_withholding_entry",
"tax_withholding_group",
@@ -199,6 +201,7 @@
"unrealized_profit_loss_account",
"against_income_account",
"commission_section",
"column_break_rdiw",
"sales_partner",
"amount_eligible_for_commission",
"column_break10",
@@ -214,12 +217,14 @@
"language",
"subscription_section",
"subscription",
"from_date",
"auto_repeat",
"column_break_140",
"from_date",
"to_date",
"automation_section",
"auto_repeat",
"update_auto_repeat_reference",
"utm_analytics_section",
"column_break_rdke",
"utm_source",
"utm_medium",
"column_break_ixxw",
@@ -1147,6 +1152,7 @@
"hide_seconds": 1,
"label": "Rounding Adjustment",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1159,6 +1165,7 @@
"label": "Rounded Total",
"oldfieldname": "rounded_total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -2304,14 +2311,6 @@
"options": "fa fa-group",
"print_hide": 1
},
{
"fieldname": "section_break_vacb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdks",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ixxw",
"fieldtype": "Column Break"
@@ -2321,6 +2320,40 @@
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "UTM Analytics"
},
{
"collapsible": 1,
"fieldname": "automation_section",
"fieldtype": "Section Break",
"label": "Automation"
},
{
"fieldname": "section_break_pxwz",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdke",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_rdiw",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_iaso",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_qllv",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -2334,7 +2367,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-03-09 17:15:30.931929",
"modified": "2026-05-01 02:37:29.742764",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -225,6 +225,7 @@ class SalesInvoice(SellingController):
terms: DF.TextEditor | None
territory: DF.Link | None
timesheets: DF.Table[SalesInvoiceTimesheet]
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency

View File

@@ -2025,10 +2025,6 @@ class TestSalesInvoice(ERPNextTestSuite):
)
def test_multiple_uom_in_selling(self):
frappe.db.sql(
"""delete from `tabItem Price`
where price_list='_Test Price List' and item_code='_Test Item'"""
)
item_price = frappe.new_doc("Item Price")
item_price.price_list = "_Test Price List"
item_price.item_code = "_Test Item"

View File

@@ -10,8 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestShareTransfer(ERPNextTestSuite):
def setUp(self):
frappe.db.sql("delete from `tabShare Transfer`")
frappe.db.sql("delete from `tabShare Balance`")
share_transfers = [
{
"doctype": "Share Transfer",

View File

@@ -5,8 +5,7 @@ import datetime
from unittest.mock import patch
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils import add_days, add_months, today
from frappe.utils import add_days, add_months, getdate, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.utils import get_fiscal_year
@@ -1922,7 +1921,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
def set_previous_fy_and_tax_category(self):
test_company = "_Test Company"
category = "Cumulative Threshold TDS"
def add_company_to_fy(fy, company):
if not [x.company for x in fy.companies if x.company == company]:
@@ -1948,20 +1946,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
)
self.prev_fy.save()
# setup tax withholding category for previous fiscal year
cat = frappe.get_doc("Tax Withholding Category", category)
cat.append(
"rates",
{
"from_date": self.prev_fy.year_start_date,
"to_date": self.prev_fy.year_end_date,
"tax_withholding_rate": 10,
"single_threshold": 0,
"cumulative_threshold": 30000,
},
)
cat.save()
def test_tds_across_fiscal_year(self):
"""
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
@@ -1972,6 +1956,14 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
supplier = "Test TDS Supplier"
# Cumulative threshold 30000 and tax rate 10%
category = "Cumulative Threshold TDS"
create_tax_withholding_category(
category_name=category,
rate=10,
from_date=self.prev_fy.year_start_date,
to_date=self.prev_fy.year_end_date,
account="TDS - _TC",
cumulative_threshold=30000,
)
frappe.db.set_value(
"Supplier",
supplier,
@@ -2043,6 +2035,158 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
self.assertEqual(pi2.taxes, [])
self.assertEqual(payment.taxes[0].tax_amount, 6000)
def test_threshold_resets_in_new_fiscal_year(self):
"""
Threshold entries from a previous FY must not carry over into the new FY.
"""
self.set_previous_fy_and_tax_category()
invoices = []
supplier = "Test TDS Supplier"
category = "Cumulative Threshold TDS"
create_tax_withholding_category(
category_name=category,
rate=10,
from_date=self.prev_fy.year_start_date,
to_date=self.prev_fy.year_end_date,
account="TDS - _TC",
cumulative_threshold=30000,
)
self.setup_party_with_category("Supplier", supplier, category)
prev_fy_date = add_days(self.prev_fy.year_end_date, -10)
# Previous FY: 3 invoices to cross the 30000 cumulative threshold
for _ in range(3):
pi = create_purchase_invoice(supplier=supplier, posting_date=prev_fy_date, set_posting_time=True)
pi.submit()
invoices.append(pi)
# Third invoice crosses the threshold - 3000 TDS deducted across all three
self.validate_tax_deduction(invoices[-1], 3000)
# Current FY: 10000 invoice - must be Under Withheld, threshold resets
pi_curr = create_purchase_invoice(supplier=supplier)
pi_curr.submit()
invoices.append(pi_curr)
self.validate_tax_deduction(pi_curr, 0)
self.validate_tax_withholding_entries(
"Purchase Invoice",
pi_curr.name,
[
self.get_tax_withholding_entry(
tax_withholding_category=category,
party_type="Supplier",
party=supplier,
taxable_doctype="Purchase Invoice",
taxable_name=pi_curr.name,
tax_rate=10.0,
taxable_amount=10000.0,
withholding_amount=0.0,
status="Under Withheld",
withholding_doctype=None,
withholding_name=None,
under_withheld_reason=None,
)
],
)
self.cleanup_invoices(invoices)
def test_tax_on_excess_threshold_resets_in_new_fiscal_year(self):
"""
For tax-on-excess categories, unused threshold must reset each FY.
"""
self.set_previous_fy_and_tax_category()
invoices = []
supplier = "Test TDS Supplier3"
category = "New TDS Category"
create_tax_withholding_category(
category_name=category,
rate=10,
from_date=self.prev_fy.year_start_date,
to_date=self.prev_fy.year_end_date,
account="TDS - _TC",
cumulative_threshold=30000,
tax_on_excess_amount=1,
round_off_tax_amount=1,
)
self.setup_party_with_category("Supplier", supplier, category)
prev_fy_date = add_days(self.prev_fy.year_end_date, -10)
for _ in range(2):
pi = create_purchase_invoice(supplier=supplier, posting_date=prev_fy_date, set_posting_time=True)
pi.submit()
invoices.append(pi)
pi3 = create_purchase_invoice(
supplier=supplier, rate=20000, posting_date=prev_fy_date, set_posting_time=True
)
pi3.submit()
invoices.append(pi3)
self.validate_tax_deduction(pi3, 1000)
self.validate_tax_withholding_entries(
"Purchase Invoice",
pi3.name,
[
self.get_tax_withholding_entry(
tax_withholding_category=category,
party_type="Supplier",
party=supplier,
taxable_doctype="Purchase Invoice",
taxable_name=pi3.name,
tax_rate=10.0,
taxable_amount=10000.0,
withholding_amount=0.0,
status="Settled",
withholding_doctype="Purchase Invoice",
withholding_name=pi3.name,
under_withheld_reason="Threshold Exemption",
),
self.get_tax_withholding_entry(
tax_withholding_category=category,
party_type="Supplier",
party=supplier,
taxable_doctype="Purchase Invoice",
taxable_name=pi3.name,
tax_rate=10.0,
taxable_amount=10000.0,
withholding_amount=1000.0,
status="Settled",
withholding_doctype="Purchase Invoice",
withholding_name=pi3.name,
under_withheld_reason=None,
),
],
)
# no excess, so no TDS
pi_curr = create_purchase_invoice(supplier=supplier, rate=30000)
pi_curr.submit()
invoices.append(pi_curr)
self.validate_tax_deduction(pi_curr, 0)
self.validate_tax_withholding_entries(
"Purchase Invoice",
pi_curr.name,
[
self.get_tax_withholding_entry(
tax_withholding_category=category,
party_type="Supplier",
party=supplier,
taxable_doctype="Purchase Invoice",
taxable_name=pi_curr.name,
tax_rate=10.0,
taxable_amount=30000.0,
withholding_amount=0.0,
status="Settled",
withholding_doctype="Purchase Invoice",
withholding_name=pi_curr.name,
under_withheld_reason="Threshold Exemption",
),
],
)
self.cleanup_invoices(invoices)
@ERPNextTestSuite.change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
def test_tds_payment_entry_cancellation(self):
"""
@@ -3997,7 +4141,7 @@ def create_tax_withholding_category(
tax_deduction_basis="Net Total",
):
if not frappe.db.exists("Tax Withholding Category", category_name):
frappe.get_doc(
doc = frappe.get_doc(
{
"doctype": "Tax Withholding Category",
"name": category_name,
@@ -4018,6 +4162,22 @@ def create_tax_withholding_category(
"accounts": [{"company": "_Test Company", "account": account}],
}
).insert()
else:
doc = frappe.get_doc("Tax Withholding Category", category_name)
if not any(getdate(r.from_date) == getdate(from_date) for r in doc.rates):
doc.append(
"rates",
{
"from_date": from_date,
"to_date": to_date,
"tax_withholding_rate": rate,
"single_threshold": single_threshold,
"cumulative_threshold": cumulative_threshold,
},
)
doc.save()
return doc
def create_lower_deduction_certificate(

View File

@@ -640,6 +640,7 @@ class TaxWithholdingController:
.where(entry.tax_withholding_category == category.name)
.where(entry.company == self.doc.company)
.where(entry.docstatus == 1)
.where(entry.taxable_date.between(category.from_date, category.to_date))
.groupby(entry.status)
)

View File

@@ -1,118 +1,147 @@
[
{
"account_category_name": "Cash and Cash Equivalents",
"root_type": "Asset",
"description": "Cash on hand, demand deposits, and short-term highly liquid investments readily convertible to cash with original maturities of three months or less. Examples: Cash in hand, bank current accounts, money market funds, treasury bills \u22643 months."
},
{
"account_category_name": "Cost of Goods Sold",
"root_type": "Expense",
"description": "Direct costs attributable to cost of goods sold. Examples: Raw materials, stock in trade."
},
{
"account_category_name": "Current Tax Liabilities",
"root_type": "Liability",
"description": "Income tax obligations for current and prior periods. Examples: Provision for income tax, advance tax paid, tax deducted at source."
},
{
"account_category_name": "Finance Costs",
"root_type": "Expense",
"description": "Interest and financing-related expenses. Examples: Interest on borrowings, bank charges, lease interest, foreign exchange losses."
},
{
"account_category_name": "Intangible Assets",
"root_type": "Asset",
"description": "Identifiable non-monetary assets without physical substance. Examples: Software, patents, trademarks, licenses, development costs."
},
{
"account_category_name": "Investment Income",
"root_type": "Income",
"description": "Returns generated from financial investments and cash management. Examples: Interest income, dividend income, rental income, fair value gains."
},
{
"account_category_name": "Long-term Borrowings",
"root_type": "Liability",
"description": "Interest-bearing debt obligations with maturity beyond one year. Examples: Term loans, bonds, debentures, mortgages."
},
{
"account_category_name": "Long-term Investments",
"root_type": "Asset",
"description": "Investments held for strategic purposes or extended periods. Examples: Equity investments, bonds, associates, joint ventures, deposits."
},
{
"account_category_name": "Long-term Provisions",
"root_type": "Liability",
"description": "Present obligations beyond one year with uncertain timing/amount. Examples: Asset retirement obligations, environmental remediation, legal settlements."
},
{
"account_category_name": "Operating Expenses",
"root_type": "Expense",
"description": "Costs incurred in ordinary business operations excluding direct costs. Examples: Selling expenses, administrative costs, marketing, utilities, rent."
},
{
"account_category_name": "Other Current Assets",
"root_type": "Asset",
"description": "Current assets not classified elsewhere including prepaid expenses and advances. Examples: Prepaid insurance, prepaid rent, advance to suppliers, security deposits recoverable within one year."
},
{
"account_category_name": "Other Current Liabilities",
"root_type": "Liability",
"description": "Short-term obligations not classified elsewhere. Examples: Accrued expenses, statutory liabilities, employee payables."
},
{
"account_category_name": "Other Direct Costs",
"root_type": "Expense",
"description": "Direct costs excluding cost of goods sold. Examples: Direct labor, manufacturing overhead, freight inward."
},
{
"account_category_name": "Other Non-current Assets",
"root_type": "Asset",
"description": "Long-term assets not classified elsewhere. Examples: Security deposits, long-term prepayments, advances for capital goods."
},
{
"account_category_name": "Other Non-current Liabilities",
"root_type": "Liability",
"description": "Long-term obligations not classified elsewhere. Examples: Long-term deposits, deferred income, government grants."
},
{
"account_category_name": "Other Operating Income",
"root_type": "Income",
"description": "Incidental income related to business operations but not core revenue. Examples: Scrap sales, government grants, insurance claims, foreign exchange gains."
},
{
"account_category_name": "Other Payables",
"root_type": "Liability",
"description": "Non-trade payables and obligations to parties other than suppliers. Examples: Employee payables, accrued expenses, customer advances, security deposits received."
},
{
"account_category_name": "Other Receivables",
"root_type": "Asset",
"description": "Non-trade amounts due to the entity excluding financing arrangements. Examples: Employee advances, insurance claims, tax refunds, deposits recoverable."
},
{
"account_category_name": "Reserves and Surplus",
"root_type": "Equity",
"description": "Accumulated profits and other reserves created from profits or share premium. Examples: General reserves, retained earnings, statutory reserves, share premium."
},
{
"account_category_name": "Revenue from Operations",
"root_type": "Income",
"description": "Income from primary business activities in ordinary course. Examples: Sales of goods, service revenue, commission income, royalty income."
},
{
"account_category_name": "Share Capital",
"root_type": "Equity",
"description": "Nominal value of issued and paid-up equity shares. Examples: Common stock, ordinary shares, preference shares."
},
{
"account_category_name": "Short-term Borrowings",
"root_type": "Liability",
"description": "Interest-bearing debt obligations due within one year. Examples: Bank overdrafts, short-term loans, current portion of long-term debt."
},
{
"account_category_name": "Short-term Investments",
"root_type": "Asset",
"description": "Financial instruments held for short-term investment purposes, readily convertible to cash. Examples: Marketable securities, fixed deposits >3 months, mutual funds."
},
{
"account_category_name": "Short-term Provisions",
"root_type": "Liability",
"description": "Present obligations due within one year with uncertain timing or amount. Examples: Warranty provisions, legal claims, restructuring costs."
},
{
"account_category_name": "Stock Assets",
"root_type": "Asset",
"description": "Inventory and stock-related assets including raw materials, work in progress, finished goods, and stock in trade. Examples: Raw materials, finished goods, trading merchandise, consumables."
},
{
"account_category_name": "Tangible Assets",
"root_type": "Asset",
"description": "Physical assets used in business operations including property, plant, and equipment. Examples: Land, buildings, machinery, equipment, vehicles, furniture, capital work in progress."
},
{
"account_category_name": "Tax Expense",
"root_type": "Expense",
"description": "Current and deferred income tax obligations. Examples: Current tax provision, deferred tax expense, withholding taxes."
},
{
"account_category_name": "Trade Payables",
"root_type": "Liability",
"description": "Amounts owed to suppliers. Examples: Supplier invoices, accrued purchases, bills payable."
},
{
"account_category_name": "Trade Receivables",
"root_type": "Asset",
"description": "Amounts due from customers for goods sold or services provided in ordinary course of business. Examples: Accounts receivable, notes receivable from customers, unbilled revenue."
}
]
]

View File

@@ -36,7 +36,8 @@ def make_gl_entries(
):
if gl_map:
if (
not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
not cancel
and not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
and gl_map[0].voucher_type != "Period Closing Voucher"
):
bud_val = BudgetValidation(gl_map=gl_map)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountBalance(ERPNextTestSuite):
def test_account_balance(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
filters = {
"company": "_Test Company 2",
"report_date": getdate(),

View File

@@ -1 +1,227 @@
{% include "accounts/report/accounts_receivable/accounts_receivable.html" %}
<style type="text/css">
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table thead th {
background: #f8f8f8;
text-align: center;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.text-center { text-align: center; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<br>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Supplier") %}:</strong>
{%= (filters.party.length && filters.party.join(", ")) || __("All Parties") %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Report Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.report_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th style="width: 8em; text-align: left;">{%= __("Date") %}</th>
<th style="text-align: left;">{%= __("Reference") %}</th>
{% if(filters.show_remarks) { %}
<th style="text-align: left;">{%= __("Remarks") %}</th>
{% } %}
<th style="width: 10em; text-align: right;">{%= __("Age (Days)") %}</th>
<th style="width: 10em; text-align: right;">{%= __("Invoiced Amount") %}</th>
<th style="width: 11em; text-align: right;">{%= __("Outstanding Amount") %}</th>
</tr>
</thead>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>
<td class="text-left">{%= frappe.datetime.str_to_user(data[i]["posting_date"]) %}</td>
<td class="{% if(i == data.length - 1) { %}text-left text-bold{% } %}">
{% if(i == data.length - 1) { %}
{%= __("Total") %}
{% } else { %}
{%= data[i]["voucher_no"] %}
{% } %}
</td>
{% if(filters.show_remarks) { %}
<td class="text-left">
{% if(data[i]["remarks"] && data[i]["remarks"] != "No Remarks") { %}
{%= data[i]["remarks"] %}
{% } %}
</td>
{% } %}
<td class="text-right">{%= data[i]["age"] %}</td>
<td class="text-right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td class="text-right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
</tr>
{% } %}
</tbody>
</table>
</div>
&nbsp;
{% if(filters.show_future_payments) { %}
{%
var balance_row = data.slice(-1).pop();
var start = report.columns.findIndex(e => e.fieldname == 'age');
var currency = data[data.length - 1]["currency"];
var ranges = [
report.columns[start].label,
report.columns[start+1].label,
report.columns[start+2].label,
report.columns[start+3].label,
report.columns[start+4].label,
report.columns[start+5].label
];
%}
{% if(balance_row) { %}
<div class="report-table">
<table>
<thead>
<tr>
<th style="text-align: right;"></th>
{% for(var i = 0; i < ranges.length; i++) { %}
<th style="text-align: right;">{%= __(ranges[i]) %}</th>
{% } %}
<th style="text-align: right;">{%= __("Total") %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{%= __("Total Outstanding") %}</td>
<td class="text-right">{%= format_number(balance_row["age"], null, 2) %}</td>
<td class="text-right">{%= format_currency(balance_row["range1"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range2"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range3"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range4"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range5"], currency) %}</td>
<td class="text-right">{%= format_currency(flt(balance_row["outstanding"]), currency) %}</td>
</tr>
</tbody>
</table>
</div>
{% } %}
{% } %}
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1 +1,180 @@
{% include "accounts/report/accounts_receivable/accounts_receivable.html" %}
<style type="text/css">
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table thead th {
background: #f8f8f8;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<br>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Supplier") %}:</strong>
{%= (filters.party && filters.party.length && filters.party.join(", ")) || __("All Parties") %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Ageing Based On") %}:</strong>
{%= __(filters.ageing_based_on) %}
</div>
<div>
<strong>{%= __("As on Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.report_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th class="text-left">{%= __("Supplier") %}</th>
<th class="text-right">{%= __("Total Invoiced Amount") %}</th>
<th class="text-right">{%= __("Total Paid Amount") %}</th>
<th class="text-right">{%= __("Debit Note Amount") %}</th>
<th class="text-right">{%= __("Total Outstanding Amount") %}</th>
</tr>
</thead>
<tbody>
{% for (var i = 0, l = data.length; i < l; i++) {
var row = data[i];
if (!(row.party || row.is_total_row)) continue;
%}
<tr>
<td class="{% if (row.is_total_row) { %}text-bold{% } %}">
{% if (row.is_total_row) { %}
{%= __("Total") %}
{% } else { %}
{%= row.party %}
{% } %}
</td>
<td class="text-right">
{%= format_currency(row.invoiced, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.paid, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.debit_note, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.outstanding, row.currency) %}
</td>
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1,291 +1,225 @@
<style>
.print-format {
padding: 4mm;
font-size: 8.0pt !important;
}
.print-format td {
vertical-align:middle !important;
}
</style>
<style type="text/css">
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
<h2 class="text-center" style="margin-top:0">{%= __(report.report_name) %}</h2>
<h4 class="text-center">
{% if (filters.party) { %}
{%= __(filters.party) %}
{% } %}
</h4>
<h6 class="text-center">
{% if (filters.tax_id) { %}
{%= __("Tax Id: ")%} {%= filters.tax_id %}
{% } %}
</h6>
<h5 class="text-center">
{%= __(filters.ageing_based_on) %}
{%= __("Until") %}
{%= frappe.datetime.str_to_user(filters.report_date) %}
</h5>
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
<div class="clearfix">
<div class="pull-left">
{% if(filters.payment_terms) { %}
<strong>{%= __("Payment Terms") %}:</strong> {%= filters.payment_terms %}
{% } %}
</div>
<div class="pull-right">
{% if(filters.credit_limit) { %}
<strong>{%= __("Credit Limit") %}:</strong> {%= format_currency(filters.credit_limit) %}
{% } %}
</div>
</div>
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
{% if(filters.show_future_payments) { %}
{% var balance_row = data.slice(-1).pop();
var start = report.columns.findIndex((elem) => (elem.fieldname == 'age'));
var range1 = report.columns[start].label;
var range2 = report.columns[start+1].label;
var range3 = report.columns[start+2].label;
var range4 = report.columns[start+3].label;
var range5 = report.columns[start+4].label;
var range6 = report.columns[start+5].label;
%}
{% if(balance_row) { %}
<table class="table table-bordered table-condensed">
<caption class="text-right">(Amount in {%= data[0]["currency"] || "" %})</caption>
<colgroup>
<col style="width: 30mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
<col style="width: 18mm;">
</colgroup>
.report-table thead th {
background: #f8f8f8;
text-align: center;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.text-center { text-align: center; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-left { text-align: left; }
.text-bold { font-weight: 700;}
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<br>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Customer") %}:</strong>
{%= (filters.party.length && filters.party.join(", ")) || __("All Parties") %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Report Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.report_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th>{%= __(" ") %}</th>
<th>{%= __(range1) %}</th>
<th>{%= __(range2) %}</th>
<th>{%= __(range3) %}</th>
<th>{%= __(range4) %}</th>
<th>{%= __(range5) %}</th>
<th>{%= __(range6) %}</th>
<th>{%= __("Total") %}</th>
<th style="width: 8em; text-align: left;">{%= __("Date") %}</th>
<th style="text-align: left;">{%= __("Reference") %}</th>
{% if(filters.show_remarks) { %}
<th style="text-align: left;">{%= __("Remarks") %}</th>
{% } %}
<th style="width: 10em; text-align: right;">{%= __("Age (Days)") %}</th>
<th style="width: 10em; text-align: right;">{%= __("Invoiced Amount") %}</th>
<th style="width: 11em; text-align: right;">{%= __("Outstanding Amount") %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{%= __("Total Outstanding") %}</td>
<td class="text-right">
{%= format_number(balance_row["age"], null, 2) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %}
</td>
</tr>
<td>{%= __("Future Payments") %}</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="text-right">
{%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
</td>
<tr class="cvs-footer">
<th class="text-left">{%= __("Cheques Required") %}</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th class="text-right">
{%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}</th>
</tr>
</tbody>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>
<td class="text-left">{%= frappe.datetime.str_to_user(data[i]["posting_date"]) %}</td>
<td class="{% if(i == data.length - 1) { %}text-left text-bold{% } %}">
{% if(i == data.length - 1) { %}
{%= __("Total") %}
{% } else { %}
{%= data[i]["voucher_no"] %}
{% } %}
</td>
{% if(filters.show_remarks) { %}
<td class="text-left">
{% if(data[i]["remarks"] && data[i]["remarks"] != "No Remarks") { %}
{%= data[i]["remarks"] %}
{% } %}
</td>
{% } %}
<td class="text-right">{%= data[i]["age"] %}</td>
<td class="text-right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td class="text-right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
</tr>
{% } %}
</tbody>
</table>
</div>
&nbsp;
{% if(filters.show_future_payments) { %}
{%
var balance_row = data.slice(-1).pop();
var start = report.columns.findIndex(e => e.fieldname == 'age');
var currency = data[data.length - 1]["currency"];
var ranges = [
report.columns[start].label,
report.columns[start+1].label,
report.columns[start+2].label,
report.columns[start+3].label,
report.columns[start+4].label,
report.columns[start+5].label
];
%}
{% if(balance_row) { %}
<div class="report-table">
<table>
<thead>
<tr>
<th style="text-align: right;"></th>
{% for(var i = 0; i < ranges.length; i++) { %}
<th style="text-align: right;">{%= __(ranges[i]) %}</th>
{% } %}
<th style="text-align: right;">{%= __("Total") %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{%= __("Total Outstanding") %}</td>
<td class="text-right">{%= format_number(balance_row["age"], null, 2) %}</td>
<td class="text-right">{%= format_currency(balance_row["range1"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range2"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range3"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range4"], currency) %}</td>
<td class="text-right">{%= format_currency(balance_row["range5"], currency) %}</td>
<td class="text-right">{%= format_currency(flt(balance_row["outstanding"]), currency) %}</td>
</tr>
</tbody>
</table>
</div>
{% } %}
{% } %}
<table class="table table-bordered">
<thead>
<tr>
{% if(report.report_name === "Accounts Receivable" || report.report_name === "Accounts Payable") { %}
<th style="width: 10%">{%= __("Date") %}</th>
<th style="width: 4%">{%= __("Age (Days)") %}</th>
{% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %}
<th style="width: 14%">{%= __("Reference") %}</th>
<th style="width: 10%">{%= __("Sales Person") %}</th>
{% } else { %}
<th style="width: 24%">{%= __("Reference") %}</th>
{% } %}
{% if(!filters.show_future_payments) { %}
<th style="width: 20%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
{% } %}
<th style="width: 10%; text-align: right">{%= __("Invoiced Amount") %}</th>
{% if(!filters.show_future_payments) { %}
<th style="width: 10%; text-align: right">{%= __("Paid Amount") %}</th>
<th style="width: 10%; text-align: right">{%= report.report_name === "Accounts Receivable" ? __('Credit Note') : __('Debit Note') %}</th>
{% } %}
<th style="width: 10%; text-align: right">{%= __("Outstanding Amount") %}</th>
{% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %}
<th style="width: 12%">{%= __("Customer LPO No.") %}</th>
{% } %}
<th style="width: 10%">{%= __("Future Payment Ref") %}</th>
<th style="width: 10%">{%= __("Future Payment Amount") %}</th>
<th style="width: 10%">{%= __("Remaining Balance") %}</th>
{% } %}
{% } else { %}
<th style="width: 40%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
<th style="width: 15%">{%= __("Total Invoiced Amount") %}</th>
<th style="width: 15%">{%= __("Total Paid Amount") %}</th>
<th style="width: 15%">{%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %}</th>
<th style="width: 15%">{%= __("Total Outstanding Amount") %}</th>
{% } %}
</tr>
</thead>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr>
{% if(report.report_name === "Accounts Receivable" || report.report_name === "Accounts Payable") { %}
{% if(data[i]["party"]) { %}
<td>{%= frappe.datetime.str_to_user(data[i]["posting_date"]) %}</td>
<td style="text-align: right">{%= data[i]["age"] %}</td>
<td>
{% if(!filters.show_future_payments) { %}
{%= data[i]["voucher_type"] %}
<br>
{% } %}
{%= data[i]["voucher_no"] %}
</td>
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
{% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %}
<td>{%= data[i]["sales_person"] %}</td>
{% } %}
{% if(!filters.show_future_payments) { %}
<td>
{% if(!filters.party?.length) { %}
{%= data[i]["party"] %}
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
<br> {%= data[i]["customer_name"] %}
{% } else if(data[i]["supplier_name"] != data[i]["party"]) { %}
<br> {%= data[i]["supplier_name"] %}
{% } %}
{% } %}
<div>
{% if data[i]["remarks"] %}
{%= __("Remarks") %}:
{%= data[i]["remarks"] %}
{% } %}
</div>
</td>
{% } %}
<td style="text-align: right">
{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
{% if(!filters.show_future_payments) { %}
<td style="text-align: right">
{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">
{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}</td>
{% } %}
<td style="text-align: right">
{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %}
<td style="text-align: right">
{%= data[i]["po_no"] %}</td>
{% } %}
<td style="text-align: right">{%= data[i]["future_ref"] %}</td>
<td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}</td>
{% } %}
{% } else { %}
<td></td>
{% if(!filters.show_future_payments) { %}
<td></td>
{% } %}
{% if(report.report_name === "Accounts Receivable" && filters.show_sales_person) { %}
<td></td>
{% } %}
<td></td>
<td style="text-align: right"><b>{%= __("Total") %}</b></td>
<td style="text-align: right">
{%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %}</td>
{% if(!filters.show_future_payments) { %}
<td style="text-align: right">
{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} </td>
{% } %}
<td style="text-align: right">
{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %}
<td style="text-align: right">
{%= data[i]["po_no"] %}</td>
{% } %}
<td style="text-align: right">{%= data[i]["future_ref"] %}</td>
<td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}</td>
{% } %}
{% } %}
{% } else { %}
{% if(data[i]["party"]|| "&nbsp;") { %}
{% if(!data[i]["is_total_row"]) { %}
<td>
{% if(!filters.party?.length) { %}
{%= data[i]["party"] %}
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
<br> {%= data[i]["customer_name"] %}
{% } else if(data[i]["supplier_name"] != data[i]["party"]) { %}
<br> {%= data[i]["supplier_name"] %}
{% } %}
{% } %}
<br>{%= __("Remarks") %}:
{%= data[i]["remarks"] %}
</td>
{% } else { %}
<td><b>{%= __("Total") %}</b></td>
{% } %}
<td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% } %}
{% } %}
</tr>
{% } %}
</tbody>
</table>
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
</div>

View File

@@ -1 +1,180 @@
{% include "accounts/report/accounts_receivable/accounts_receivable.html" %}
<style type="text/css">
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table thead th {
background: #f8f8f8;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<br>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Customer") %}:</strong>
{%= (filters.party && filters.party.length && filters.party.join(", ")) || __("All Parties") %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Ageing Based On") %}:</strong>
{%= __(filters.ageing_based_on) %}
</div>
<div>
<strong>{%= __("As on Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.report_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th class="text-left">{%= __("Customer") %}</th>
<th class="text-right">{%= __("Total Invoiced Amount") %}</th>
<th class="text-right">{%= __("Total Paid Amount") %}</th>
<th class="text-right">{%= __("Credit Note Amount") %}</th>
<th class="text-right">{%= __("Total Outstanding Amount") %}</th>
</tr>
</thead>
<tbody>
{% for (var i = 0, l = data.length; i < l; i++) {
var row = data[i];
if (!(row.party || row.is_total_row)) continue;
%}
<tr>
<td class="{% if (row.is_total_row) { %}text-bold{% } %}">
{% if (row.is_total_row) { %}
{%= __("Total") %}
{% } else { %}
{%= row.party %}
{% } %}
</td>
<td class="text-right">
{%= format_currency(row.invoiced, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.paid, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.credit_note, row.currency) %}
</td>
<td class="text-right">
{%= format_currency(row.outstanding, row.currency) %}
</td>
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -1 +1,224 @@
{% include "accounts/report/financial_statements.html" %}
{%
const report_columns = report
.get_columns_for_print()
.filter(col => !col.hidden);
if (report_columns.length > 8) {
frappe.throw(
__("Too many columns. Export the report and print it using a spreadsheet application.")
);
}
%}
<style>
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.financial-statements-important td { font-weight: bold; }
.financial-statements-blank-row td { height: 20px; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right {
text-align: right;
font-variant-numeric: tabular-nums;
}
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table th,
.report-table td {
padding: 6px 8px;
font-size: 14px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
}
.report-table thead th {
background: #f8f8f8;
font-weight: 500;
color: #7c7c7c;
}
.report-table tbody td {
vertical-align: top;
word-wrap: break-word;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Company") %}:</strong> {%= filters.company %}
</div>
<div>
<strong>{%= __("Currency") %}:</strong>
{%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Period Based On") %}:</strong>
{%= filters.filter_based_on %}
</div>
{% if (filters.filter_based_on === "Fiscal Year") { %}
<div>
<strong>{%= __("Start Year") %}:</strong> {%= filters.from_fiscal_year %}
</div>
<div>
<strong>{%= __("End Year") %}:</strong> {%= filters.to_fiscal_year %}
</div>
{% } else if (filters.filter_based_on === "Date Range") { %}
<div>
<strong>{%= __("Start Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_start_date) %}
</div>
<div>
<strong>{%= __("End Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_end_date) %}
</div>
{% } %}
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const align = i === 0 ? "text-left" : "text-right";
%}
<th class="{%= align %}">
{%= col.label %}
</th>
{% } %}
</tr>
</thead>
<tbody>
{% for (let j = 0, k = data.length; j < k; j++) { %}
{%
const row = data[j];
let row_class = "";
if (!(row.parent_account || row.parent_section)) {
row_class = "financial-statements-important";
}
if (!(row.account_name || row.section)) {
row_class += " financial-statements-blank-row";
}
%}
<tr class="{%= row_class %}">
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const value = row[col.fieldname];
const align = i === 0 ? "text-left" : "text-right";
%}
<td class="{%= align %}">
{% if (i === 0) { %}
<span style="padding-left: {%= cint(row.indent) * 2 %}em">
{%= String(row.account_name || row.section || "").replace(/^['"]|['"]$/g, "") %}
</span>
{% } else if (!is_null(value)) { %}
{%= frappe.format(value, col, {}, row) %}
{% } %}
</td>
{% } %}
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right text-muted">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -8,6 +8,7 @@ from frappe.utils import cint, flt
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,

View File

@@ -13,9 +13,6 @@ COMPANY_SHORT_NAME = "_TC6"
class TestBalanceSheet(ERPNextTestSuite):
def test_balance_sheet(self):
frappe.db.sql(f"delete from `tabJournal Entry` where company='{COMPANY}'")
frappe.db.sql(f"delete from `tabGL Entry` where company='{COMPANY}'")
create_account("VAT Liabilities", f"Duties and Taxes - {COMPANY_SHORT_NAME}", COMPANY)
create_account("Advance VAT Paid", f"Duties and Taxes - {COMPANY_SHORT_NAME}", COMPANY)
create_account("My Bank", f"Bank Accounts - {COMPANY_SHORT_NAME}", COMPANY)

View File

@@ -1 +1,224 @@
{% include "accounts/report/financial_statements.html" %}
{%
const report_columns = report
.get_columns_for_print()
.filter(col => !col.hidden);
if (report_columns.length > 8) {
frappe.throw(
__("Too many columns. Export the report and print it using a spreadsheet application.")
);
}
%}
<style>
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.financial-statements-important td { font-weight: bold; }
.financial-statements-blank-row td { height: 20px; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right {
text-align: right;
font-variant-numeric: tabular-nums;
}
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table th,
.report-table td {
padding: 6px 8px;
font-size: 14px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
}
.report-table thead th {
background: #f8f8f8;
font-weight: 500;
color: #7c7c7c;
}
.report-table tbody td {
vertical-align: top;
word-wrap: break-word;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Company") %}:</strong> {%= filters.company %}
</div>
<div>
<strong>{%= __("Currency") %}:</strong>
{%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Period Based On") %}:</strong>
{%= filters.filter_based_on %}
</div>
{% if (filters.filter_based_on === "Fiscal Year") { %}
<div>
<strong>{%= __("Start Year") %}:</strong> {%= filters.from_fiscal_year %}
</div>
<div>
<strong>{%= __("End Year") %}:</strong> {%= filters.to_fiscal_year %}
</div>
{% } else if (filters.filter_based_on === "Date Range") { %}
<div>
<strong>{%= __("Start Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_start_date) %}
</div>
<div>
<strong>{%= __("End Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_end_date) %}
</div>
{% } %}
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const align = i === 0 ? "text-left" : "text-right";
%}
<th class="{%= align %}">
{%= col.label %}
</th>
{% } %}
</tr>
</thead>
<tbody>
{% for (let j = 0, k = data.length; j < k; j++) { %}
{%
const row = data[j];
let row_class = "";
if (!(row.parent_account || row.parent_section)) {
row_class = "financial-statements-important";
}
if (!(row.account_name || row.section)) {
row_class += " financial-statements-blank-row";
}
%}
<tr class="{%= row_class %}">
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const value = row[col.fieldname];
const align = i === 0 ? "text-left" : "text-right";
%}
<td class="{%= align %}">
{% if (i === 0) { %}
<span style="padding-left: {%= cint(row.indent) * 2 %}em">
{%= String(row.account_name || row.section || "").replace(/^['"]|['"]$/g, "") %}
</span>
{% } else if (!is_null(value)) { %}
{%= frappe.format(value, col, {}, row) %}
{% } %}
</td>
{% } %}
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right text-muted">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -12,6 +12,7 @@ from pypika import Order
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)
from erpnext.accounts.report.financial_statements import (
get_columns,

View File

@@ -3,6 +3,7 @@
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)

View File

@@ -1,51 +1,114 @@
<!-- Modified on 25-11-2024
-->
<style type="text/css">
/* General styles for both screen display and print */
body, html {
margin-top: 10;
margin-top: 10px;
padding: 0;
width: 100%;
height: auto; /* Allow content to expand */
font-family: Arial, sans-serif; /* Example font */
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
/* Ensure consistent letter spacing across all media */
.title-letter-spacing {
letter-spacing: .2rem;
font-size: 15px;
font-weight: 600;
color: #171717;
}
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table thead th {
background: #f8f8f8;
text-align: center;
font-size: 14px;
font-weight: 500;
color: #7c7c7c;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
padding: 6px 8px;
}
.report-table tbody td {
padding: 6px 8px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
vertical-align: top;
word-wrap: break-word;
font-size: 14px;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
.date-col {
white-space: nowrap;
}
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; font-variant-numeric: tabular-nums; }
.text-bold { font-weight: 700; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
color: #171717;
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta .filter-row {
margin-bottom: 4px;
line-height: 1.5;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
/* Styles specific to printing and PDF generation */
@media print {
/* Set page size and margins for printing */
@page {
size: A4; /* Use fixed A4 page size */
size: A4;
margin-top: 10mm;
}
/* Force a page break before elements with the class "page-break" */
.page-break {
page-break-before: always;
margin-top: 10mm; /* Add some space after the break */
}
/* Ensure table headers repeat on each printed page */
thead {
display: table-header-group;
}
/* Ensure table footers repeat on each printed page */
tfoot {
display: table-footer-group;
}
th, td {
padding: 1px;
border: 1px solid black; /* Example border for clarity */
tr {
page-break-inside: avoid;
}
/* Hide elements that should not appear in print (optional) */
.no-print {
display: none !important;
}
@@ -53,129 +116,154 @@
</style>
<br>
<div style="font-family:Arial">
<div>
<div class="title-letter-spacing" style="text-align:center; font-size:15px; text-decoration:underline;">
<b>
{%= __("STATEMENT OF ACCOUNTS") %}<br>
{% if (filters.party_name) { %}
<br>{%= filters.party_name %}
{% } else if (filters.party && filters.party.length) { %}
<br>{%= filters.party %}
{% } else if (filters.account) { %}
<br>{%= filters.account %}
{% } else { %}
<br>{%= __("All Parties ") %}
{% } %}
</b>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __("Statement Of Accounts") %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div class="filter-row">
<strong>{%= __("Customer") %}:</strong>
{%=
(filters.party.length && filters.party.join(", ")) || filters.party_name || "All Parties"
%}
</div>
</div>
<div class="right text-right">
<div class="filter-row">
<strong>{%= __("Statement Period") %}:</strong>
{%= __("{0} to {1}", [
frappe.datetime.str_to_user(filters.from_date),
frappe.datetime.str_to_user(filters.to_date)
]) %}
</div>
</div>
</div>
<div style="text-align:center; font-size:13px;">
<b>
{%= __("{0} to {1}", [frappe.datetime.str_to_user(filters.from_date), frappe.datetime.str_to_user(filters.to_date)]) %}<br><br>
</b>
</div>
</div>
<div class="show-filters">
{% if subtitle %}
{{ subtitle }}
<hr>
{% endif %}
</div>
<table style="width:100%; font-size: 11px">
<thead>
<tr class="title-letter-spacing" style="text-align: center; font-weight:bold">
<td style="border: 1.5px solid black; width: 7em">{%= __("Date").toLocaleUpperCase() %}</td>
<td style="border: 1.5px solid black">{%= __("Particulars").toLocaleUpperCase() %}</td>
{% if(filters.show_remarks) { %}
<td style="border: 1.5px solid black">{%= __("Remarks").toLocaleUpperCase() %}</td>
{% } %}
<td style="border: 1.5px solid black; width: 9em">{%= __("Debit").toLocaleUpperCase() %}</td>
<td style="border: 1.5px solid black; width: 9em">{%= __("Credit").toLocaleUpperCase() %}</td>
<td style="border: 1.5px solid black; width: 10.2em">{%= __("Balance").toLocaleUpperCase() %}</td>
</tr>
</thead>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
<tr style="border-bottom: 1px solid black">
{% if(data[i].posting_date) { %}
<td style="text-align: center; border: 1px dotted black">
{%= frappe.datetime.str_to_user(data[i].posting_date) %}
</td>
<td style="border-right: 1px dotted black">
{%= data[i].voucher_type %} {%= data[i].voucher_no %}
{% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %}
{% } %}<br>
{% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
</td>
{% if(filters.show_remarks) { %}
<td style="border-right: 1px dotted black; font-size: 10px">
{% if(data[i].remarks != "No Remarks" && data[i].remarks != "") { %}
{%= __("Remarks") %}: {%= data[i].remarks %}<br>
{% } %}
</td>
{% } %}
<td style="text-align: right; border-right: 1px dotted black">
{% if data[i].debit != 0 %}
{%= format_currency(data[i].debit, filters.presentation_currency) %}
{% } %}
</td>
<td style="text-align: right; border-right: 1px dotted black">
{% if data[i].credit != 0 %}
{%= format_currency(data[i].credit, filters.presentation_currency) %}
{% } %}
</td>
{% } else { %}
<td style="text-align: center; border: 1px dotted black">
{% if(i == 0) { %}
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
<th style="width: 8em; text-align: left;">{%= __("Date") %}</th>
<th style="text-align: left;">{%= __("Voucher Details") %}</th>
{% if(filters.show_remarks) { %}
<th style="text-align: left;">{%= __("Remarks") %}</th>
{% } %}
<th style="width: 10em; text-align: right;">{%= __("Debit") %}</th>
<th style="width: 10em; text-align: right;">{%= __("Credit") %}</th>
<th style="width: 10em; text-align: right;">{%= __("Balance") %}</th>
</tr>
</thead>
<tbody>
{% for(var i=0, l=data.length; i<l; i++) { %}
{% var row = data[i]; %}
{% var is_entry = row.posting_date; %}
{% var is_last = i == l-1; %}
{% var is_second_last = i == l-2; %}
<tr>
<td class="text-left date-col">
{% if(is_entry) { %}
{%= frappe.datetime.str_to_user(row.posting_date) %}
{% } else if(i == 0) { %}
{%= frappe.datetime.str_to_user(filters.from_date) %}
{% } %}
</td>
<td style="text-align: left; border-right: 1px dotted black"><b>
{% if(i == l-2) { %}
{%= __("Total") %}
<td class="{% if(!is_entry) { %}text-left text-bold{% } %}">
{% if(is_entry) { %}
{%= row.voucher_type %} {%= row.voucher_no %}
{% if(!(filters.party || filters.account)) { %}
<div style="margin-top: 2px;">
{%= row.party || row.account %}
</div>
{% } %}
{% if(row.bill_no) { %}
<div style="margin-top: 2px;">
{%= __("Supplier Invoice No") %}: {%= row.bill_no %}
</div>
{% } %}
{% } else { %}
{% if(i == l-1) { %}
{% if(is_second_last) { %}
{%= __("Total") %}
{% } else if(is_last) { %}
{%= __("Closing [Opening + Total] ") %}
{% } else { %}
{%= frappe.format(data[i].account, {fieldtype: "Link"}) || "&nbsp;" %}
{% } %}
{% } %}</b>
</td>
{% if(filters.show_remarks) { %} <td style="text-align: left; border-right: 1px dotted black"></td>{% } %}
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != 0){ %}
{% if(i != l-1){ %}
{%= data[i].account && format_currency(data[i].debit, filters.presentation_currency) %}
{%= frappe.format(row.account, {fieldtype: "Link"}) || "&nbsp;" %}
{% } %}
{% } %}
</td>
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != 0){ %}
{% if(i != l-1){ %}
{%= data[i].account && format_currency(data[i].credit, filters.presentation_currency) %}
{% } %}
{% if(filters.show_remarks) { %}
<td class="text-left">
{% if(is_entry && row.remarks && row.remarks != "No Remarks") { %}
{%= row.remarks %}
{% } %}
</td>
{% } %}
{% if(i == l-1) { %}
<td style="text-align: right; font-weight:bold; border-right: 1px dotted black">
{%= format_currency(data[i].balance, filters.presentation_currency) %}
{% if(data[i].balance < 0){ %}Cr{% } %}
{% if(data[i].balance > 0){ %}Dr{% } %}
</td>
{% } else { %}
<td style="text-align: right; border-right: 1px dotted black">
{% if(i != l-2) { %}
{%= format_currency(data[i].balance, filters.presentation_currency) %}
{% } %}
</td>
{% } %}
</tr>
{% endfor%}
</tbody>
</table>
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
</div>
<td class="text-right">
{% if(is_entry) { %}
{% if(row.debit != 0) { %}
{%= format_currency(row.debit, filters.presentation_currency) %}
{% } %}
{% } else if(i != 0 && !is_last) { %}
{%= row.account && format_currency(row.debit, filters.presentation_currency) %}
{% } %}
</td>
<td class="text-right">
{% if(is_entry) { %}
{% if(row.credit != 0) { %}
{%= format_currency(row.credit, filters.presentation_currency) %}
{% } %}
{% } else if(i != 0 && !is_last) { %}
{%= row.account && format_currency(row.credit, filters.presentation_currency) %}
{% } %}
</td>
<td class="text-right {% if(is_last) { %}text-bold{% } %}">
{% if(is_last) { %}
{%= format_currency(row.balance, filters.presentation_currency) %}
{% if(row.balance < 0) { %} Cr{% } %}
{% if(row.balance > 0) { %} Dr{% } %}
{% } else { %}
{%= format_currency(row.balance, filters.presentation_currency) %}
{% } %}
</td>
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -417,7 +417,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
# Opening for filtered account
add_total_to_data(totals, "opening")
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
if not filters.get("categorize_by"):
all_entries = []
for acc_dict in gle_map.values():
all_entries.extend(acc_dict.entries)
data += all_entries
elif filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
set_opening_closing = (not filters.get("categorize_by") and not filters.get("voucher_no")) or (
filters.get("categorize_by") and filters.get("categorize_by") != "Categorize by Voucher"
)
@@ -483,7 +489,6 @@ def initialize_gle_map(gl_entries, filters):
totals=get_totals_dict(),
entries=[],
)
return gle_map

View File

@@ -1 +1,224 @@
{% include "accounts/report/financial_statements.html" %}
{%
const report_columns = report
.get_columns_for_print()
.filter(col => !col.hidden);
if (report_columns.length > 8) {
frappe.throw(
__("Too many columns. Export the report and print it using a spreadsheet application.")
);
}
%}
<style>
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.financial-statements-important td { font-weight: bold; }
.financial-statements-blank-row td { height: 20px; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right {
text-align: right;
font-variant-numeric: tabular-nums;
}
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table th,
.report-table td {
padding: 6px 8px;
font-size: 14px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
}
.report-table thead th {
background: #f8f8f8;
font-weight: 500;
color: #7c7c7c;
}
.report-table tbody td {
vertical-align: top;
word-wrap: break-word;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Company") %}:</strong> {%= filters.company %}
</div>
<div>
<strong>{%= __("Currency") %}:</strong>
{%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("Period Based On") %}:</strong>
{%= filters.filter_based_on %}
</div>
{% if (filters.filter_based_on === "Fiscal Year") { %}
<div>
<strong>{%= __("Start Year") %}:</strong> {%= filters.from_fiscal_year %}
</div>
<div>
<strong>{%= __("End Year") %}:</strong> {%= filters.to_fiscal_year %}
</div>
{% } else if (filters.filter_based_on === "Date Range") { %}
<div>
<strong>{%= __("Start Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_start_date) %}
</div>
<div>
<strong>{%= __("End Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.period_end_date) %}
</div>
{% } %}
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const align = i === 0 ? "text-left" : "text-right";
%}
<th class="{%= align %}">
{%= col.label %}
</th>
{% } %}
</tr>
</thead>
<tbody>
{% for (let j = 0, k = data.length; j < k; j++) { %}
{%
const row = data[j];
let row_class = "";
if (!(row.parent_account || row.parent_section)) {
row_class = "financial-statements-important";
}
if (!(row.account_name || row.section)) {
row_class += " financial-statements-blank-row";
}
%}
<tr class="{%= row_class %}">
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const value = row[col.fieldname];
const align = i === 0 ? "text-left" : "text-right";
%}
<td class="{%= align %}">
{% if (i === 0) { %}
<span style="padding-left: {%= cint(row.indent) * 2 %}em">
{%= String(row.account_name || row.section || "").replace(/^['"]|['"]$/g, "") %}
</span>
{% } else if (!is_null(value)) { %}
{%= frappe.format(value, col, {}, row) %}
{% } %}
</td>
{% } %}
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right text-muted">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -8,6 +8,7 @@ from frappe.utils import flt
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,

View File

@@ -11,9 +11,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPurchaseRegister(ERPNextTestSuite):
def test_purchase_register(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
@@ -28,9 +25,6 @@ class TestPurchaseRegister(ERPNextTestSuite):
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
@@ -74,9 +68,6 @@ class TestPurchaseRegister(ERPNextTestSuite):
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ledger_view(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
filters = frappe._dict(
company="_Test Company 6",
from_date=add_months(today(), -1),

View File

@@ -6,301 +6,282 @@ from frappe import _
from frappe.query_builder.functions import IfNull
def execute(filters=None):
"""Generate Tax Withholding Details report"""
validate_filters(filters)
class TaxWithholdingDetailsReport:
party_types = ("Customer", "Supplier")
document_types = ("Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry")
# Process and format data
data = get_tax_withholding_data(filters)
columns = get_columns(filters)
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
self.entries = []
self.doc_info = {}
self.party_details = {}
return columns, data
@classmethod
def execute(cls, filters=None):
return cls(filters).run()
def run(self):
self.validate_filters()
return self.get_columns(), self.get_data()
def validate_filters(filters):
"""Validate report filters"""
filters = frappe._dict(filters or {})
def validate_filters(self):
if not self.filters.from_date or not self.filters.to_date:
frappe.throw(_("From Date and To Date are required"))
if not filters.from_date or not filters.to_date:
frappe.throw(_("From Date and To Date are required"))
if self.filters.from_date > self.filters.to_date:
frappe.throw(_("From Date must be before To Date"))
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
def get_data(self):
self.entries = self.get_entries_query().run(as_dict=True)
if not self.entries:
return []
self.doc_info = self.fetch_additional_doc_info()
self.party_details = self.fetch_party_details()
return self.build_rows()
def get_tax_withholding_data(filters):
"""Process entries into final report format"""
data = []
entries = get_tax_withholding_entries(filters)
if not entries:
return data
def build_rows(self):
rows = []
for entry in self.entries:
doc_details = (
self.doc_info.get((entry.transaction_type, entry.ref_no), {}) if entry.ref_no else {}
)
party_info = self.party_details.get((entry.party_type, entry.party), {})
rows.append({**entry, **doc_details, **party_info})
doc_info = get_additional_doc_info(entries)
party_details = get_party_details(entries)
rows.sort(
key=lambda x: (
x["tax_withholding_category"] or "",
x["transaction_date"] or "",
x["withholding_name"] or "",
)
)
return rows
for entry in entries:
doc_details = frappe._dict()
if entry.taxable_name:
doc_details = doc_info.get((entry.taxable_doctype, entry.taxable_name), {})
def get_entries_query(self):
twe = frappe.qb.DocType("Tax Withholding Entry")
query = (
frappe.qb.from_(twe)
.select(
twe.party_type,
twe.party,
IfNull(twe.tax_id, "").as_("tax_id"),
twe.tax_withholding_category,
twe.taxable_amount.as_("total_amount"),
twe.tax_rate.as_("rate"),
twe.withholding_amount.as_("tax_amount"),
IfNull(twe.taxable_doctype, "").as_("transaction_type"),
IfNull(twe.taxable_name, "").as_("ref_no"),
twe.taxable_date,
IfNull(twe.withholding_doctype, "").as_("withholding_doctype"),
IfNull(twe.withholding_name, "").as_("withholding_name"),
twe.withholding_date.as_("transaction_date"),
)
.where(twe.docstatus == 1)
.where(twe.withholding_date >= self.filters.from_date)
.where(twe.withholding_date <= self.filters.to_date)
.where(IfNull(twe.withholding_name, "") != "")
.where(twe.status != "Duplicate")
)
party_info = party_details.get((entry.party_type, entry.party), {})
if self.filters.company:
query = query.where(twe.company == self.filters.company)
if self.filters.party_type:
query = query.where(twe.party_type == self.filters.party_type)
if self.filters.party:
query = query.where(twe.party == self.filters.party)
row = {
"section_code": entry.tax_withholding_category,
"entity_type": party_info.get("entity_type"),
"rate": entry.tax_rate,
"total_amount": entry.taxable_amount,
"grand_total": doc_details.get("grand_total", 0),
"base_total": doc_details.get("base_total", 0),
"tax_amount": entry.withholding_amount,
"transaction_date": entry.withholding_date,
"transaction_type": entry.taxable_doctype,
"ref_no": entry.taxable_name,
"taxable_date": entry.taxable_date,
"supplier_invoice_no": doc_details.get("bill_no"),
"supplier_invoice_date": doc_details.get("bill_date"),
"withholding_doctype": entry.withholding_doctype,
"withholding_name": entry.withholding_name,
"party_name": party_info.get("party_name"),
"tax_id": entry.tax_id,
"party": entry.party,
"party_type": entry.party_type,
}
data.append(row)
return query
# Sort by section code, transaction date, then withholding_name for deterministic ordering
data.sort(
key=lambda x: (x["section_code"] or "", x["transaction_date"] or "", x["withholding_name"] or "")
)
return data
def fetch_party_details(self):
parties_by_type = {pt: set() for pt in self.party_types}
for entry in self.entries:
if entry.party_type in parties_by_type and entry.party:
parties_by_type[entry.party_type].add(entry.party)
party_map = {}
for party_type, party_set in parties_by_type.items():
if not party_set:
continue
def get_party_details(entries):
"""Fetch party details in batch for all entries"""
party_map = frappe._dict()
parties_by_type = {"Customer": set(), "Supplier": set()}
query = self.get_party_query(party_type, party_set)
if query is None:
continue
# Group parties by type
for entry in entries:
if entry.party_type in parties_by_type and entry.party:
parties_by_type[entry.party_type].add(entry.party)
for row in query.run(as_dict=True):
party_map[(party_type, row.pop("name"))] = row
# Batch fetch for each party type
for party_type, party_set in parties_by_type.items():
if not party_type or not party_set:
continue
return party_map
def get_party_query(self, party_type, party_set):
doctype = frappe.qb.DocType(party_type)
fields = [doctype.name]
if party_type == "Supplier":
fields.extend([doctype.supplier_type.as_("entity_type"), doctype.supplier_name.as_("party_name")])
fields.extend(
[
doctype.supplier_type.as_("party_entity_type"),
doctype.supplier_name.as_("party_name"),
]
)
elif party_type == "Customer":
fields.extend([doctype.customer_type.as_("entity_type"), doctype.customer_name.as_("party_name")])
fields.extend(
[
doctype.customer_type.as_("party_entity_type"),
doctype.customer_name.as_("party_name"),
]
)
else:
return None
query = frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(party_set))
party_details = query.run(as_dict=True)
return frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(party_set))
for party in party_details:
party_map[(party_type, party.name)] = party
def fetch_additional_doc_info(self):
docs_by_type = {dt: set() for dt in self.document_types}
for entry in self.entries:
if entry.ref_no and entry.transaction_type in docs_by_type:
docs_by_type[entry.transaction_type].add(entry.ref_no)
return party_map
doc_info = {}
for doctype_name, voucher_set in docs_by_type.items():
if not voucher_set:
continue
query = self.get_doc_info_query(doctype_name, voucher_set)
if query is None:
continue
for row in query.run(as_dict=True):
doc_info[(doctype_name, row.pop("name"))] = row
return doc_info
def get_doc_info_query(self, doctype_name, voucher_set):
if doctype_name == "Purchase Invoice":
get_doc_fields = self.get_purchase_invoice_fields
elif doctype_name == "Sales Invoice":
get_doc_fields = self.get_sales_invoice_fields
elif doctype_name == "Payment Entry":
get_doc_fields = self.get_payment_entry_fields
elif doctype_name == "Journal Entry":
get_doc_fields = self.get_journal_entry_fields
else:
return None
doctype = frappe.qb.DocType(doctype_name)
fields = [doctype.name, *get_doc_fields(doctype)]
return frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(voucher_set))
def get_purchase_invoice_fields(self, doctype):
return [
doctype.grand_total,
doctype.base_total,
doctype.bill_no.as_("supplier_invoice_no"),
doctype.bill_date.as_("supplier_invoice_date"),
]
def get_sales_invoice_fields(self, doctype):
return [doctype.grand_total, doctype.base_total]
def get_payment_entry_fields(self, doctype):
return [
doctype.paid_amount_after_tax.as_("grand_total"),
doctype.base_paid_amount.as_("base_total"),
]
def get_journal_entry_fields(self, doctype):
return [doctype.total_debit.as_("grand_total"), doctype.total_debit.as_("base_total")]
def get_columns(self):
party_type = self.filters.get("party_type", "Party")
return [
{
"label": _("Tax Withholding Category"),
"options": "Tax Withholding Category",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"width": 90,
},
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 60},
{
"label": _(f"{party_type} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _(party_type),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
{
"label": _(f"{party_type} Type"),
"fieldname": "party_entity_type",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Supplier Invoice No"),
"fieldname": "supplier_invoice_no",
"fieldtype": "Data",
"width": 120,
},
{
"label": _("Supplier Invoice Date"),
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"width": 120,
},
{"label": _("Tax Rate %"), "fieldname": "rate", "fieldtype": "Percent", "width": 60},
{
"label": _("Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Currency",
"width": 120,
},
{"label": _("Tax Amount"), "fieldname": "tax_amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Grand Total (Company Currency)"),
"fieldname": "base_total",
"fieldtype": "Currency",
"width": 150,
},
{
"label": _("Grand Total (Transaction Currency)"),
"fieldname": "grand_total",
"fieldtype": "Currency",
"width": 170,
},
{"label": _("Reference Date"), "fieldname": "taxable_date", "fieldtype": "Date", "width": 100},
{
"label": _("Transaction Type"),
"fieldname": "transaction_type",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Reference No."),
"fieldname": "ref_no",
"fieldtype": "Dynamic Link",
"options": "transaction_type",
"width": 180,
},
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Withholding Document"),
"fieldname": "withholding_name",
"fieldtype": "Dynamic Link",
"options": "withholding_doctype",
"width": 150,
},
]
def get_columns(filters):
"""Generate report columns based on filters"""
columns = [
{
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 90,
},
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 60},
{
"label": _(f"{filters.get('party_type', 'Party')} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _(filters.get("party_type", "Party")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
{
"label": _("Entity Type"),
"fieldname": "entity_type",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Supplier Invoice No"),
"fieldname": "supplier_invoice_no",
"fieldtype": "Data",
"width": 120,
},
{
"label": _("Supplier Invoice Date"),
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"width": 120,
},
{
"label": _("Tax Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 60,
},
{
"label": _("Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Grand Total (Company Currency)"),
"fieldname": "base_total",
"fieldtype": "Currency",
"width": 150,
},
{
"label": _("Grand Total (Transaction Currency)"),
"fieldname": "grand_total",
"fieldtype": "Currency",
"width": 170,
},
{
"label": _("Reference Date"),
"fieldname": "taxable_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Transaction Type"),
"fieldname": "transaction_type",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Reference No."),
"fieldname": "ref_no",
"fieldtype": "Dynamic Link",
"options": "transaction_type",
"width": 180,
},
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Withholding Document"),
"fieldname": "withholding_name",
"fieldtype": "Dynamic Link",
"options": "withholding_doctype",
"width": 150,
},
]
return columns
def get_tax_withholding_entries(filters):
twe = frappe.qb.DocType("Tax Withholding Entry")
query = (
frappe.qb.from_(twe)
.select(
twe.company,
twe.party_type,
twe.party,
IfNull(twe.tax_id, "").as_("tax_id"),
twe.tax_withholding_category,
IfNull(twe.tax_withholding_group, "").as_("tax_withholding_group"),
twe.taxable_amount,
twe.tax_rate,
twe.withholding_amount,
IfNull(twe.taxable_doctype, "").as_("taxable_doctype"),
IfNull(twe.taxable_name, "").as_("taxable_name"),
twe.taxable_date,
IfNull(twe.under_withheld_reason, "").as_("under_withheld_reason"),
IfNull(twe.lower_deduction_certificate, "").as_("lower_deduction_certificate"),
IfNull(twe.withholding_doctype, "").as_("withholding_doctype"),
IfNull(twe.withholding_name, "").as_("withholding_name"),
twe.withholding_date,
twe.status,
)
.where(twe.docstatus == 1)
.where(twe.withholding_date >= filters.from_date)
.where(twe.withholding_date <= filters.to_date)
.where(IfNull(twe.withholding_name, "") != "")
.where(twe.status != "Duplicate")
)
if filters.get("company"):
query = query.where(twe.company == filters.get("company"))
if filters.get("party_type"):
query = query.where(twe.party_type == filters.get("party_type"))
if filters.get("party"):
query = query.where(twe.party == filters.get("party"))
return query.run(as_dict=True)
def get_additional_doc_info(entries):
"""Fetch additional document information in batch"""
doc_info = {}
docs_by_type = {
"Purchase Invoice": set(),
"Sales Invoice": set(),
"Payment Entry": set(),
"Journal Entry": set(),
}
# Group documents by type
for entry in entries:
if entry.taxable_name and entry.taxable_doctype in docs_by_type:
docs_by_type[entry.taxable_doctype].add(entry.taxable_name)
for doctype_name, voucher_set in docs_by_type.items():
if voucher_set:
_fetch_doc_info(doctype_name, voucher_set, doc_info)
return doc_info
def _fetch_doc_info(doctype_name, voucher_set, doc_info):
doctype = frappe.qb.DocType(doctype_name)
fields = [doctype.name]
# Add doctype-specific fields
if doctype_name == "Purchase Invoice":
fields.extend([doctype.grand_total, doctype.base_total, doctype.bill_no, doctype.bill_date])
elif doctype_name == "Sales Invoice":
fields.extend([doctype.grand_total, doctype.base_total])
elif doctype_name == "Payment Entry":
fields.extend(
[doctype.paid_amount_after_tax.as_("grand_total"), doctype.base_paid_amount.as_("base_total")]
)
elif doctype_name == "Journal Entry":
fields.extend([doctype.total_debit.as_("grand_total"), doctype.total_debit.as_("base_total")])
else:
return
query = frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(voucher_set))
entries = query.run(as_dict=True)
for entry in entries:
doc_info[(doctype_name, entry.name)] = entry
execute = TaxWithholdingDetailsReport.execute

View File

@@ -40,7 +40,7 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
expected_values = [
[jv.name, "TCS", 0.075, 1000.75, 0.75, 1000.75],
["", "TCS", 0.075, 0, 0.75, 0],
["", "TCS", 0.075, None, 0.75, None],
[si.name, "TCS", 0.075, 1000.0, 0.75, 1000.75],
]
self.check_expected_values(result, expected_values)
@@ -124,7 +124,7 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
voucher_expected_values = expected_values[i]
voucher_actual_values = (
voucher.ref_no,
voucher.section_code,
voucher.tax_withholding_category,
voucher.rate,
voucher.base_total,
voucher.tax_amount,

View File

@@ -2,121 +2,94 @@ import frappe
from frappe import _
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import (
get_tax_withholding_data,
TaxWithholdingDetailsReport,
)
from erpnext.accounts.utils import get_fiscal_year
def execute(filters=None):
validate_filters(filters)
class TDSComputationSummaryReport(TaxWithholdingDetailsReport):
GROUP_BY_FIELDS = ("party_type", "party", "tax_withholding_category")
CARRY_OVER_FIELDS = (
"tax_id",
"party",
"party_type",
"party_name",
"tax_withholding_category",
"party_entity_type",
"rate",
)
AGGREGATE_FIELDS = ("total_amount", "tax_amount")
data = get_tax_withholding_data(filters)
columns = get_columns(filters)
def validate_filters(self):
if self.filters.from_date > self.filters.to_date:
frappe.throw(_("From Date must be before To Date"))
final_result = group_by_party_and_category(data, filters)
from_year = get_fiscal_year(self.filters.from_date)[0]
to_year = get_fiscal_year(self.filters.to_date)[0]
if from_year != to_year:
frappe.throw(_("From Date and To Date lie in different Fiscal Year"))
return columns, final_result
self.filters.fiscal_year = from_year
def get_data(self):
return self.group_rows(super().get_data())
def validate_filters(filters):
"""Validate if dates are properly set and lie in the same fiscal year"""
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
def group_rows(self, data):
grouped = {}
for row in data:
key = tuple(row.get(f) for f in self.GROUP_BY_FIELDS)
bucket = grouped.setdefault(
key,
{
**{f: row.get(f) for f in self.CARRY_OVER_FIELDS},
**{f: 0.0 for f in self.AGGREGATE_FIELDS},
},
)
from_year = get_fiscal_year(filters.from_date)[0]
to_year = get_fiscal_year(filters.to_date)[0]
if from_year != to_year:
frappe.throw(_("From Date and To Date lie in different Fiscal Year"))
for f in self.AGGREGATE_FIELDS:
bucket[f] += row.get(f) or 0.0
filters["fiscal_year"] = from_year
return list(grouped.values())
def group_by_party_and_category(data, filters):
party_category_wise_map = {}
for row in data:
party_category_wise_map.setdefault(
(row.get("party"), row.get("section_code")),
def get_columns(self):
party_type = self.filters.get("party_type", "Party")
return [
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 90},
{
"tax_id": row.get("tax_id"),
"party": row.get("party"),
"party_name": row.get("party_name"),
"section_code": row.get("section_code"),
"entity_type": row.get("entity_type"),
"rate": row.get("rate"),
"total_amount": 0.0,
"tax_amount": 0.0,
"label": _(party_type),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
)
party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get(
"total_amount", 0.0
)
party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get(
"tax_amount", 0.0
)
final_result = get_final_result(party_category_wise_map)
return final_result
{
"label": _(f"{party_type} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Tax Withholding Category"),
"options": "Tax Withholding Category",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"width": 180,
},
{
"label": _(f"{party_type} Type"),
"fieldname": "party_entity_type",
"fieldtype": "Data",
"width": 180,
},
{"label": _("Tax Rate %"), "fieldname": "rate", "fieldtype": "Percent", "width": 120},
{
"label": _("Total Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
{"label": _("Tax Amount"), "fieldname": "tax_amount", "fieldtype": "Float", "width": 120},
]
def get_final_result(party_category_wise_map):
out = []
for _key, value in party_category_wise_map.items():
out.append(value)
return out
def get_columns(filters):
columns = [
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 90},
{
"label": _(filters.get("party_type")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
{
"label": _(f"{filters.get('party_type', 'Party')} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 180,
},
{
"label": _("Entity Type"),
"fieldname": "entity_type",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Tax Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 120,
},
{
"label": _("Total Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,
},
]
return columns
execute = TDSComputationSummaryReport.execute

View File

@@ -42,9 +42,6 @@ class TestTrialBalance(ERPNextTestSuite):
"""
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'")
frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'")
branch1 = frappe.new_doc("Branch")
branch1.branch = "Location 1"
branch1.insert(ignore_if_duplicate=True)

View File

@@ -1 +1,215 @@
{% include "accounts/report/financial_statements.html" %}
{%
const report_columns = report
.get_columns_for_print()
.filter(col => !col.hidden);
if (report_columns.length > 8) {
frappe.throw(
__("Too many columns. Export the report and print it using a spreadsheet application.")
);
}
%}
<style>
body, html {
margin-top: 10px;
padding: 0;
width: 100%;
height: auto;
font-family: Inter, sans-serif;
font-size: 14px;
line-height: 21px;
color: #171717;
}
.title-letter-spacing {
font-size: 15px;
font-weight: 600;
color: #171717;
}
.financial-statements-important td { font-weight: bold; }
.financial-statements-blank-row td { height: 20px; }
.report-meta {
margin: 10px 0 14px;
padding: 8px 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.report-meta .left,
.report-meta .right {
display: flex;
flex-direction: column;
}
.report-meta strong {
color: #7c7c7c;
font-weight: 500;
}
.report-subtitle {
margin: 10px 0 14px;
}
.text-center { text-align: center; }
.text-right {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.text-left { text-align: left; }
.text-bold { font-weight: 700; }
.report-table table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.report-table th,
.report-table td {
padding: 6px 8px;
font-size: 14px;
border-top: 1px solid #ededed;
border-bottom: 1px solid #ededed;
}
.report-table thead th {
background: #f8f8f8;
font-weight: 500;
color: #7c7c7c;
}
.report-table tbody td.text-left {
vertical-align: top;
word-wrap: break-word;
}
.report-table thead th:first-child {
border-left: 1px solid #ededed;
}
.report-table thead th:last-child {
border-right: 1px solid #ededed;
}
.report-table tbody tr:last-child td {
border-bottom: none;
}
@media print {
@page {
size: A4;
margin-top: 10mm;
}
thead { display: table-header-group; }
tr { page-break-inside: avoid; }
}
</style>
<div>
<div class="text-center" style="margin-bottom: 12px;">
<div class="title-letter-spacing">
{%= __(report.report_name) %}
</div>
</div>
{% if (subtitle && subtitle.trim()) { %}
<div class="report-subtitle">
{{ subtitle }}
</div>
{% } else { %}
<div class="report-meta">
<div class="left">
<div>
<strong>{%= __("Company") %}:</strong> {%= filters.company %}
</div>
<div>
<strong>{%= __("Currency") %}:</strong>
{%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
</div>
</div>
<div class="right text-right">
<div>
<strong>{%= __("From Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.from_date) %}
</div>
<div>
<strong>{%= __("To Date") %}:</strong>
{%= frappe.datetime.str_to_user(filters.to_date) %}
</div>
</div>
</div>
{% } %}
<div class="report-table">
<table>
<thead>
<tr>
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const align = i === 0 ? "text-left" : "text-right";
const styling = i === 0 ? "" : "width: 9em";
%}
<th class="{%= align %}" style= "{%= styling%}">
{%= col.label %}
</th>
{% } %}
</tr>
</thead>
<tbody>
{% for (let j = 0, k = data.length; j < k; j++) { %}
{%
const row = data[j];
let row_class = "";
if (!(row.parent_account || row.parent_section)) {
row_class = "financial-statements-important";
}
if (!(row.account_name || row.section)) {
row_class += " financial-statements-blank-row";
}
%}
<tr class="{%= row_class %}">
{% for (let i = 0, l = report_columns.length; i < l; i++) { %}
{%
const col = report_columns[i];
const value = row[col.fieldname];
const align = i === 0 ? "text-left" : "text-right";
%}
<td class="{%= align %}">
{% if (i === 0) { %}
<span style="padding-left: {%= cint(row.indent) * 2 %}em">
{%= String(row.account_name || row.section || "").replace(/^['"]|['"]$/g, "") %}
</span>
{% } else if (!is_null(value)) { %}
{%= frappe.format(value, col, {}, row) %}
{% } %}
</td>
{% } %}
</tr>
{% } %}
</tbody>
</table>
</div>
<p class="text-right text-muted">
{%= __("Printed on {0}", [
frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())
]) %}
</p>
</div>

View File

@@ -546,7 +546,7 @@ def reconcile_against_document(
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
)
if referenced_row.get("outstanding_amount"):
if referenced_row.get("outstanding_amount") and entry.get("outstanding_amount") is None:
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
reposting_rows.append(referenced_row)

View File

@@ -26,7 +26,6 @@ class TestAssetCapitalization(ERPNextTestSuite):
def setUp(self):
set_depreciation_settings_in_company()
create_asset_capitalization_data()
frappe.db.sql("delete from `tabTax Rule`")
def test_capitalization_with_perpetual_inventory(self):
company = "_Test Company with perpetual inventory"

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

@@ -2,10 +2,59 @@
// For license information, please see license.txt
frappe.ui.form.on("Buying Settings", {
// refresh: function(frm) {
// }
refresh(frm) {
if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm);
const display = frm.doc.supp_master_name === "Naming Series";
frm.set_df_property("naming_series_details", "hidden", !display);
frm.set_df_property("configure", "hidden", !display);
if (display) {
frm.naming_controller.load_master_series("Supplier", "naming_series_details");
}
frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
},
supp_master_name(frm) {
const display = frm.doc.supp_master_name === "Naming Series";
frm.set_df_property("naming_series_details", "hidden", !display);
frm.set_df_property("configure", "hidden", !display);
if (display) {
frm.naming_controller.load_master_series("Supplier", "naming_series_details");
} else {
frm.doc.naming_series_details = "";
frm.refresh_field("naming_series_details");
}
frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
},
configure(frm) {
frm.naming_controller.show_naming_series_dialog("Supplier", ({ naming_series_options }) => {
frm.doc.naming_series_details = naming_series_options;
frm.refresh_field("naming_series_details");
});
},
});
function get_transactions(frm) {
const transactions = [
{ label: __("Supplier"), doctype: "Supplier" },
{ label: __("Material Request"), doctype: "Material Request" },
{ label: __("Request for Quotation"), doctype: "Request for Quotation" },
{ label: __("Purchase Order"), doctype: "Purchase Order" },
{ label: __("Purchase Invoice"), doctype: "Purchase Invoice" },
{ label: __("Purchase Receipt"), doctype: "Purchase Receipt" },
];
if (frm.doc.supp_master_name !== "Naming Series") {
return transactions.filter((t) => t.doctype !== "Supplier");
}
return transactions;
}
frappe.tour["Buying Settings"] = [
{
fieldname: "supp_master_name",

View File

@@ -6,44 +6,51 @@
"engine": "InnoDB",
"field_order": [
"supplier_and_price_defaults_section",
"supplier_defaults_section",
"supp_master_name",
"supplier_group",
"buying_price_list",
"naming_series_details",
"configure",
"column_break_4",
"supplier_group",
"pricing_tab",
"buying_price_list",
"section_break_vwgg",
"maintain_same_rate",
"column_break_lwxs",
"maintain_same_rate_action",
"role_to_override_stop_action",
"section_break_xmlt",
"po_required",
"blanket_order_allowance",
"column_break_sbwq",
"pr_required",
"project_update_frequency",
"transaction_settings_section",
"column_break_fcyl",
"set_landed_cost_based_on_purchase_invoice_rate",
"allow_zero_qty_in_supplier_quotation",
"use_transaction_date_exchange_rate",
"allow_zero_qty_in_request_for_quotation",
"allow_negative_rates_for_items",
"po_required",
"pr_required",
"project_update_frequency",
"column_break_12",
"maintain_same_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"allow_negative_rates_for_items",
"set_valuation_rate_for_rejected_materials",
"disable_last_purchase_rate",
"show_pay_button",
"purchase_invoice_settings_section",
"bill_for_rejected_quantity_in_purchase_invoice",
"use_transaction_date_exchange_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
"zero_quantity_line_items_section",
"allow_zero_qty_in_supplier_quotation",
"allow_zero_qty_in_request_for_quotation",
"allow_zero_qty_in_purchase_order",
"blanket_order_section",
"blanket_order_allowance",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
"over_transfer_allowance",
"validate_consumed_qty",
"section_break_xcug",
"auto_create_subcontracting_order",
"column_break_izrr",
"auto_create_purchase_receipt",
"request_for_quotation_tab",
"fixed_email"
"fixed_email",
"document_naming_tab",
"transaction_naming_html"
],
"fields": [
{
@@ -54,6 +61,7 @@
"options": "Supplier Name\nNaming Series\nAuto Name"
},
{
"documentation_url": "https://docs.frappe.io/erpnext/buying-settings#2-default-supplier-group",
"fieldname": "supplier_group",
"fieldtype": "Link",
"label": "Default Supplier Group",
@@ -68,26 +76,27 @@
{
"fieldname": "po_required",
"fieldtype": "Select",
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
"label": "Is Purchase Order required for Purchase Invoice & Receipt creation?",
"options": "No\nYes"
},
{
"fieldname": "pr_required",
"fieldtype": "Select",
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
"label": "Is Purchase Receipt required for Purchase Invoice creation?",
"options": "No\nYes"
},
{
"default": "0",
"description": "Warn or stop if Item rate is changed in Purchase Invoice or Purchase Receipt generated from a Purchase Order.",
"fieldname": "maintain_same_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout the Purchase Cycle"
"label": "Maintain same rate throughout the purchase cycle"
},
{
"default": "0",
"fieldname": "allow_multiple_items",
"fieldtype": "Check",
"label": "Allow Item To Be Added Multiple Times in a Transaction"
"label": "Allow Item to be added multiple times in a transaction"
},
{
"fieldname": "subcontract",
@@ -96,9 +105,10 @@
},
{
"default": "BOM",
"documentation_url": "https://docs.frappe.io/erpnext/buying-settings#1-backflush-raw-materials-of-subcontract-based-on",
"fieldname": "backflush_raw_materials_of_subcontract_based_on",
"fieldtype": "Select",
"label": "Backflush Raw Materials of Subcontract Based On",
"label": "Backflush raw materials of subcontract based on",
"options": "BOM\nMaterial Transferred for Subcontract"
},
{
@@ -108,25 +118,21 @@
"fieldtype": "Float",
"label": "Over Transfer Allowance (%)"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "Stop",
"depends_on": "maintain_same_rate",
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action If Same Rate is Not Maintained",
"label": "Action if same rate is not maintained",
"mandatory_depends_on": "maintain_same_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval:doc.maintain_same_rate_action == 'Stop'",
"depends_on": "maintain_same_rate",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"label": "Role allowed to override stop action",
"options": "Role"
},
{
@@ -134,12 +140,12 @@
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice"
"label": "Bill for rejected quantity in Purchase Invoice"
},
{
"fieldname": "supplier_and_price_defaults_section",
"fieldtype": "Tab Break",
"label": "Naming Series and Price Defaults"
"label": "Defaults"
},
{
"fieldname": "column_break_4",
@@ -156,16 +162,17 @@
},
{
"default": "0",
"description": "Prevents the system from automatically using the rate from the last purchase transaction when creating new purchase orders or transactions.",
"fieldname": "disable_last_purchase_rate",
"fieldtype": "Check",
"label": "Disable Last Purchase Rate"
"label": "Disable last purchase rate"
},
{
"default": "1",
"depends_on": "eval: frappe.boot.versions && frappe.boot.versions.payments",
"fieldname": "show_pay_button",
"fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal"
"label": "Show pay button in Purchase Order portal"
},
{
"default": "0",
@@ -193,30 +200,25 @@
"fieldname": "section_break_xcug",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_izrr",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Subcontracting Order (Draft) will be auto-created on submission of Purchase Order.",
"fieldname": "auto_create_subcontracting_order",
"fieldtype": "Check",
"label": "Auto Create Subcontracting Order"
"label": "Auto create Subcontracting Order"
},
{
"default": "0",
"description": "Purchase Receipt (Draft) will be auto-created on submission of Subcontracting Receipt.",
"fieldname": "auto_create_purchase_receipt",
"fieldtype": "Check",
"label": "Auto Create Purchase Receipt"
"label": "Auto create Purchase Receipt"
},
{
"default": "Each Transaction",
"description": "How often should Project be updated of Total Purchase Cost ?",
"fieldname": "project_update_frequency",
"fieldtype": "Select",
"label": "Update frequency of Project",
"label": "How often should project be updated of Total Purchase Cost ?",
"options": "Each Transaction\nManual"
},
{
@@ -240,14 +242,6 @@
"fieldtype": "Check",
"label": "Allow Supplier Quotation with Zero Quantity"
},
{
"fieldname": "section_break_xmlt",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_sbwq",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_fcyl",
"fieldtype": "Column Break"
@@ -258,7 +252,7 @@
"description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.",
"fieldname": "set_valuation_rate_for_rejected_materials",
"fieldtype": "Check",
"label": "Set Valuation Rate for Rejected Materials"
"label": "Set valuation rate for rejected Materials"
},
{
"fieldname": "request_for_quotation_tab",
@@ -279,23 +273,77 @@
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
"label": "Validate consumed quantity (as per BOM)"
},
{
"default": "0",
"fieldname": "allow_negative_rates_for_items",
"fieldtype": "Check",
"label": "Allow Negative rates for Items"
"label": "Allow negative rates for Items"
},
{
"fieldname": "supplier_defaults_section",
"fieldtype": "Section Break",
"label": "Supplier Defaults"
},
{
"fieldname": "section_break_vwgg",
"fieldtype": "Section Break"
},
{
"fieldname": "blanket_order_section",
"fieldtype": "Section Break",
"label": "Blanket Orders"
},
{
"fieldname": "zero_quantity_line_items_section",
"fieldtype": "Section Break",
"label": "Zero-Quantity Line Items"
},
{
"fieldname": "purchase_invoice_settings_section",
"fieldtype": "Section Break",
"label": "Purchase Invoice Settings"
},
{
"fieldname": "column_break_lwxs",
"fieldtype": "Column Break"
},
{
"fieldname": "pricing_tab",
"fieldtype": "Tab Break",
"label": "Pricing"
},
{
"fieldname": "document_naming_tab",
"fieldtype": "Tab Break",
"label": "Document Naming"
},
{
"fieldname": "configure",
"fieldtype": "Button",
"hidden": 1,
"label": "Configure Series"
},
{
"fieldname": "transaction_naming_html",
"fieldtype": "HTML"
},
{
"fieldname": "naming_series_details",
"fieldtype": "Small Text",
"hidden": 1,
"is_virtual": 1,
"label": "Naming Series options"
}
],
"grid_page_length": 50,
"hide_toolbar": 0,
"icon": "fa fa-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-04-15 16:07:35.484787",
"modified": "2026-05-05 16:30:37.184607",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@@ -313,7 +361,6 @@
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"role": "Purchase Manager",

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

@@ -23,6 +23,8 @@
"is_subcontracted",
"has_unit_price_items",
"supplier_warehouse",
"section_break_ahub",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -147,7 +149,7 @@
"column_break_86",
"select_print_heading",
"language",
"subscription_section",
"auto_repeat_section",
"from_date",
"to_date",
"column_break_97",
@@ -1013,12 +1015,6 @@
"label": "Print Language",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"allow_on_submit": 1,
"fieldname": "from_date",
@@ -1309,6 +1305,24 @@
"fieldtype": "Time",
"label": "Time",
"mandatory_depends_on": "is_internal_supplier"
},
{
"collapsible": 1,
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_ahub",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -1316,7 +1330,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-03-25 11:46:18.748951",
"modified": "2026-04-28 06:11:46.904768",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -159,6 +159,7 @@ class PurchaseOrder(BuyingController):
taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_net_weight: DF.Float
@@ -217,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"
@@ -492,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()
@@ -565,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:
@@ -591,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

@@ -16,6 +16,8 @@
"status",
"has_unit_price_items",
"amended_from",
"section_break_mhyw",
"title",
"suppliers_section",
"suppliers",
"items_section",
@@ -371,6 +373,18 @@
"fieldtype": "Text Editor",
"label": "Shipping Address Details",
"read_only": 1
},
{
"fieldname": "section_break_mhyw",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -378,7 +392,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-03-19 15:27:56.730649",
"modified": "2026-04-28 06:18:05.661710",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
@@ -448,5 +462,6 @@
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
"states": [],
"title_field": "company"
}

View File

@@ -63,6 +63,7 @@ class RequestforQuotation(BuyingController):
suppliers: DF.Table[RequestforQuotationSupplier]
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
transaction_date: DF.Date
use_html: DF.Check
vendor: DF.Link | None

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

@@ -9,7 +9,6 @@
"engine": "InnoDB",
"field_order": [
"supplier_section",
"title",
"naming_series",
"supplier",
"supplier_name",
@@ -21,6 +20,8 @@
"quotation_number",
"has_unit_price_items",
"amended_from",
"section_break_kumc",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -111,7 +112,7 @@
"column_break_85",
"select_print_heading",
"language",
"subscription_section",
"auto_repeat_section",
"auto_repeat",
"update_auto_repeat_reference",
"more_info",
@@ -127,10 +128,9 @@
"options": "fa fa-user"
},
{
"default": "{supplier_name}",
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1,
@@ -736,11 +736,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "auto_repeat",
"fieldtype": "Link",
@@ -940,6 +935,15 @@
"no_copy": 1,
"options": "Item Wise Tax Detail",
"print_hide": 1
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_kumc",
"fieldtype": "Section Break"
}
],
"grid_page_length": 50,
@@ -948,7 +952,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-01-29 21:23:13.778468",
"modified": "2026-04-28 06:23:52.813948",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
@@ -1016,5 +1020,5 @@
"sort_order": "DESC",
"states": [],
"timeline_field": "supplier",
"title_field": "title"
"title_field": "supplier_name"
}

View File

@@ -13,7 +13,6 @@ class TestSupplierScorecard(ERPNextTestSuite):
self.assertEqual(doc.name, valid_scorecard[0].get("supplier"))
def test_criteria_weight(self):
delete_test_scorecards()
my_doc = make_supplier_scorecard()
for d in my_doc.criteria:
d.weight = 0
@@ -33,26 +32,6 @@ def make_supplier_scorecard():
return my_doc
def delete_test_scorecards():
my_doc = make_supplier_scorecard()
if frappe.db.exists("Supplier Scorecard", my_doc.name):
# Delete all the periods, then delete the scorecard
frappe.db.sql(
"""delete from `tabSupplier Scorecard Period` where scorecard = %(scorecard)s""",
{"scorecard": my_doc.name},
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.delete_doc(my_doc.doctype, my_doc.name)
valid_scorecard = [
{
"standings": [

View File

@@ -9,41 +9,15 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestSupplierScorecardCriteria(ERPNextTestSuite):
def test_variables_exist(self):
delete_test_scorecards()
for d in test_good_criteria:
frappe.get_doc(d).insert()
self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[0]).insert)
def test_formula_validate(self):
delete_test_scorecards()
self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[1]).insert)
def delete_test_scorecards():
# Delete all the periods so we can delete all the criteria
frappe.db.sql("""delete from `tabSupplier Scorecard Period`""")
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'"""
)
for d in test_good_criteria:
if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")):
# Delete all the periods, then delete the scorecard
frappe.delete_doc(d.get("doctype"), d.get("name"))
for d in test_bad_criteria:
if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")):
# Delete all the periods, then delete the scorecard
frappe.delete_doc(d.get("doctype"), d.get("name"))
test_good_criteria = [
{
"name": "Delivery",

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,7 +461,7 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
)
net_rate = item.qty * item.base_net_rate
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()

View File

@@ -10,7 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestContract(ERPNextTestSuite):
def setUp(self):
frappe.db.sql("delete from `tabContract`")
self.contract_doc = get_contract()
def test_validate_start_date_before_end_date(self):

View File

@@ -87,9 +87,6 @@ class TestLead(ERPNextTestSuite):
self.assertEqual(len(address_1.get("links")), 1)
def test_prospect_creation_from_lead(self):
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
lead = make_lead(
first_name="Rahul",
last_name="Tripathi",
@@ -109,9 +106,6 @@ class TestLead(ERPNextTestSuite):
self.assertEqual(event.event_participants[1].reference_docname, prospect)
def test_opportunity_from_lead(self):
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
frappe.db.sql("delete from `tabOpportunity` where party_name='Rahul Tripathi'")
lead = make_lead(
first_name="Rahul",
last_name="Tripathi",
@@ -139,9 +133,6 @@ class TestLead(ERPNextTestSuite):
)
def test_copy_events_from_lead_to_prospect(self):
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
lead = make_lead(
first_name="Rahul",
last_name="Tripathi",

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