mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-04 14:10:52 +00:00
Compare commits
133 Commits
ci_set_tz
...
copilot/de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06ffe52d6e | ||
|
|
c120cc7ed1 | ||
|
|
25be38e23c | ||
|
|
2a720e7008 | ||
|
|
f38eca9124 | ||
|
|
ad89f88c93 | ||
|
|
78f654765d | ||
|
|
231dd1856f | ||
|
|
da081254a6 | ||
|
|
c543d15f3c | ||
|
|
ddf0e35009 | ||
|
|
88b82383f5 | ||
|
|
c4155b6c81 | ||
|
|
a04c028522 | ||
|
|
5c5a5361bc | ||
|
|
060defcc2b | ||
|
|
d0d8cff48f | ||
|
|
844f3dbc0b | ||
|
|
43937acd8b | ||
|
|
503b5bf140 | ||
|
|
3542087003 | ||
|
|
d68801e73a | ||
|
|
addec3aa8f | ||
|
|
b001884f9d | ||
|
|
d1a80d40c4 | ||
|
|
a8030c9713 | ||
|
|
54f20de7e3 | ||
|
|
f8893b04d5 | ||
|
|
1bade56e37 | ||
|
|
a2b96799ff | ||
|
|
d0f0e38e8d | ||
|
|
590f2ffe28 | ||
|
|
084c7f72f0 | ||
|
|
84aa54c540 | ||
|
|
5fc3ca1d4b | ||
|
|
d62fa3c464 | ||
|
|
07337ba9da | ||
|
|
2088a01c19 | ||
|
|
68cc518497 | ||
|
|
6f9089dd5b | ||
|
|
63edd5ddc6 | ||
|
|
2b3e047143 | ||
|
|
cb2e6e1e2e | ||
|
|
37e3493ec4 | ||
|
|
601581d6f8 | ||
|
|
837cdc9cc3 | ||
|
|
5281d60f2d | ||
|
|
0aadd1e3a5 | ||
|
|
60a6b38c31 | ||
|
|
be2a4b7b2a | ||
|
|
5c839f60e4 | ||
|
|
6e77a45c05 | ||
|
|
2a6ddc7f67 | ||
|
|
fee5bcadb2 | ||
|
|
f572bc51e1 | ||
|
|
fba33b7e7a | ||
|
|
ebca389136 | ||
|
|
c94b8c41f3 | ||
|
|
e517eeaaa2 | ||
|
|
c3931d4e29 | ||
|
|
0b9fdcd8cd | ||
|
|
b4e941835b | ||
|
|
9132f0fc4a | ||
|
|
ce37530e70 | ||
|
|
889fdf2f11 | ||
|
|
5518e8c99f | ||
|
|
419b9b3279 | ||
|
|
a9e6f8efd8 | ||
|
|
0e20e35842 | ||
|
|
b4107b8fd5 | ||
|
|
a165b240a7 | ||
|
|
f6639db0e9 | ||
|
|
c35221852a | ||
|
|
3854d2cbf6 | ||
|
|
ab19b16fe2 | ||
|
|
1fd6c3ba1a | ||
|
|
4274c2aba3 | ||
|
|
79d6a51e1e | ||
|
|
4eb9107e22 | ||
|
|
5a915cb45e | ||
|
|
b8c3765b85 | ||
|
|
9ead8d4e3f | ||
|
|
7f8fa7cf5e | ||
|
|
fd4cedf5e4 | ||
|
|
435db260ee | ||
|
|
f5357c233d | ||
|
|
0d2da6d86c | ||
|
|
0349e7a0b8 | ||
|
|
7ae91cac01 | ||
|
|
b925469c4d | ||
|
|
f0ea20e579 | ||
|
|
3faeb1609b | ||
|
|
b16dd3f2dd | ||
|
|
ffae7e42d3 | ||
|
|
b5550f747e | ||
|
|
f6adef45bf | ||
|
|
07b023a934 | ||
|
|
53666974a3 | ||
|
|
c3e7f7f02f | ||
|
|
75a068aea8 | ||
|
|
6dca96b423 | ||
|
|
f6eb844d20 | ||
|
|
6d727c90b6 | ||
|
|
d8fc9444ea | ||
|
|
e65b9fc2ae | ||
|
|
1995fcfdd8 | ||
|
|
c2590c174d | ||
|
|
11fc3e5495 | ||
|
|
0edee23e53 | ||
|
|
6545bcbbd9 | ||
|
|
3c8a066484 | ||
|
|
9eeb819106 | ||
|
|
d9b255b952 | ||
|
|
0cad511136 | ||
|
|
d51dbf5254 | ||
|
|
3aeb7d6b01 | ||
|
|
84e5272f5d | ||
|
|
697f521e14 | ||
|
|
44e0b36093 | ||
|
|
915fcc0166 | ||
|
|
e3019c827c | ||
|
|
e8d08df044 | ||
|
|
4228885f1e | ||
|
|
e6a32a9d02 | ||
|
|
ffc59ebc9c | ||
|
|
86ee9959a2 | ||
|
|
5bbcb73808 | ||
|
|
2bf9d41797 | ||
|
|
c051536182 | ||
|
|
0d4f56bf84 | ||
|
|
9660debe28 | ||
|
|
3ba36212b0 | ||
|
|
fa34ebea94 |
1
.github/workflows/server-tests-mariadb.yml
vendored
1
.github/workflows/server-tests-mariadb.yml
vendored
@@ -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' }}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}`];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
{% 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>
|
||||
@@ -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"),
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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>
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
{% 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"]|| " ") { %}
|
||||
{% 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>
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
"""
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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>
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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"}) || " " %}
|
||||
{% } %}
|
||||
{% } %}</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"}) || " " %}
|
||||
{% } %}
|
||||
|
||||
{% } %}
|
||||
</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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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"]],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
3018
erpnext/locale/ar.po
3018
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
2993
erpnext/locale/bs.po
2993
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
3198
erpnext/locale/cs.po
3198
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
2964
erpnext/locale/da.po
2964
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
2998
erpnext/locale/de.po
2998
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
2998
erpnext/locale/eo.po
2998
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
2994
erpnext/locale/es.po
2994
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
3124
erpnext/locale/fa.po
3124
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
3054
erpnext/locale/fr.po
3054
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
2999
erpnext/locale/hr.po
2999
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
3082
erpnext/locale/hu.po
3082
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
3028
erpnext/locale/id.po
3028
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
3080
erpnext/locale/it.po
3080
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3140
erpnext/locale/my.po
3140
erpnext/locale/my.po
File diff suppressed because it is too large
Load Diff
2968
erpnext/locale/nb.po
2968
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
2992
erpnext/locale/nl.po
2992
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
3430
erpnext/locale/pl.po
3430
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
3300
erpnext/locale/pt.po
3300
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3022
erpnext/locale/ru.po
3022
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
2966
erpnext/locale/sl.po
2966
erpnext/locale/sl.po
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
Reference in New Issue
Block a user