Compare commits

..

133 Commits

Author SHA1 Message Date
rohitwaghchaure
06ffe52d6e Merge pull request #54681 from rohitwaghchaure/fixed-support-66529
fix: incorrect expense account book in purchase return
2026-05-01 08:12:31 +05:30
Raffael Meyer
c120cc7ed1 fix: add missing fields in set_currency_labels (#54689) 2026-05-01 03:54:14 +02:00
Raffael Meyer
25be38e23c fix: Backfill not_applicable on Item Tax Template Details for German companies (#54682) 2026-04-30 19:21:24 +00:00
Rohit Waghchaure
2a720e7008 fix: incorrect expense account book in purchase return 2026-04-30 20:36:20 +05:30
Raffael Meyer
f38eca9124 fix: mark item tax templates as not applicable (#54673)
* fix: mark item tax templates as not applicable

For new German charts of accounts, mark accounts for different tax rates as *Not Applicable* in **Item Tax Templates**.

* fix: wrong applicable rate 19 in template 7
2026-04-30 11:44:08 +00:00
rohitwaghchaure
ad89f88c93 Merge pull request #54664 from rohitwaghchaure/fixed-support-66924
fix: show in and out qty in the stock ledger report for stock recos
2026-04-30 14:13:42 +05:30
Trusted Computer
78f654765d fix: correct titles set to {customer_name} or {supplier_name} text strings (#54656) 2026-04-30 10:28:14 +02:00
Hemil-Sangani
231dd1856f 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

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

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

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

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-30 07:53:32 +00:00
Rohit Waghchaure
da081254a6 fix: show in and out qty in the stock ledger report for stock recos 2026-04-30 13:16:13 +05:30
Raffael Meyer
c543d15f3c feat: copy terms attachments to transactions (#53403) 2026-04-29 21:14:58 +00:00
Khushi Rawat
ddf0e35009 Merge pull request #54658 from khushi8112/skip-rescheduling-for-fully-depreciated-asset-sale
fix: skip depreciation rescheduling when asset is fully depreciated on sale
2026-04-30 02:34:10 +05:30
khushi8112
88b82383f5 fix: skip rescheduling only for asset being disposed 2026-04-30 02:11:57 +05:30
khushi8112
c4155b6c81 fix: skip depreciation rescheduling when asset is fully depreciated on sale 2026-04-30 02:01:57 +05:30
Mihir Kandoi
a04c028522 fix: correct project filter in buying doctypes (#54644) 2026-04-29 11:27:47 +00:00
diptanilsaha
5c5a5361bc fix(payment_entry): convert the date args to string type before escaping in get_outstanding_reference_documents (#54639) 2026-04-29 16:38:57 +05:30
Mihir Kandoi
060defcc2b fix: dont show serial/batch button when PR is submitted (#54642) 2026-04-29 16:36:31 +05:30
Mihir Kandoi
d0d8cff48f fix: py error on sales forecast doctype (#54641)
fix: py error on sales forecase doctype
2026-04-29 10:49:05 +00:00
Nishka Gosalia
844f3dbc0b feat(ux): Naming series dialog (#54554) 2026-04-29 14:45:10 +05:30
Khushi Rawat
43937acd8b 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
2026-04-29 14:44:55 +05:30
Mihir Kandoi
503b5bf140 perf: max recursion depth error in serial no (#54629) 2026-04-29 08:34:08 +00:00
rohitwaghchaure
3542087003 Merge pull request #54567 from barredterra/sn-ledger-status
fix: show correct status in Serial No Ledger
2026-04-29 12:42:54 +05:30
Pandiyan P
d68801e73a fix(selling): blanket order ordered qty recalculation on sales order status change (#54593) 2026-04-29 11:57:40 +05:30
Nishka Gosalia
addec3aa8f Merge pull request #53295 from aerele/project-not-copied-from-first-item-row 2026-04-29 11:38:07 +05:30
MochaMind
b001884f9d fix: sync translations from crowdin (#54607) 2026-04-29 01:52:16 +05:30
Ravibharathi
d1a80d40c4 fix: avoid double reduction of pe reference outstanding (#54193)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-29 01:46:55 +05:30
Ravibharathi
a8030c9713 fix: filter overdue purchase order items by company (#54099) 2026-04-29 00:58:01 +05:30
Mihir Kandoi
54f20de7e3 fix: duplicate entries being shown in batch exists in future transact… (#54604)
fix: duplicate entries being shown in batch exists in future transactions msg
2026-04-28 16:28:53 +00:00
diptanilsaha
f8893b04d5 refactor(sms_center): replaced raw SQL queries with Query Builder (#54600) 2026-04-28 15:15:46 +00:00
Lakshit Jain
1bade56e37 Merge pull request #54362 from frappe/ignore-opening-check
fix: filter opening entries after closing voucher
2026-04-28 18:43:25 +05:30
Lakshit Jain
a2b96799ff Merge pull request #54517 from vorasmit/exclude-pcv
fix: always exclude pcv entries except for closing account head
2026-04-28 18:42:42 +05:30
Smit Vora
d0f0e38e8d Merge pull request #54479 from Abdeali099/cash-flow-fixes 2026-04-28 17:30:32 +05:30
Smit Vora
590f2ffe28 test: include both accounts to test sum = 0 2026-04-28 16:45:26 +05:30
diptanilsaha
084c7f72f0 fix(get_stock_balance): validate inventory dimension fieldnames (#54587) 2026-04-28 16:41:22 +05:30
Smit Vora
84aa54c540 test: pcv is excluded from PL accounts 2026-04-28 16:30:02 +05:30
Smit Vora
5fc3ca1d4b test: opening entries after period closing 2026-04-28 16:02:32 +05:30
diptanilsaha
d62fa3c464 fix(payment_entry): escape arguments on invoice and order fetching sql queries (#54582) 2026-04-28 15:55:45 +05:30
diptanilsaha
07337ba9da chore(sidebar): moved Inactive Customers from CRM to Selling Workspace Sidbar (#54578) 2026-04-28 09:31:51 +00:00
Mihir Kandoi
2088a01c19 fix: update status of quotation in patch (#54577) 2026-04-28 09:20:41 +00:00
ravibharathi656
68cc518497 fix: copy project to new item row from parent 2026-04-28 13:10:48 +05:30
Sudharsanan Ashok
6f9089dd5b fix(manufacturing): remove conversion factor for stock qty (#54525) 2026-04-28 10:45:54 +05:30
Vinay Mishra
63edd5ddc6 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:
2026-04-28 05:12:49 +00:00
barredterra
2b3e047143 fix: show correct status in Serial No Ledger 2026-04-27 21:41:38 +02:00
barredterra
cb2e6e1e2e refactor: extract SN status logic 2026-04-27 21:41:12 +02:00
MochaMind
37e3493ec4 fix: sync translations from crowdin (#54520) 2026-04-27 20:53:04 +02:00
Mihir Kandoi
601581d6f8 fix: debit credit not equal in purchase transactions for multi currency (#54456) 2026-04-27 20:30:41 +05:30
ruthra kumar
837cdc9cc3 Merge pull request #54509 from ruthra-kumar/hide_toggleable_fields
fix: hide feature flag controlled fields on install
2026-04-27 14:43:11 +05:30
Mihir Kandoi
5281d60f2d fix: correct display depends on condition (#54548) 2026-04-27 09:07:36 +00:00
Mihir Kandoi
0aadd1e3a5 fix: make inv dimen reqd only in delivery note (#54546) 2026-04-27 08:28:55 +00:00
Pandiyan P
60a6b38c31 fix(stock): remove validation for transfer_qty field (#54542) 2026-04-27 06:56:30 +00:00
Mihir Kandoi
be2a4b7b2a refactor: quality inspection item query (#54511) 2026-04-27 10:45:25 +05:30
MochaMind
5c839f60e4 chore: update POT file (#54536) 2026-04-26 18:55:27 +02:00
rohitwaghchaure
6e77a45c05 Merge pull request #54514 from aerele/fix/incoming-rate-issue
fix(stock): set incoming rate as zero for outward sle
2026-04-26 10:06:35 +05:30
rohitwaghchaure
2a6ddc7f67 Merge pull request #54530 from aerele/fix/support-#66029
fix(stock): show item code in serial and batch selector dialog
2026-04-26 10:04:41 +05:30
Sudharsanan11
fee5bcadb2 fix(stock): add stock entry in batch master connection 2026-04-26 00:05:19 +05:30
Sudharsanan11
f572bc51e1 fix(stock): show item code in serial and batch selector dialog 2026-04-26 00:05:19 +05:30
Nishka Gosalia
fba33b7e7a refactor(UX): selling settings form (#54412)
refactor(UX): Selling settings form cleanup
2026-04-25 15:27:32 +05:30
diptanilsaha
ebca389136 fix(PCV): set correct filters of from_date and to_date on General Ledger Report on clicking Ledger button (#54522) 2026-04-25 00:03:38 +05:30
Smit Vora
c94b8c41f3 chore: comment 2026-04-24 19:32:00 +05:30
mahsem
e517eeaaa2 feat: danish_bosnian_address_template (#54093) 2026-04-24 14:54:37 +02:00
Khushi Rawat
c3931d4e29 Merge pull request #53843 from Shllokkk/ap-print-format
feat: Accounts Payable print template revamp and print format introduction
2026-04-24 17:51:45 +05:30
Khushi Rawat
0b9fdcd8cd Merge pull request #53870 from Shllokkk/arap-summary-print-formats
feat: AR and AP summary reports print template revamp and print format introduction
2026-04-24 17:42:52 +05:30
Khushi Rawat
b4e941835b Merge pull request #53822 from Shllokkk/ar-print-format
feat: Accounts Receivable print template revamp and print format introduction
2026-04-24 17:41:13 +05:30
Khushi Rawat
9132f0fc4a Merge pull request #53762 from Shllokkk/gl-print-format
feat: General ledger print template revamp and print format introduction
2026-04-24 17:39:45 +05:30
Sudharsanan11
ce37530e70 fix(stock): set incoming rate as zero for outward sle 2026-04-24 17:29:13 +05:30
ruthra kumar
889fdf2f11 fix: hide feature flag controlled fields on install 2026-04-24 17:13:36 +05:30
Smit Vora
5518e8c99f Merge pull request #54480 from ljain112/fix-change-customer 2026-04-24 13:19:32 +05:30
Smit Vora
419b9b3279 Merge pull request #54476 from ljain112/fix-tds-threshhold
fix: ensure tax withholding entries respect date range of category
2026-04-24 13:18:25 +05:30
Khushi Rawat
a9e6f8efd8 Merge pull request #53314 from aerele/budget-validation-on-cancel
fix: skip budget validation when cancelling GL entries
2026-04-24 12:14:09 +05:30
Mihir Kandoi
0e20e35842 fix: preserve inventory dimensions when raw materials are reset (#54440)
* fix: preserve inventory dimensions when raw materials are reset

* test: add test case
2026-04-23 17:16:12 +00:00
Raffael Meyer
b4107b8fd5 test(Code List): check content, not filename (#54490) 2026-04-23 15:40:22 +00:00
Raffael Meyer
a165b240a7 fix(edi): hardcode "Code List" DocType in importer (#54488) 2026-04-23 13:48:18 +00:00
Abdeali Chharchhodawala
f6639db0e9 feat: enhance account category with root type (#53190) 2026-04-23 17:34:37 +05:30
Abdeali Chharchhodawala
c35221852a feat: Add XLSX styling support to custom financial report templates (#52612) 2026-04-23 17:15:41 +05:30
Abdeali Chharchhoda
3854d2cbf6 chore: minor fix 2026-04-23 17:13:01 +05:30
Sudharsanan Ashok
ab19b16fe2 fix(stock): show available qty in warehouse link field (#54474) 2026-04-23 17:08:54 +05:30
Abdeali Chharchhoda
1fd6c3ba1a fix: update account identification to avoid using name_field in financial statements 2026-04-23 17:05:33 +05:30
Abdeali Chharchhoda
4274c2aba3 fix: add filter labels and required filters for financial report validation 2026-04-23 16:29:38 +05:30
Abdeali Chharchhoda
79d6a51e1e fix: update fiscal year filter to use mandatory_depends_on instead of reqd 2026-04-23 15:54:42 +05:30
ljain112
4eb9107e22 fix: update type hint for get_item_tax_template function 2026-04-23 15:50:03 +05:30
Abdeali Chharchhoda
5a915cb45e fix: ensure fiscal year is checked before validating date filters in financial statements 2026-04-23 15:43:20 +05:30
Smit Vora
b8c3765b85 Merge pull request #54449 from vorasmit/tds-reports-refactor
refactor: tax witholding report
2026-04-23 14:57:31 +05:30
ljain112
9ead8d4e3f fix: ensure tax withholding entries respect date range of category 2026-04-23 13:35:46 +05:30
Raffael Meyer
7f8fa7cf5e ci: test correctness pattern (#54186) 2026-04-22 22:00:42 +02:00
rohitwaghchaure
fd4cedf5e4 Merge pull request #54471 from rohitwaghchaure/fixed-delivery-schedule
fix: delivery schedule in the sales order
2026-04-22 22:02:15 +05:30
Rohit Waghchaure
435db260ee fix: delivery schedule in the sales order 2026-04-22 21:36:51 +05:30
Mihir Kandoi
f5357c233d fix: py error on stock ageing report (#54467) 2026-04-22 19:45:12 +05:30
diptanilsaha
0d2da6d86c ci: fix timezone for python mariadb tests (#54464) 2026-04-22 17:46:28 +05:30
Smit Vora
0349e7a0b8 fix: always exclude pcv entries except for closing account head 2026-04-22 16:09:42 +05:30
Smit Vora
7ae91cac01 fix: summing of values could be zero even if values exist 2026-04-22 13:27:36 +05:30
Smit Vora
b925469c4d fix: add party type for dynamic link support 2026-04-22 12:06:38 +05:30
Smit Vora
f0ea20e579 refactor: make report extensible by regional apps 2026-04-22 12:04:35 +05:30
ruthra kumar
3faeb1609b Merge pull request #54447 from ruthra-kumar/test_remove_raw_sql_delete_on_setup
refactor(test): remove explicit sql delete calls
2026-04-22 11:12:59 +05:30
ruthra kumar
b16dd3f2dd refactor(test): remove explicit sql delete calls 2026-04-22 10:33:25 +05:30
MochaMind
ffae7e42d3 fix: sync translations from crowdin (#54454) 2026-04-22 00:26:57 +05:30
Smit Vora
b5550f747e test: None is better than zero, as no values exist 2026-04-21 19:33:29 +05:30
Shllokkk
f6adef45bf Merge pull request #54307 from aerele/fix/populate_project_from_pe
fix(accounts): fetch project name from payment entry to journal entry
2026-04-21 18:57:45 +05:30
Smit Vora
07b023a934 refactor: updated key for withholding_date 2026-04-21 18:45:26 +05:30
Smit Vora
53666974a3 refactor: better label for entity type 2026-04-21 18:29:50 +05:30
Smit Vora
c3e7f7f02f refactor: how data is built 2026-04-21 18:04:02 +05:30
ruthra kumar
75a068aea8 Merge pull request #54446 from ruthra-kumar/wrong_type_hint_in_pos
fix: incorrect type hint
2026-04-21 17:56:01 +05:30
Smit Vora
6dca96b423 refactor: use consistent report column names 2026-04-21 17:28:56 +05:30
Smit Vora
f6eb844d20 Merge pull request #54422 from ljain112/fix-test-tds-report 2026-04-21 17:21:40 +05:30
Smit Vora
6d727c90b6 Merge pull request #54272 from ljain112/parrenttype 2026-04-21 17:20:56 +05:30
Smit Vora
d8fc9444ea Merge pull request #54344 from ljain112/project-filter-ar-ap 2026-04-21 17:15:37 +05:30
Mihir Kandoi
e65b9fc2ae fix: sales order is not valid when creating WO from MR from PP (#54435) 2026-04-21 09:47:02 +00:00
ruthra kumar
1995fcfdd8 fix: incorrect type hint 2026-04-21 13:57:12 +05:30
MochaMind
c2590c174d fix: sync translations from crowdin (#54358) 2026-04-21 09:24:40 +05:30
diptanilsaha
11fc3e5495 refactor: Sales Partner Commission Summary and Sales Partner Transaction Summary report (#54268) 2026-04-21 03:12:22 +00:00
Khushi Rawat
0edee23e53 Merge pull request #54131 from khushi8112/journal-entry-custom-remark-toggle
feat: use single remark field with custom remark toggle
2026-04-21 00:45:46 +05:30
ljain112
6545bcbbd9 refactor: fix test cases in tax withholding details report 2026-04-20 18:15:10 +05:30
vorasmit
3c8a066484 fix: filter opening entries in first year in custom financial statement 2026-04-17 22:27:48 +05:30
sarathibalamurugan
9eeb819106 test: add test for project name in exchange gain loss entry 2026-04-17 18:44:44 +05:30
sarathibalamurugan
d9b255b952 fix(accounts): fetch project name from payment entry to journal entry 2026-04-17 18:43:50 +05:30
ljain112
0cad511136 test: add test with project not in payment entry 2026-04-17 15:18:57 +05:30
ljain112
d51dbf5254 fix: add project filter to accounts payable and receivable reports 2026-04-17 14:00:01 +05:30
ljain112
3aeb7d6b01 fix(purchase_register): filter tax rows by parenttype in invoice tax map query 2026-04-14 12:31:10 +05:30
khushi8112
84e5272f5d fix: append row level user remarks in gl map 2026-04-13 15:15:52 +05:30
khushi8112
697f521e14 feat: use single remark field with custom remark toggle 2026-04-13 15:15:45 +05:30
Shllokkk
44e0b36093 fix: minor changes in print templates 2026-04-12 13:14:24 +05:30
Shllokkk
915fcc0166 fix: minor changes in print template 2026-04-12 13:07:41 +05:30
Shllokkk
e3019c827c fix: minor changes in print template 2026-04-12 13:00:51 +05:30
Shllokkk
e8d08df044 fix: changes to gl print template 2026-04-11 23:06:01 +05:30
Shllokkk
4228885f1e fix: minor bug fixes for ar print template 2026-04-07 13:01:18 +05:30
Shllokkk
e6a32a9d02 feat: introduce print format for Accounts Receivable report 2026-04-07 13:01:18 +05:30
Shllokkk
ffc59ebc9c fix: improve design and refactor ar print template 2026-04-07 13:01:18 +05:30
Shllokkk
86ee9959a2 fix: minor bugs in print templates 2026-04-01 12:32:15 +05:30
Shllokkk
5bbcb73808 fix: revamp print formats for accounts receivable summary and accounts payable summary reports 2026-03-29 02:02:59 +05:30
Shllokkk
2bf9d41797 feat: add print format for accounts payable report 2026-03-27 16:03:20 +05:30
Shllokkk
c051536182 refactor: revamp print template for accounts payable report 2026-03-27 12:32:17 +05:30
Shllokkk
0d4f56bf84 refactor: table body data rendering cleanup 2026-03-26 21:56:51 +05:30
Shllokkk
9660debe28 fix: improve filter details render logic to avoid showing duplicate information 2026-03-26 16:18:51 +05:30
Shllokkk
3ba36212b0 refactor: clean and standardize print template for general ledger report 2026-03-24 15:32:13 +05:30
nareshkannasln
fa34ebea94 fix: skip BudgetValidation when cancelling GL entries 2026-03-11 11:59:14 +05:30
191 changed files with 52629 additions and 42719 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' }}

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

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

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

@@ -8,6 +8,7 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_engine
DependencyResolver,
FilterExpressionParser,
FinancialQueryBuilder,
FinancialReportEngine,
FormulaCalculator,
)
from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import (
@@ -1949,6 +1950,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"
@@ -2022,3 +2176,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

@@ -648,7 +648,7 @@ $.extend(erpnext.journal_entry, {
reqd: 1,
default: frm.doc.posting_date,
},
{ fieldtype: "Small Text", fieldname: "user_remark", label: __("User Remark") },
{ fieldtype: "Small Text", fieldname: "remark", label: __("Remark") },
{
fieldtype: "Select",
fieldname: "naming_series",
@@ -665,8 +665,14 @@ $.extend(erpnext.journal_entry, {
var values = dialog.get_values();
frm.set_value("posting_date", values.posting_date);
frm.set_value("user_remark", values.user_remark);
frm.set_value("naming_series", values.naming_series);
if (values.remark) {
frm.set_value("custom_remark", 1);
frm.set_value("remark", values.remark);
} else {
frm.set_value("custom_remark", 0);
frm.set_value("remark", "");
}
// clear table is used because there might've been an error while adding child
// and cleanup didn't happen

View File

@@ -78,6 +78,7 @@
"from_template",
"title",
"column_break3",
"custom_remark",
"remark",
"mode_of_payment",
"party_not_required"
@@ -202,6 +203,7 @@
{
"fieldname": "user_remark",
"fieldtype": "Small Text",
"hidden": 1,
"label": "User Remark",
"no_copy": 1,
"oldfieldname": "user_remark",
@@ -315,7 +317,7 @@
"no_copy": 1,
"oldfieldname": "remark",
"oldfieldtype": "Small Text",
"read_only": 1
"read_only_depends_on": "eval: !doc.custom_remark"
},
{
"depends_on": "eval:doc.voucher_type== \"Inter Company Journal Entry\"",
@@ -651,6 +653,12 @@
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"default": "0",
"fieldname": "custom_remark",
"fieldtype": "Check",
"label": "Custom Remark"
}
],
"icon": "fa fa-file-text",
@@ -665,7 +673,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-03-09 17:15:26.569327",
"modified": "2026-04-08 14:19:30.870894",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -62,6 +62,7 @@ class JournalEntry(AccountsController):
cheque_no: DF.Data | None
clearance_date: DF.Date | None
company: DF.Link
custom_remark: DF.Check
difference: DF.Currency
due_date: DF.Date | None
finance_book: DF.Link | None
@@ -1027,8 +1028,8 @@ class JournalEntry(AccountsController):
if self.flags.skip_remarks_creation:
return
if self.user_remark:
r.append(_("Note: {0}").format(self.user_remark))
if self.get("custom_remark"):
return
if self.cheque_no:
if self.cheque_date:
@@ -1571,7 +1572,7 @@ def get_against_jv(
frappe.qb.from_(JournalEntry)
.join(JournalEntryAccount)
.on(JournalEntryAccount.parent == JournalEntry.name)
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.user_remark)
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.remark)
.where(JournalEntryAccount.account == filters.get("account"))
.where(JournalEntryAccount.reference_type.isnull() | (JournalEntryAccount.reference_type == ""))
.where(JournalEntry.docstatus == 1)

View File

@@ -1,5 +1,5 @@
frappe.listview_settings["Journal Entry"] = {
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"],
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark"],
get_indicator: function (doc) {
if (doc.docstatus === 1) {
return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`];

View File

@@ -523,7 +523,7 @@ class TestJournalEntry(ERPNextTestSuite):
jv = frappe.new_doc("Journal Entry")
jv.posting_date = nowdate()
jv.company = "_Test Company"
jv.user_remark = "test"
jv.remark = "test"
jv.extend(
"accounts",
[
@@ -592,6 +592,14 @@ class TestJournalEntry(ERPNextTestSuite):
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
def test_custom_remark(self):
# When custom_remark is enabled, remark should not be auto-overwritten on save
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
jv.custom_remark = 1
jv.remark = "My custom remark text"
jv.insert()
self.assertEqual(jv.remark, "My custom remark text")
def test_credit_limit_for_customer(self):
customer = make_customer("_Test New Customer")
set_credit_limit("_Test New Customer", "_Test Company", 50)
@@ -620,7 +628,7 @@ def make_journal_entry(
jv = frappe.new_doc("Journal Entry")
jv.posting_date = posting_date or nowdate()
jv.company = company or "_Test Company"
jv.user_remark = "test"
jv.remark = "test"
jv.multi_currency = 1
jv.set(
"accounts",

View File

@@ -2308,22 +2308,20 @@ def get_outstanding_reference_documents(args: str | dict, validate: bool = 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 = {
@@ -2332,18 +2330,19 @@ def get_outstanding_reference_documents(args: str | dict, validate: bool = 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"):
@@ -2563,7 +2562,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",
@@ -2105,6 +2129,37 @@ class TestPaymentEntry(ERPNextTestSuite):
self.assertEqual(ref.voucher_no, so.name)
self.assertIsNotNone(ref.payment_term)
def test_project_name_in_exchange_gain_loss_entry(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_submit=True,
)
from erpnext.projects.doctype.project.test_project import make_project
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
si.submit()
pe = get_payment_entry("Sales Invoice", si.name)
pe.source_exchange_rate = 100
pe.insert()
pe.submit()
rows = frappe.get_all(
"Journal Entry Account",
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
fields=["project"],
)
self.assertEqual(len(rows), 2)
self.assertEqual(rows[0].project, si.project)
self.assertEqual(rows[1].project, si.project)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

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

@@ -17,9 +17,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")
@@ -69,9 +66,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()
@@ -135,9 +129,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")
@@ -189,9 +180,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")
@@ -212,10 +200,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

@@ -200,7 +200,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

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

View File

@@ -755,7 +755,7 @@ class POSInvoice(SalesInvoice):
return profile
@frappe.whitelist()
def set_missing_values(self, for_validate: bool = False):
def set_missing_values(self, for_validate: bool | None = False):
profile = self.set_pos_fields(for_validate)
if not self.debit_to:
@@ -1027,7 +1027,7 @@ def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
@frappe.whitelist()
def make_sales_return(source_name: str, target_doc: Document | None = None):
def make_sales_return(source_name: str, target_doc: Document | str | None = None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("POS Invoice", source_name, target_doc)

View File

@@ -34,7 +34,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

@@ -33,7 +33,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})
@@ -63,7 +62,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})
@@ -122,7 +120,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

@@ -15,7 +15,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))
@@ -1584,16 +1583,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

@@ -443,13 +443,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 +559,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

@@ -552,12 +552,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

@@ -1150,6 +1150,7 @@
"hide_seconds": 1,
"label": "Rounding Adjustment",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1162,6 +1163,7 @@
"label": "Rounded Total",
"oldfieldname": "rounded_total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -2355,7 +2357,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-03-30 12:17:16.201016",
"modified": "2026-05-01 02:37:29.742764",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

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

@@ -9,8 +9,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

@@ -642,6 +642,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

@@ -35,7 +35,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

View File

@@ -8,9 +8,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

@@ -34,6 +34,17 @@ frappe.query_reports["Accounts Payable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_account",
label: __("Payable Account"),

View File

@@ -117,3 +117,49 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 300.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])

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

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

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

@@ -36,6 +36,17 @@ frappe.query_reports["Accounts Receivable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -196,6 +196,7 @@ class ReceivablePayableReport:
and ple.against_voucher_type in self.advance_payment_doctypes
):
self.voucher_balance[key].cost_center = ple.cost_center
self.voucher_balance[key].project = ple.project
self.get_invoices(ple)
@@ -362,6 +363,7 @@ class ReceivablePayableReport:
posting_date,
account_currency,
cost_center,
project,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
@@ -390,6 +392,7 @@ class ReceivablePayableReport:
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
"project",
]:
_d[field] = x.get(field)
@@ -931,6 +934,7 @@ class ReceivablePayableReport:
ple.against_voucher_no,
ple.party_type,
ple.cost_center,
ple.project,
ple.party,
ple.posting_date,
ple.due_date,
@@ -998,6 +1002,9 @@ class ReceivablePayableReport:
if self.filters.cost_center:
self.get_cost_center_conditions()
if self.filters.project:
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
@@ -1240,6 +1247,7 @@ class ReceivablePayableReport:
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Project"), fieldname="project", fieldtype="Link", options="Project")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
self.add_column(
label=_("Voucher No"),
@@ -1423,6 +1431,7 @@ class InitSQLProceduresForAR:
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
@@ -1442,6 +1451,7 @@ class InitSQLProceduresForAR:
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
@@ -1457,7 +1467,7 @@ class InitSQLProceduresForAR:
begin
if not exists (select name from `{_voucher_balance_name}` where name = sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)))
then
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
@@ -1499,7 +1509,7 @@ class InitSQLProceduresForAR:
end if;
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
end;
"""

View File

@@ -1194,3 +1194,52 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(len(report[1]), 2)
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 100.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project even when the payment entry
has no project set.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
# payment has no project — report row must still show the invoice's project
self.create_payment_entry(si.name)
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])

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

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

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

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

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

@@ -499,7 +499,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
from `tabPurchase Taxes and Charges`
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
group by parent, account_head, add_deduct_tax
"""
% ", ".join(["%s"] * len(invoice_list)),

View File

@@ -5,14 +5,12 @@ import frappe
from frappe.utils import add_months, today
from erpnext.accounts.report.purchase_register.purchase_register import execute
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
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()
@@ -26,10 +24,50 @@ class TestPurchaseRegister(ERPNextTestSuite):
self.assertEqual(first_row.total_tax, 100)
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'")
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
# Real workflow setup: create a Purchase Receipt tax row in the same shared child table.
pr = make_purchase_receipt(
company="_Test Company 6",
supplier="_Test Supplier",
item="_Test Item",
warehouse="_Test Warehouse - _TC6",
cost_center="_Test Cost Center - _TC6",
do_not_save=1,
do_not_submit=1,
qty=1,
rate=1000,
)
pr.append(
"taxes",
{
"account_head": "GST - _TC6",
"cost_center": "_Test Cost Center - _TC6",
"add_deduct_tax": "Add",
"category": "Valuation and Total",
"charge_type": "Actual",
"description": "PR Tax",
"tax_amount": 100.0,
"rate": 100,
},
)
pr.insert()
pr.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Purchase Receipt", pr.name, pi.name, force=True)
report_results = execute(filters)
first_row = frappe._dict(report_results[1][0])
self.assertEqual(first_row.voucher_no, pi.name)
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ledger_view(self):
filters = frappe._dict(
company="_Test Company 6",
from_date=add_months(today(), -1),

View File

@@ -4,6 +4,7 @@ from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
@@ -72,6 +73,43 @@ class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_sales_register_ignores_tax_rows_from_other_doctype(self):
si = self.create_sales_invoice(rate=98)
# Real workflow setup: create a Sales Order with taxes in the shared child table.
so = make_sales_order(
item=self.item,
company=self.company,
customer=self.customer,
rate=77,
do_not_save=1,
do_not_submit=1,
)
so.append(
"taxes",
{
"charge_type": "Actual",
"account_head": self.income_account,
"description": "SO Tax",
"tax_amount": 55.0,
},
)
so.insert()
so.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Sales Order", so.name, si.name, force=True)
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
report = execute(filters)
self.assertEqual(len(report[1]), 1)
result = frappe._dict(report[1][0])
self.assertEqual(result.voucher_no, si.name)
self.assertEqual(result.net_total, 98.0)
self.assertEqual(result.tax_total, 0)
self.assertEqual(result.grand_total, 98.0)
def test_journal_with_cost_center_filter(self):
je1 = frappe.get_doc(
{

View File

@@ -6,299 +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 and transaction date
data.sort(key=lambda x: (x["section_code"] or "", x["transaction_date"] 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

@@ -5,10 +5,12 @@ import frappe
from frappe.utils import add_to_date, today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
create_purchase_invoice,
create_records,
create_sales_invoice,
create_tax_withholding_category,
make_journal_entry_with_tax_withholding,
)
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
@@ -20,43 +22,45 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.clear_old_entries()
create_tax_accounts()
create_records()
def test_tax_withholding_for_customers(self):
create_tax_category(cumulative_threshold=300)
frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS")
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "TCS")
si = create_sales_invoice(customer="Test TCS Customer", rate=1000)
si.submit()
create_tcs_payment_entry()
jv = create_tcs_journal_entry()
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
# Check for JV totals using back calculation logic
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
[jv.name, "TCS", 0.075, 1000.75, 0.75, 1000.75],
["", "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)
def test_single_account_for_multiple_categories(self):
create_tax_category("TDS - 1", rate=10, account="TDS - _TC")
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_1.tax_withholding_category = "TDS - 1"
create_tax_category("TDS - 1", rate=10, account="TDS - _TC", cumulative_threshold=1)
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "TDS - 1")
inv_1 = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
inv_1.submit()
create_tax_category("TDS - 2", rate=20, account="TDS - _TC")
inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_2.tax_withholding_category = "TDS - 2"
create_tax_category("TDS - 2", rate=20, account="TDS - _TC", cumulative_threshold=1)
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "TDS - 2")
inv_2 = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
inv_2.submit()
result = execute(
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
)[1]
expected_values = [
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
[inv_1.name, "TDS - 1", 10, 5000, 500, 4500],
[inv_2.name, "TDS - 2", 20, 5000, 1000, 4000],
]
self.check_expected_values(result, expected_values)
@@ -81,20 +85,21 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
tds_doc.save()
inv_1 = make_purchase_invoice(
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", tds_doc.name)
inv_1 = create_purchase_invoice(
supplier="Test TDS Supplier",
rate=5000,
posting_date=add_to_date(fiscal_year[1], days=1),
set_posting_time=True,
)
inv_1.set_posting_time = 1
inv_1.apply_tds = 1
inv_1.tax_withholding_category = tds_doc.name
inv_1.save()
inv_1.submit()
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
inv_2.set_posting_time = 1
inv_2.apply_tds = 1
inv_2.tax_withholding_category = tds_doc.name
inv_2.save()
inv_2 = create_purchase_invoice(
supplier="Test TDS Supplier",
rate=5000,
posting_date=from_date,
set_posting_time=True,
)
inv_2.submit()
result = execute(
@@ -113,12 +118,13 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
self.check_expected_values(result, expected_values)
def check_expected_values(self, result, expected_values):
self.assertEqual(len(result), len(expected_values))
for i in range(len(result)):
voucher = frappe._dict(result[i])
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,
@@ -127,21 +133,6 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
def create_tax_accounts():
account_names = ["TCS", "TDS"]
for account in account_names:
frappe.get_doc(
{
"doctype": "Account",
"company": "_Test Company",
"account_name": account,
"parent_account": "Duties and Taxes - _TC",
"report_type": "Balance Sheet",
"root_type": "Liability",
}
).insert(ignore_if_duplicate=True)
def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0):
fiscal_year = get_fiscal_year(today(), company="_Test Company")
from_date = fiscal_year[1]
@@ -157,55 +148,34 @@ def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulat
)
def create_tcs_payment_entry():
def create_tcs_payment_entry(party="Test TCS Customer", category="TCS", amount=1000):
"""Create a TCS Payment Entry that generates a Tax Withholding Entry (Over Withheld)."""
payment_entry = create_payment_entry(
payment_type="Receive",
party_type="Customer",
party="_Test Customer",
party=party,
paid_from="Debtors - _TC",
paid_to="Cash - _TC",
paid_amount=2550,
)
payment_entry.append(
"taxes",
{
"account_head": "TCS - _TC",
"charge_type": "Actual",
"tax_amount": 0.53,
"add_deduct_tax": "Add",
"description": "Test",
"cost_center": "Main - _TC",
},
paid_amount=amount,
)
payment_entry.apply_tds = 1
payment_entry.tax_withholding_category = category
payment_entry.save()
payment_entry.submit()
return payment_entry
def create_tcs_journal_entry():
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = "_Test Company"
jv.set(
"accounts",
[
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 10000,
},
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"debit_in_account_currency": 9992.5,
},
{
"account": "TCS - _TC",
"debit_in_account_currency": 7.5,
},
],
def create_tcs_journal_entry(party="Test TCS Customer", category="TCS", amount=1000):
"""Create a TCS Credit Note Journal Entry that generates a Tax Withholding Entry."""
jv = make_journal_entry_with_tax_withholding(
party_type="Customer",
party=party,
voucher_type="Credit Note",
amount=amount,
save=False,
)
jv.insert()
return jv.submit()
jv.apply_tds = 1
jv.tax_withholding_category = category
jv.save()
jv.submit()
return jv

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

@@ -3,7 +3,6 @@
from collections import defaultdict
from datetime import date, datetime
from json import loads
from typing import TYPE_CHECKING, Optional
@@ -31,6 +30,7 @@ from frappe.utils import (
nowdate,
)
from frappe.utils.caching import site_cache
from frappe.utils.data import DateTimeLikeObject
from pypika import Order
from pypika.functions import Coalesce
from pypika.terms import ExistsCriterion
@@ -61,7 +61,7 @@ OUTSTANDING_DOCTYPES = frozenset(["Sales Invoice", "Purchase Invoice", "Fees"])
@frappe.whitelist()
def get_fiscal_year(
date: str | datetime | None = None,
date: DateTimeLikeObject | None = None,
fiscal_year: str | None = None,
label: str = "Date",
verbose: int = 1,
@@ -201,7 +201,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
@frappe.whitelist()
def get_balance_on(
account: str | None = None,
date: str | date | None = None,
date: DateTimeLikeObject | None = None,
party_type: str | None = None,
party: str | None = None,
company: str | None = None,
@@ -547,7 +547,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)
@@ -2526,6 +2526,7 @@ def create_gain_loss_journal(
ref2_detail_no,
cost_center,
dimensions,
project=None,
) -> str:
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
@@ -2552,6 +2553,7 @@ def create_gain_loss_journal(
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref1_dt,
"reference_name": ref1_dn,
"reference_detail_no": ref1_detail_no,
@@ -2569,6 +2571,7 @@ def create_gain_loss_journal(
"account_currency": gain_loss_account_currency,
"exchange_rate": 1,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref2_dt,
"reference_name": ref2_dn,
"reference_detail_no": ref2_detail_no,

View File

@@ -22,7 +22,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

@@ -194,6 +194,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

@@ -660,12 +660,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);
}
}
@@ -740,12 +748,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

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

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

@@ -1770,6 +1770,7 @@ class AccountsController(TransactionBase):
arg.get("referenced_row"),
arg.get("cost_center"),
dimensions_dict,
arg.get("project"),
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -1854,6 +1855,7 @@ class AccountsController(TransactionBase):
d.idx,
self.cost_center,
dimensions_dict,
self.project,
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(

View File

@@ -461,7 +461,17 @@ 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 = (
flt(
(item.base_net_amount / item.received_qty) * item.qty,
item.precision("base_net_amount"),
)
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate

View File

@@ -9,7 +9,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

@@ -86,9 +86,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",
@@ -108,9 +105,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",
@@ -138,9 +132,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",

View File

@@ -26,7 +26,7 @@ def import_genericode():
content, file_name = get_uploaded_genericode_file()
return import_genericode_content(
doctype=frappe.form_dict.doctype,
doctype="Code List",
docname=frappe.form_dict.docname,
content=content,
file_name=file_name,

View File

@@ -98,7 +98,7 @@ class TestCodeListImport(ERPNextTestSuite):
)
file_doc = frappe.get_doc("File", import_result["file"])
self.assertEqual(file_doc.file_name, "trusted.xml")
self.assertEqual(file_doc.get_content(encodings=()), SAMPLE_GENERICODE)
self.assertFalse(file_doc.file_url.startswith("https://"))
def test_import_genericode_from_trusted_url_propagates_fetch_errors(self):
@@ -117,7 +117,7 @@ class TestCodeListImport(ERPNextTestSuite):
self.assert_import_response(import_result)
file_doc = frappe.get_doc("File", import_result["file"])
self.assertEqual(file_doc.file_name, "uploaded_genericode.xml")
self.assertEqual(file_doc.get_content(encodings=()), SAMPLE_GENERICODE)
def test_process_genericode_import_reads_file_doc_content(self):
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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