Merge pull request #54583 from frappe/version-16-hotfix

This commit is contained in:
diptanilsaha
2026-04-29 02:31:54 +05:30
committed by GitHub
105 changed files with 3112 additions and 1562 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,8 @@
"due_date",
"amended_from",
"return_against",
"section_break_abck",
"title",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -187,7 +189,7 @@
"subscription_section",
"from_date",
"to_date",
"column_break_140",
"auto_repeat_section",
"auto_repeat",
"update_auto_repeat_reference",
"against_income_account"
@@ -1462,7 +1464,7 @@
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
"label": "Subscription"
},
{
"allow_on_submit": 1,
@@ -1480,10 +1482,6 @@
"no_copy": 1,
"print_hide": 1
},
{
"fieldname": "column_break_140",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "auto_repeat",
@@ -1619,12 +1617,29 @@
{
"fieldname": "column_break_bhao",
"fieldtype": "Column Break"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_abck",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-02-10 14:23:07.181782",
"modified": "2026-04-28 06:06:14.283612",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,6 +33,8 @@
"is_created_using_pos",
"pos_closing_entry",
"has_subcontracted",
"section_break_qllv",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -91,9 +93,9 @@
"column_break_xjag",
"base_rounding_adjustment",
"base_rounded_total",
"section_break_vacb",
"section_break_pxwz",
"total_advance",
"column_break_rdks",
"column_break_iaso",
"outstanding_amount",
"section_tax_withholding_entry",
"tax_withholding_group",
@@ -199,6 +201,7 @@
"unrealized_profit_loss_account",
"against_income_account",
"commission_section",
"column_break_rdiw",
"sales_partner",
"amount_eligible_for_commission",
"column_break10",
@@ -214,12 +217,14 @@
"language",
"subscription_section",
"subscription",
"from_date",
"auto_repeat",
"column_break_140",
"from_date",
"to_date",
"automation_section",
"auto_repeat",
"update_auto_repeat_reference",
"utm_analytics_section",
"column_break_rdke",
"utm_source",
"utm_medium",
"column_break_ixxw",
@@ -2304,14 +2309,6 @@
"options": "fa fa-group",
"print_hide": 1
},
{
"fieldname": "section_break_vacb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdks",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ixxw",
"fieldtype": "Column Break"
@@ -2321,6 +2318,40 @@
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "UTM Analytics"
},
{
"collapsible": 1,
"fieldname": "automation_section",
"fieldtype": "Section Break",
"label": "Automation"
},
{
"fieldname": "section_break_pxwz",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdke",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_rdiw",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_iaso",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_qllv",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -2334,7 +2365,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-03-09 17:15:30.931929",
"modified": "2026-04-28 13:08:19.849783",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -159,6 +159,7 @@ class PurchaseOrder(BuyingController):
taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_net_weight: DF.Float

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ erpnext.edi.import_genericode = function (listview_or_form) {
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
doctype: doctype,
docname: docname,
allow_web_link: false,
allow_toggle_private: false,
allow_take_photo: false,
on_success: function (_file_doc, r) {

View File

@@ -1,48 +1,106 @@
import json
from urllib.parse import urlsplit
import frappe
import requests
from frappe import _
from frappe.utils import escape_html
from frappe.utils.file_manager import save_file
from lxml import etree
URL_PREFIXES = ("http://", "https://")
GENERICODE_FETCH_TIMEOUT = 15
LOCAL_FILE_PREFIXES = ("/files/", "/private/files/")
class RemoteGenericodeUrlNotAllowedError(Exception):
pass
class CodeListSelectionMismatchError(Exception):
pass
@frappe.whitelist()
def import_genericode():
doctype = frappe.form_dict.doctype
docname = frappe.form_dict.docname
content = frappe.local.uploaded_file
# recover the content, if it's a link
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
try:
# If it's a URL, fetch the content and make it a local file (for durable audit)
response = requests.get(frappe.local.uploaded_file_url)
response.raise_for_status()
frappe.local.uploaded_file = content = response.content
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
frappe.local.uploaded_file_url = None
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
if file_url := frappe.local.uploaded_file_url:
file_path = frappe.utils.file_manager.get_file_path(file_url)
with open(file_path.encode(), mode="rb") as f:
content = f.read()
# Parse the xml content
parser = etree.XMLParser(
remove_blank_text=True,
resolve_entities=False,
load_dtd=False,
no_network=True,
)
try:
root = etree.fromstring(content, parser=parser)
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
content, file_name = get_uploaded_genericode_file()
return import_genericode_content(
doctype="Code List",
docname=frappe.form_dict.docname,
content=content,
file_name=file_name,
)
except RemoteGenericodeUrlNotAllowedError:
frappe.throw(
_("Importing Code Lists from remote URLs is not allowed."),
title=_("Invalid Upload"),
)
except CodeListSelectionMismatchError:
frappe.throw(_("The uploaded file does not match the selected Code List."))
except etree.XMLSyntaxError:
frappe.throw(
_("The uploaded file could not be parsed as a genericode XML document."),
title=_("Parsing Error"),
)
def import_genericode_from_url(
url: str,
doctype: str = "Code List",
docname: str | None = None,
):
"""Import a Code List from a trusted backend URL."""
content = fetch_genericode_from_url(url)
file_name = urlsplit(url).path.rsplit("/", 1)[-1] or "genericode.xml"
return import_genericode_content(
doctype=doctype,
docname=docname,
content=content,
file_name=file_name,
)
def get_uploaded_genericode_file() -> tuple[bytes, str | None]:
uploaded_data = frappe.local.uploaded_file
file_name = frappe.local.uploaded_filename
if uploaded_data and file_name:
return uploaded_data, file_name
file_url = frappe.local.uploaded_file_url
if not file_url:
raise frappe.ValidationError(_("No file uploaded or URL provided."))
if not is_local_file_url(file_url):
raise RemoteGenericodeUrlNotAllowedError
file_doc = frappe.get_doc("File", {"file_url": file_url})
file_doc.check_permission("read")
return file_doc.get_content(encodings=()), file_name
def is_local_file_url(file_url: str | None) -> bool:
if not file_url:
return False
parsed = urlsplit(file_url.strip())
return not parsed.scheme and not parsed.netloc and parsed.path.startswith(LOCAL_FILE_PREFIXES)
def fetch_genericode_from_url(url: str) -> bytes:
response = requests.get(url, timeout=GENERICODE_FETCH_TIMEOUT)
response.raise_for_status()
return response.content
def import_genericode_content(
doctype: str,
docname: str | None,
content: bytes,
file_name: str | None,
):
root = parse_genericode_content(content)
# Extract the name (CanonicalVersionUri) from the parsed XML
name = root.find(".//CanonicalVersionUri").text
@@ -51,7 +109,7 @@ def import_genericode():
if frappe.db.exists(doctype, docname):
code_list = frappe.get_doc(doctype, docname)
if code_list.name != name:
frappe.throw(_("The uploaded file does not match the selected Code List."))
raise CodeListSelectionMismatchError
else:
# Create a new Code List document with the extracted name
code_list = frappe.new_doc(doctype)
@@ -60,19 +118,13 @@ def import_genericode():
code_list.from_genericode(root)
code_list.save()
# Attach the file and provide a recoverable identifier
file_doc = frappe.get_doc(
{
"doctype": "File",
"attached_to_doctype": "Code List",
"attached_to_name": code_list.name,
"folder": frappe.db.get_value("File", {"is_attachments_folder": 1}),
"file_name": frappe.local.uploaded_filename,
"file_url": frappe.local.uploaded_file_url,
"is_private": 1,
"content": content,
}
).save()
file_doc = save_file(
fname=file_name,
content=content,
dt=doctype,
dn=code_list.name,
is_private=1,
)
# Get available columns and example values
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
@@ -87,6 +139,16 @@ def import_genericode():
}
def parse_genericode_content(content: bytes):
parser = etree.XMLParser(
remove_blank_text=True,
resolve_entities=False,
load_dtd=False,
no_network=True,
)
return etree.fromstring(content, parser=parser)
@frappe.whitelist()
def process_genericode_import(
code_list_name: str,

View File

@@ -0,0 +1,200 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from unittest.mock import Mock, patch
import frappe
import requests
from erpnext.edi.doctype.code_list import code_list_import
from erpnext.tests.utils import ERPNextTestSuite
SAMPLE_GENERICODE = b"""<?xml version="1.0" encoding="UTF-8"?>
<CodeList>
<Identification>
<ShortName>Test Code List</ShortName>
<Version>1.0</Version>
<CanonicalUri>test-code-list</CanonicalUri>
<LongName>Code list for tests</LongName>
<Agency>
<ShortName>Test Agency</ShortName>
<Identifier>TEST</Identifier>
</Agency>
<LocationUri>https://example.com/codelists/test.xml</LocationUri>
</Identification>
<CanonicalVersionUri>test-code-list-v1</CanonicalVersionUri>
<ColumnSet>
<Column Id="code" />
<Column Id="name" />
<Column Id="category" />
</ColumnSet>
<SimpleCodeList>
<Row>
<Value ColumnRef="code"><SimpleValue>A</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Alpha</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
</Row>
<Row>
<Value ColumnRef="code"><SimpleValue>B</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Beta</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 2</SimpleValue></Value>
</Row>
<Row>
<Value ColumnRef="code"><SimpleValue>C</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Gamma</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
</Row>
</SimpleCodeList>
</CodeList>
"""
class TestCodeListImport(ERPNextTestSuite):
def test_import_genericode_rejects_remote_file_url(self):
self.set_upload_context(
file_name="trusted.xml",
file_url="https://example.com/codelists/trusted.xml",
)
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
with self.assertRaisesRegex(
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
):
code_list_import.import_genericode()
mock_get.assert_not_called()
def test_import_genericode_rejects_file_scheme_url(self):
self.set_upload_context(
file_name="trusted.xml",
file_url="file:///tmp/trusted.xml",
)
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
with self.assertRaisesRegex(
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
):
code_list_import.import_genericode()
mock_get.assert_not_called()
def test_import_genericode_from_trusted_url(self):
response = Mock()
response.content = SAMPLE_GENERICODE
response.raise_for_status.return_value = None
with patch(
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
return_value=response,
) as mock_get:
import_result = code_list_import.import_genericode_from_url(
"https://example.com/codelists/trusted.xml"
)
self.assert_import_response(import_result)
mock_get.assert_called_once_with(
"https://example.com/codelists/trusted.xml",
timeout=code_list_import.GENERICODE_FETCH_TIMEOUT,
)
file_doc = frappe.get_doc("File", import_result["file"])
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):
with patch(
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
side_effect=requests.Timeout,
):
with self.assertRaises(requests.Timeout):
code_list_import.import_genericode_from_url("https://example.com/codelists/trusted.xml")
def test_import_genericode_from_uploaded_file_returns_metadata(self):
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
import_result = code_list_import.import_genericode()
self.assert_import_response(import_result)
file_doc = frappe.get_doc("File", import_result["file"])
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")
import_result = code_list_import.import_genericode()
count = code_list_import.process_genericode_import(
code_list_name=import_result["code_list"],
file_name=import_result["file"],
code_column="code",
title_column="name",
)
self.assertEqual(count, 3)
self.assertEqual(frappe.db.count("Common Code", {"code_list": import_result["code_list"]}), 3)
self.assertEqual(
frappe.db.get_value(
"Common Code",
{"code_list": import_result["code_list"], "common_code": "A"},
"title",
),
"Alpha",
)
def test_import_genericode_from_local_file_url(self):
source_file = frappe.get_doc(
{
"doctype": "File",
"file_name": "library_genericode.xml",
"content": SAMPLE_GENERICODE,
"is_private": 1,
}
).insert()
self.set_upload_context(file_name=source_file.file_name, file_url=source_file.file_url)
import_result = code_list_import.import_genericode()
self.assert_import_response(import_result)
def set_upload_context(
self,
content: bytes | None = None,
file_name: str = "genericode.xml",
file_url: str | None = None,
docname: str | None = None,
):
attrs = ("form_dict", "uploaded_file", "uploaded_file_url", "uploaded_filename")
originals = {attr: getattr(frappe.local, attr, None) for attr in attrs}
frappe.local.form_dict = frappe._dict(doctype="Code List", docname=docname)
frappe.local.uploaded_file = content
frappe.local.uploaded_file_url = file_url
frappe.local.uploaded_filename = file_name
def restore():
for attr, value in originals.items():
setattr(frappe.local, attr, value)
self.addCleanup(restore)
def assert_import_response(self, import_result):
self.assertEqual(
set(import_result),
{
"code_list",
"code_list_title",
"file",
"columns",
"example_values",
"filterable_columns",
},
)
self.assertEqual(import_result["code_list"], "test-code-list-v1")
self.assertEqual(import_result["code_list_title"], "Test Code List")
self.assertEqual(import_result["columns"], ["code", "name", "category"])
self.assertEqual(import_result["example_values"]["code"], ["A", "B", "C"])
self.assertEqual(import_result["example_values"]["name"], ["Alpha", "Beta", "Gamma"])
self.assertEqual(import_result["example_values"]["category"], ["Group 1", "Group 2", "Group 1"])
self.assertCountEqual(import_result["filterable_columns"]["category"], ["Group 1", "Group 2"])
self.assertTrue(frappe.db.exists("Code List", import_result["code_list"]))
self.assertTrue(frappe.db.exists("File", import_result["file"]))

View File

@@ -9,6 +9,8 @@ from frappe.model.document import Document
from frappe.utils.data import get_link_to_form
from lxml import etree
from erpnext.edi.doctype.code_list.code_list_import import parse_genericode_content
class CommonCode(Document):
# begin: auto-generated types
@@ -86,15 +88,15 @@ def simple_hash(input_string, length=6):
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
"""Import genericode file and create Common Code entries"""
file_path = frappe.utils.file_manager.get_file_path(file_name)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser=parser)
root = tree.getroot()
file_doc = frappe.get_doc("File", file_name)
file_doc.check_permission("read")
root = parse_genericode_content(file_doc.get_content(encodings=()))
# Construct the XPath expression
xpath_expr = ".//SimpleCodeList/Row"
filter_conditions = [
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'"
for column_ref, value in (filters or {}).items()
]
if filter_conditions:
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
@@ -102,7 +104,7 @@ def import_genericode(code_list: str, file_name: str, column_map: dict, filters:
elements = root.xpath(xpath_expr)
total_elements = len(elements)
for i, xml_element in enumerate(elements, start=1):
common_code: "CommonCode" = frappe.new_doc("Common Code")
common_code: CommonCode = frappe.new_doc("Common Code")
common_code.code_list = code_list
common_code.from_genericode(column_map, xml_element)
common_code.save()

View File

@@ -581,6 +581,8 @@ accounting_dimension_doctypes = [
"Advance Taxes and Charges",
]
subscription_doctypes = ["Sales Invoice", "Purchase Invoice", "Payment Request", "POS Invoice"]
get_matching_queries = (
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_matching_queries"
)

File diff suppressed because it is too large Load Diff

View File

@@ -120,7 +120,7 @@ class BlanketOrder(Document):
def validate_item_qty(self):
for d in self.items:
if d.qty < 0:
if flt(d.qty) < 0:
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))

View File

@@ -165,9 +165,6 @@ class TestBOM(ERPNextTestSuite):
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
frappe.db.sql(
"delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s", item_code
)
item_price = frappe.new_doc("Item Price")
item_price.price_list = "_Test Price List"
item_price.item_code = item_code

View File

@@ -382,9 +382,9 @@ class ProductionPlan(Document):
items = items_query.run(as_dict=True)
for item in items:
item.pending_qty = (
flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0)
) * item.conversion_factor
item.pending_qty = flt(item.qty) - max(
item.work_order_qty, flt(item.delivered_qty) * item.conversion_factor, 0
)
pi = frappe.qb.DocType("Packed Item")

View File

@@ -476,4 +476,7 @@ erpnext.patches.v16_0.co_by_product_patch
erpnext.patches.v16_0.depends_on_inv_dimensions
erpnext.patches.v16_0.uom_category
erpnext.patches.v16_0.merge_repost_settings_to_accounts_settings
erpnext.patches.v16_0.set_root_type_in_account_categories
erpnext.patches.v16_0.scr_inv_dimension
erpnext.patches.v16_0.packed_item_inv_dimen
erpnext.patches.v16_0.correct_po_titles

View File

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

View File

@@ -0,0 +1,27 @@
import frappe
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
def execute():
for dimension in get_inventory_dimensions():
if frappe.db.exists(
"Custom Field",
{
"fieldname": dimension.source_fieldname,
"dt": "Packed Item",
"reqd": 1,
},
):
frappe.set_value(
"Custom Field",
{
"fieldname": dimension.source_fieldname,
"dt": "Packed Item",
"reqd": 1,
},
{
"reqd": 0,
"mandatory_depends_on": "eval:doc.parent_detail_docname && ['Delivery Note', 'Sales Invoice', 'POS Invoice'].includes(parent.doctype)",
},
)

View File

@@ -10,7 +10,18 @@ def execute():
)
if data:
frappe.db.auto_commit_on_many_writes = 1
frappe.db.bulk_update(
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
)
frappe.db.auto_commit_on_many_writes = 0
try:
frappe.db.bulk_update(
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
)
quotations = frappe.get_all(
"Quotation Item",
filters={"name": ["in", [d.quotation_item for d in data]]},
pluck="parent",
distinct=True,
)
for quotation in quotations:
doc = frappe.get_doc("Quotation", quotation)
doc.set_status(update=True, update_modified=False)
finally:
frappe.db.auto_commit_on_many_writes = 0

View File

@@ -0,0 +1,32 @@
import json
from pathlib import Path
import frappe
def execute():
base_path = Path(frappe.get_app_path("erpnext", "accounts")).resolve()
categories_file = (base_path / "financial_report_template" / "account_categories.json").resolve()
if not categories_file.exists():
return
categories = json.loads(frappe.read_file(str(categories_file)))
valid_root_types = set(frappe.get_meta("Account Category").get_field("root_type").options.split("\n"))
root_type_categories = {}
for category in categories:
if (root_type := category.get("root_type")) and root_type in valid_root_types:
root_type_categories.setdefault(root_type, []).append(category["account_category_name"])
if not root_type_categories:
return
for root_type, category_names in root_type_categories.items():
frappe.db.set_value(
"Account Category",
{"name": ["in", category_names], "root_type": ["is", "not set"]},
"root_type",
root_type,
)

View File

@@ -15,7 +15,6 @@ class TestActivityCost(ERPNextTestSuite):
"Activity Type", filters={"activity_type": "_Test Activity Type 1"}
)[0].name
frappe.db.sql("delete from `tabActivity Cost`")
activity_cost1 = frappe.new_doc("Activity Cost")
activity_cost1.update(
{
@@ -29,4 +28,3 @@ class TestActivityCost(ERPNextTestSuite):
activity_cost1.insert()
activity_cost2 = frappe.copy_doc(activity_cost1)
self.assertRaises(DuplicationError, activity_cost2.insert)
frappe.db.sql("delete from `tabActivity Cost`")

View File

@@ -64,7 +64,7 @@ erpnext.financial_statements = {
const isPeriodColumn = periodKeys.includes(baseName);
return {
isAccount: baseName === erpnext.financial_statements.name_field,
isAccount: baseName === "account", // DO NOT USE `name_field` ! This can be overridden in some reports!
isPeriod: isPeriodColumn,
segmentIndex: valueMatch && valueMatch[1] ? parseInt(valueMatch[1]) : null,
fieldname: baseName,
@@ -298,7 +298,7 @@ erpnext.financial_statements = {
let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
var filters = report.get_values();
if (!filters.period_start_date || !filters.period_end_date) {
if (fiscal_year && (!filters.period_start_date || !filters.period_end_date)) {
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
@@ -422,16 +422,16 @@ function get_filters() {
label: __("Start Year"),
fieldtype: "Link",
options: "Fiscal Year",
reqd: 1,
depends_on: "eval:doc.filter_based_on == 'Fiscal Year'",
mandatory_depends_on: "eval:doc.filter_based_on == 'Fiscal Year'",
},
{
fieldname: "to_fiscal_year",
label: __("End Year"),
fieldtype: "Link",
options: "Fiscal Year",
reqd: 1,
depends_on: "eval:doc.filter_based_on == 'Fiscal Year'",
mandatory_depends_on: "eval:doc.filter_based_on == 'Fiscal Year'",
},
{
fieldname: "periodicity",
@@ -455,6 +455,7 @@ function get_filters() {
label: __("Currency"),
fieldtype: "Select",
options: erpnext.get_presentation_currency_list(),
depends_on: "eval: !doc.report_template",
},
{
fieldname: "cost_center",

View File

@@ -87,7 +87,16 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
get_dialog_fields() {
let fields = [];
let fields = [
{
fieldname: "item_code",
read_only: 1,
fieldtype: "Link",
options: "Item",
label: __("Item Code"),
default: this.item.item_code,
},
];
fields.push({
fieldtype: "Link",
@@ -106,10 +115,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
},
get_query: () => {
return {
filters: {
is_group: 0,
company: this.frm.doc.company,
},
query: "erpnext.controllers.queries.warehouse_query",
filters: [
["Bin", "item_code", "=", this.item.item_code],
["Warehouse", "is_group", "=", 0],
["Warehouse", "company", "=", this.frm.doc.company],
],
};
},
});

View File

@@ -0,0 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -0,0 +1,4 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -137,12 +137,6 @@ class TestCustomer(ERPNextTestSuite):
# delete communication linked to these 2 customers
new_name = "_Test Customer 1 Renamed"
for name in ("_Test Customer 1", new_name):
frappe.db.sql(
"""delete from `tabComment`
where reference_doctype=%s and reference_name=%s""",
("Customer", name),
)
# add comments
comment = frappe.get_doc("Customer", "_Test Customer 1").add_comment(
@@ -209,8 +203,6 @@ class TestCustomer(ERPNextTestSuite):
so.save()
def test_duplicate_customer(self):
frappe.db.sql("delete from `tabCustomer` where customer_name='_Test Customer 1'")
if not frappe.db.get_value("Customer", "_Test Customer 1"):
test_customer_1 = frappe.get_doc(get_customer_dict("_Test Customer 1")).insert(
ignore_permissions=True

View File

@@ -132,6 +132,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
) {
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
this.frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
frm: this.frm,
@@ -146,8 +147,6 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
this.frm.trigger("set_as_lost_dialog");
});
}
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) {

View File

@@ -22,6 +22,8 @@
"company",
"has_unit_price_items",
"amended_from",
"section_break_jdzz",
"title",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -109,7 +111,7 @@
"tc_name",
"terms",
"more_info_tab",
"subscription_section",
"auto_repeat_section",
"auto_repeat",
"update_auto_repeat_reference",
"print_settings",
@@ -832,11 +834,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "auto_repeat",
"fieldtype": "Link",
@@ -1122,13 +1119,30 @@
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "UTM Analytics"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_jdzz",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"icon": "fa fa-shopping-cart",
"idx": 82,
"is_submittable": 1,
"links": [],
"modified": "2026-03-09 17:15:31.941114",
"modified": "2026-04-28 06:28:13.103302",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",

View File

@@ -112,6 +112,7 @@ class Quotation(SellingController):
tc_name: DF.Link | None
terms: DF.TextEditor | None
territory: DF.Link | None
title: DF.Data | None
total: DF.Currency
total_net_weight: DF.Float
total_qty: DF.Float

View File

@@ -716,29 +716,32 @@ frappe.ui.form.on("Sales Order", {
if (!frequency) {
frappe.throw(__("Please select a frequency for delivery schedule"));
}
if (!first_delivery_date) {
frappe.throw(__("Please enter the first delivery date"));
}
if (no_of_deliveries <= 0) {
frappe.throw(__("Please enter a valid number of deliveries"));
}
const month_mapper = {
Monthly: 1,
Quarterly: 3,
"Half Yearly": 6,
Yearly: 12,
};
frm.schedule_dialog.fields_dict.delivery_schedule.df.data = [];
let qty_to_deliver = row.qty;
let qty_per_delivery = qty_to_deliver / no_of_deliveries;
for (let i = 0; i < no_of_deliveries; i++) {
let qty = qty_per_delivery;
if (must_be_whole_number) {
qty = cint(qty);
}
if (i === no_of_deliveries - 1) {
// Last delivery, adjust the quantity to deliver the remaining amount
for (let i = 0; i < no_of_deliveries; i++) {
let qty;
const is_last = i === no_of_deliveries - 1;
if (is_last) {
qty = qty_to_deliver;
qty_to_deliver = 0;
} else {
qty = must_be_whole_number ? cint(qty_per_delivery) : qty_per_delivery;
qty_to_deliver -= qty;
}
@@ -747,20 +750,15 @@ frappe.ui.form.on("Sales Order", {
qty: qty,
});
if (frequency === "Weekly") {
first_delivery_date = frappe.datetime.add_days(first_delivery_date, i + 1 * 7);
} else {
let month_mapper = {
Monthly: 1,
Quarterly: 3,
Half_Yearly: 6,
Yearly: 12,
};
first_delivery_date = frappe.datetime.add_months(
first_delivery_date,
month_mapper[frequency] * i + 1
);
if (!is_last) {
if (frequency === "Weekly") {
first_delivery_date = frappe.datetime.add_days(first_delivery_date, 7);
} else {
first_delivery_date = frappe.datetime.add_months(
first_delivery_date,
month_mapper[frequency]
);
}
}
}

View File

@@ -26,6 +26,8 @@
"has_unit_price_items",
"is_subcontracted",
"amended_from",
"section_break_zstt",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -151,7 +153,7 @@
"loyalty_points",
"column_break_116",
"loyalty_amount",
"subscription_section",
"auto_repeat_section",
"from_date",
"to_date",
"column_break_108",
@@ -1372,18 +1374,6 @@
"options": "Sales Team",
"print_hide": 1
},
{
"allow_on_submit": 1,
"collapsible": 1,
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Auto Repeat",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "from_date",
@@ -1742,6 +1732,30 @@
"hidden": 1,
"label": "Ignore Default Payment Terms Template",
"read_only": 1
},
{
"allow_on_submit": 1,
"collapsible": 1,
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Auto Repeat",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_zstt",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -1749,7 +1763,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-03-04 18:04:05.873483",
"modified": "2026-04-28 06:30:35.902868",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -181,6 +181,7 @@ class SalesOrder(SellingController):
tc_name: DF.Link | None
terms: DF.TextEditor | None
territory: DF.Link | None
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_commission: DF.Currency

View File

@@ -6,6 +6,7 @@
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
"customer_defaults_tab",
"customer_defaults_section",
"cust_master_name",
"customer_group",
@@ -14,12 +15,14 @@
"item_price_tab",
"item_price_settings_section",
"selling_price_list",
"maintain_same_rate_action",
"role_to_override_stop_action",
"column_break_15",
"maintain_same_sales_rate",
"fallback_to_default_price_list",
"editable_price_list_rate",
"section_break_lpfl",
"maintain_same_sales_rate",
"column_break_hkqv",
"maintain_same_rate_action",
"role_to_override_stop_action",
"section_break_htyx",
"validate_selling_price",
"editable_bundle_item_rates",
"allow_negative_rates_for_items",
@@ -28,20 +31,25 @@
"so_required",
"dn_required",
"sales_update_frequency",
"blanket_order_allowance",
"enable_tracking_sales_commissions",
"column_break_5",
"allow_multiple_items",
"allow_against_multiple_purchase_orders",
"hide_tax_id",
"section_break_jcmi",
"allow_sales_order_creation_for_expired_quotation",
"dont_reserve_sales_order_qty_on_sales_return",
"hide_tax_id",
"enable_discount_accounting",
"enable_cutoff_date_on_bulk_delivery_note_creation",
"allow_zero_qty_in_quotation",
"allow_zero_qty_in_sales_order",
"set_zero_rate_for_expired_batch",
"section_break_zero_qty",
"allow_zero_qty_in_quotation",
"column_break_zwvf",
"allow_zero_qty_in_sales_order",
"blanket_orders_section",
"blanket_order_allowance",
"advanced_features_tab",
"section_break_avhb",
"enable_tracking_sales_commissions",
"enable_discount_accounting",
"enable_utm",
"experimental_section",
"use_legacy_js_reactivity",
@@ -61,6 +69,7 @@
"options": "Customer Name\nNaming Series\nAuto Name"
},
{
"documentation_url": "https://docs.frappe.io/erpnext/selling-settings#2-default-customer-group",
"fieldname": "customer_group",
"fieldtype": "Link",
"in_list_view": 1,
@@ -69,6 +78,7 @@
"options": "Customer Group"
},
{
"documentation_url": "https://docs.frappe.io/erpnext/selling-settings#3-default-territory",
"fieldname": "territory",
"fieldtype": "Link",
"in_list_view": 1,
@@ -80,6 +90,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Price List",
"link_filters": "[[\"Price List\", \"selling\", \"=\", 1]]",
"options": "Price List"
},
{
@@ -89,66 +100,72 @@
{
"fieldname": "so_required",
"fieldtype": "Select",
"label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?",
"label": "Is Sales Order required to create Sales Invoice/Delivery Note?",
"options": "No\nYes"
},
{
"fieldname": "dn_required",
"fieldtype": "Select",
"label": "Is Delivery Note Required for Sales Invoice Creation?",
"label": "Is Delivery Note required to create Sales Invoice?",
"options": "No\nYes"
},
{
"default": "Daily",
"description": "How often should Project and Company be updated based on Sales Transactions?",
"description": "The frequency at which project progress and company transaction details will be updated. Set it to daily or monthly if you post a lot of transactions.",
"fieldname": "sales_update_frequency",
"fieldtype": "Select",
"label": "Sales Update Frequency in Company and Project",
"label": "How often should sales data be updated in Company/Project?",
"options": "Monthly\nEach Transaction\nDaily",
"reqd": 1
},
{
"bold": 1,
"default": "0",
"description": "Warn or stop if Item rate is changed in Delivery Notes and Sales Invoices generated from a Sales Order.",
"fieldname": "maintain_same_sales_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout Sales Cycle"
"label": "Maintain same rate throughout sales cycle"
},
{
"bold": 1,
"default": "0",
"fieldname": "editable_price_list_rate",
"fieldtype": "Check",
"label": "Allow User to Edit Price List Rate in Transactions"
"label": "Allow editing Price List rate in transactions"
},
{
"default": "0",
"fieldname": "allow_multiple_items",
"fieldtype": "Check",
"label": "Allow Item to be Added Multiple Times in a Transaction"
"label": "Allow same Item to be added multiple times in a transaction"
},
{
"default": "0",
"fieldname": "allow_against_multiple_purchase_orders",
"fieldtype": "Check",
"label": "Allow Multiple Sales Orders Against a Customer's Purchase Order"
"label": "Allow multiple Sales Orders against a customer's Purchase Order"
},
{
"default": "0",
"description": "Enable this to block transactions where the selling price is less than the purchase or valuation rate",
"fieldname": "validate_selling_price",
"fieldtype": "Check",
"label": "Validate Selling Price for Item Against Purchase Rate or Valuation Rate"
"label": "Validate selling price for Item against purchase or valuation rate"
},
{
"default": "0",
"description": "Most Customers have a unique Tax ID that is fetched into selling transactions. Enable this setting if you do not want Customer Tax IDs to appear in sales transactions.",
"fieldname": "hide_tax_id",
"fieldtype": "Check",
"label": "Hide Customer's Tax ID from Sales Transactions"
"label": "Hide Customer's Tax ID from sales transactions",
"show_description_on_click": 1
},
{
"default": "Stop",
"depends_on": "maintain_same_sales_rate",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action if Same Rate is Not Maintained Throughout Sales Cycle",
"label": "Action if same rate is not maintained throughout sales cycle",
"mandatory_depends_on": "maintain_same_sales_rate",
"options": "Stop\nWarn"
},
@@ -156,13 +173,12 @@
"depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"label": "Role allowed to override stop action",
"options": "Role"
},
{
"fieldname": "customer_defaults_section",
"fieldtype": "Section Break",
"label": "Customer Defaults"
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_4",
@@ -170,12 +186,7 @@
},
{
"fieldname": "item_price_settings_section",
"fieldtype": "Section Break",
"label": "Item Price Settings"
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
"fieldtype": "Section Break"
},
{
"fieldname": "sales_transactions_settings_section",
@@ -184,34 +195,41 @@
},
{
"default": "0",
"description": "Enabling this will do the following:\n<ul style=\"padding-left:16px\">\n<li>Make the rate column of all Packed/Bundle Items tables editable.</li>\n<li>Calculate the prices of all <a href=\"/desk/product-bundle\" rel=\"noopener noreferrer\">Product Bundles</a> in the Items table, based on the prices of its child Items, specified in the Packed/Bundle Items table. </li>\n</ul>\nNote: If this is enabled, updating the rate of the Product Bundle in the Items table will not change its price. It will get reset to the price based on its Child Items on saving the doc.",
"documentation_url": "https://docs.frappe.io/erpnext/selling-settings#7-calculate-product-bundle-price-based-on-child-items-rates",
"fieldname": "editable_bundle_item_rates",
"fieldtype": "Check",
"label": "Calculate Product Bundle Price based on Child Items' Rates"
"label": "Calculate Product Bundle price based on child Item's rates",
"show_description_on_click": 1
},
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting for Selling"
"label": "Enable discount accounting for selling"
},
{
"default": "0",
"description": "This allows creation of sales orders from quotations that have passed their expiration date, providing flexibility in processing orders despite outdated quotes.",
"fieldname": "allow_sales_order_creation_for_expired_quotation",
"fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation"
"label": "Allow Sales Order creation for expired Quotation"
},
{
"default": "0",
"description": "Prevents the automatic reservation of stock quantities from sales orders when processing sales returns.",
"fieldname": "dont_reserve_sales_order_qty_on_sales_return",
"fieldtype": "Check",
"label": "Don't Reserve Sales Order Qty on Sales Return"
"label": "Don't reserve Sales Order qty on sales return"
},
{
"default": "0",
"description": "Enable this option to permit the use of negative rates for items in sales transactions. This setting is useful for applying substantial discounts, processing refunds or returns, and handling special promotional pricing.",
"fieldname": "allow_negative_rates_for_items",
"fieldtype": "Check",
"label": "Allow Negative rates for Items"
"label": "Allow negative rates for Items",
"show_description_on_click": 1
},
{
"description": "Percentage you are allowed to sell beyond the Blanket Order quantity.",
@@ -221,9 +239,10 @@
},
{
"default": "0",
"description": "When enabled, it adds a cutoff date filter to Delivery Notes created in bulk from Sales Orders. This allows you to process orders only with a transaction date up to the specified cutoff date, which is useful for period-end processing and batch fulfillment.",
"fieldname": "enable_cutoff_date_on_bulk_delivery_note_creation",
"fieldtype": "Check",
"label": "Enable Cut-Off Date on Bulk Delivery Note Creation"
"label": "Enable cut-off date on creating bulk Delivery Notes"
},
{
"fieldname": "experimental_section",
@@ -232,29 +251,26 @@
},
{
"default": "0",
"description": "Allows users to submit Sales Orders with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
"fieldname": "allow_zero_qty_in_sales_order",
"fieldtype": "Check",
"label": "Allow Sales Order with Zero Quantity"
"label": "Allow Sales Order with zero quantity"
},
{
"default": "0",
"description": "Allows users to submit Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
"fieldname": "allow_zero_qty_in_quotation",
"fieldtype": "Check",
"label": "Allow Quotation with Zero Quantity"
"label": "Allow Quotation with zero quantity"
},
{
"fieldname": "section_break_zwh6",
"fieldtype": "Section Break",
"label": "Subcontracting Inward Settings"
"fieldtype": "Section Break"
},
{
"default": "0",
"description": "If enabled, system will allow user to deliver the entire quantity of the finished goods produced against the Subcontracting Inward Order. If disabled, system will allow delivery of only the ordered quantity.",
"fieldname": "allow_delivery_of_overproduced_qty",
"fieldtype": "Check",
"label": "Allow Delivery of Overproduced Qty"
"label": "Allow delivery of overproduced quantity"
},
{
"fieldname": "column_break_mla9",
@@ -277,9 +293,10 @@
},
{
"default": "0",
"description": "If no Item Price is found for an item in the Price List set in the transaction, prices from the Default Price List will be fetched.",
"fieldname": "fallback_to_default_price_list",
"fieldtype": "Check",
"label": "Use Prices from Default Price List as Fallback"
"label": "Use prices from Default Price List as fallback"
},
{
"default": "0",
@@ -292,7 +309,7 @@
"description": "If enabled, system will set incoming rate as zero for stand-alone credit notes with expired batch item.",
"fieldname": "set_zero_rate_for_expired_batch",
"fieldtype": "Check",
"label": "Set Incoming Rate as Zero for Expired Batch"
"label": "Set incoming rate as zero for expired Batch"
},
{
"default": "0",
@@ -303,8 +320,7 @@
},
{
"fieldname": "section_break_avhb",
"fieldtype": "Section Break",
"label": "Analytics"
"fieldtype": "Section Break"
},
{
"default": "0",
@@ -319,17 +335,57 @@
"description": "If enabled, the Secondary Items generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good.",
"fieldname": "deliver_secondary_items",
"fieldtype": "Check",
"label": "Deliver Secondary Items"
"label": "Deliver secondary Items"
},
{
"fieldname": "customer_defaults_tab",
"fieldtype": "Tab Break",
"label": "Customer Defaults"
},
{
"fieldname": "section_break_lpfl",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_hkqv",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_htyx",
"fieldtype": "Section Break"
},
{
"fieldname": "advanced_features_tab",
"fieldtype": "Tab Break",
"label": "Advanced Features"
},
{
"description": "Allow sales transactions with zero quantities if the rate is fixed but the quantities are not. e.g. Rate Contracts",
"fieldname": "section_break_zero_qty",
"fieldtype": "Section Break",
"label": "Zero-Quantity Line Items"
},
{
"fieldname": "section_break_jcmi",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_zwvf",
"fieldtype": "Column Break"
},
{
"fieldname": "blanket_orders_section",
"fieldtype": "Section Break",
"label": "Blanket Orders"
}
],
"grid_page_length": 50,
"hide_toolbar": 0,
"icon": "fa fa-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:18.988883",
"modified": "2026-04-21 21:29:32.890098",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint
from frappe.core.doctype.sms_settings.sms_settings import send_sms
from frappe.model.document import Document
from frappe.query_builder import functions as fn
from frappe.utils import cstr
@@ -41,73 +42,117 @@ class SMSCenter(Document):
@frappe.whitelist()
def create_receiver_list(self):
rec, where_clause = "", ""
if self.send_to == "All Customer Contact":
where_clause = " and dl.link_doctype = 'Customer'"
if self.customer:
where_clause += (
" and dl.link_name = '%s'" % self.customer.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
if self.send_to == "All Supplier Contact":
where_clause = " and dl.link_doctype = 'Supplier'"
if self.supplier:
where_clause += (
" and dl.link_name = '%s'" % self.supplier.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
if self.send_to == "All Sales Partner Contact":
where_clause = " and dl.link_doctype = 'Sales Partner'"
if self.sales_partner:
where_clause += (
"and dl.link_name = '%s'" % self.sales_partner.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
query = None
if self.send_to == "":
return
if self.send_to in [
"All Contact",
"All Customer Contact",
"All Supplier Contact",
"All Sales Partner Contact",
]:
rec = frappe.db.sql(
"""select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')),
c.mobile_no from `tabContact` c, `tabDynamic Link` dl where ifnull(c.mobile_no,'')!='' and
c.docstatus != 2 and dl.parent = c.name%s"""
% where_clause
)
query = self.get_contact_query_for_all_contacts()
elif self.send_to == "All Lead (Open)":
rec = frappe.db.sql(
"""select lead_name, mobile_no from `tabLead` where
ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'"""
)
query = self.get_contact_query_for_all_open_leads()
elif self.send_to == "All Employee (Active)":
where_clause = (
self.department and " and department = '%s'" % self.department.replace("'", "'") or ""
)
where_clause += self.branch and " and branch = '%s'" % self.branch.replace("'", "'") or ""
rec = frappe.db.sql(
"""select employee_name, cell_number from
`tabEmployee` where status = 'Active' and docstatus < 2 and
ifnull(cell_number,'')!='' %s"""
% where_clause
)
query = self.get_contact_query_for_all_active_employee()
elif self.send_to == "All Sales Person":
rec = frappe.db.sql(
"""select sales_person_name,
tabEmployee.cell_number from `tabSales Person` left join tabEmployee
on `tabSales Person`.employee = tabEmployee.name
where ifnull(tabEmployee.cell_number,'')!=''"""
)
query = self.get_contact_query_for_all_sales_person()
rec = query.run(as_list=1)
rec_list = ""
for d in rec:
rec_list += d[0] + " - " + d[1] + "\n"
self.receiver_list = rec_list
def get_contact_query_for_all_contacts(self):
Contact = frappe.qb.DocType("Contact")
DynamicLink = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(Contact)
.join(DynamicLink)
.on(DynamicLink.parent == Contact.name)
.select(
fn.Concat(fn.IfNull(Contact.first_name, ""), " ", fn.IfNull(Contact.last_name, "")),
Contact.mobile_no,
)
.where((fn.IfNull(Contact.mobile_no, "") != "") & (Contact.docstatus != 2))
)
if self.send_to == "All Customer Contact":
query = query.where(DynamicLink.link_doctype == "Customer")
query = (
query.where(DynamicLink.link_name == self.customer)
if self.customer
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
elif self.send_to == "All Supplier Contact":
query = query.where(DynamicLink.link_doctype == "Supplier")
query = (
query.where(DynamicLink.link_name == self.supplier)
if self.supplier
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
elif self.send_to == "All Sales Partner Contact":
query = query.where(DynamicLink.link_doctype == "Sales Partner")
query = (
query.where(DynamicLink.link_name == self.sales_partner)
if self.sales_partner
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
return query
def get_contact_query_for_all_open_leads(self):
Lead = frappe.qb.DocType("Lead")
query = (
frappe.qb.from_(Lead)
.select(Lead.lead_name, Lead.mobile)
.where((fn.IfNull(Lead.mobile_no, "") != "") & (Lead.docstatus != 2) & (Lead.status == "Open"))
)
return query
def get_contact_query_for_all_active_employee(self):
Employee = frappe.qb.DocType("Employee")
query = (
frappe.qb.from_(Employee)
.select(Employee.employee_name, Employee.cell_number)
.where(
(Employee.status == "Active")
& (Employee.docstatus != 2)
& (fn.IfNull(Employee.cell_number, "") != "")
)
)
if self.department:
query = query.where(Employee.department == self.department)
if self.branch:
query = query.where(Employee.branch == self.branch)
return query
def get_contact_query_for_all_sales_person(self):
SalesPerson = frappe.qb.DocType("Sales Person")
Employee = frappe.qb.DocType("Employee")
query = (
frappe.qb.from_(SalesPerson)
.left_join(Employee)
.on(SalesPerson.employee == Employee.name)
.select(SalesPerson.sales_person_name, Employee.cell_number)
.where(fn.IfNull(Employee.cell_number, "") != "")
)
return query
def get_receiver_nos(self):
receiver_nos = []
if self.receiver_list:

View File

@@ -11,8 +11,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAnalytics(ERPNextTestSuite):
def test_sales_analytics(self):
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
create_sales_orders()
self.compare_result_for_customer()

View File

@@ -44,7 +44,6 @@ class TestCompany(ERPNextTestSuite):
for prop, val in acc_property.items():
self.assertEqual(acc.get(prop), val)
self.delete_mode_of_payment("COA from Existing Company")
frappe.delete_doc("Company", "COA from Existing Company")
def test_coa_based_on_country_template(self):
@@ -95,16 +94,8 @@ class TestCompany(ERPNextTestSuite):
self.assertTrue(has_matching_accounts, msg=error_message)
finally:
self.delete_mode_of_payment(template)
frappe.delete_doc("Company", template)
def delete_mode_of_payment(self, company):
frappe.db.sql(
""" delete from `tabMode of Payment Account`
where company =%s """,
(company),
)
def test_basic_tree(self, records=None):
self.load_test_records("Company")
min_lft = 1

View File

@@ -158,10 +158,7 @@ class EmailDigest(Document):
context.quote = {"text": quote[0], "author": quote[1]}
if self.get("purchase_orders_items_overdue"):
(
context.purchase_order_list,
context.purchase_orders_items_overdue_list,
) = self.get_purchase_orders_items_overdue_list()
context.purchase_orders_items_overdue_map = self.get_purchase_orders_items_overdue_list()
if not context:
return None
@@ -860,30 +857,42 @@ class EmailDigest(Document):
return fmt_money(value, currency=self.currency)
def get_purchase_orders_items_overdue_list(self):
fields_po = "distinct `tabPurchase Order Item`.parent as po"
fields_poi = (
"`tabPurchase Order Item`.parent, `tabPurchase Order Item`.schedule_date, item_code,"
"received_qty, qty - received_qty as missing_qty, rate, amount"
po = frappe.qb.DocType("Purchase Order")
poi = frappe.qb.DocType("Purchase Order Item")
query = (
frappe.qb.from_(poi)
.select(
poi.parent,
poi.schedule_date,
poi.item_code,
poi.received_qty,
(poi.qty - poi.received_qty).as_("missing_qty"),
poi.rate,
poi.amount,
po.currency,
)
.inner_join(po)
.on(po.name == poi.parent)
.where(po.status != "Closed")
.where(poi.docstatus == 1)
.where(poi.schedule_date < today())
.where(poi.received_qty < poi.qty)
.where(po.company == self.company)
.orderby(poi.parent, order=frappe.qb.desc)
.orderby(poi.idx)
)
sql_po = f"""select {fields_po} from `tabPurchase Order Item`
left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent
where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date
and received_qty < qty order by `tabPurchase Order Item`.parent DESC,
`tabPurchase Order Item`.schedule_date DESC"""
items_by_parent = frappe._dict()
sql_poi = f"""select {fields_poi} from `tabPurchase Order Item`
left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent
where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date
and received_qty < qty order by `tabPurchase Order Item`.idx"""
purchase_order_list = frappe.db.sql(sql_po, as_dict=True)
purchase_order_items_overdue_list = frappe.db.sql(sql_poi, as_dict=True)
for row in query.run(as_dict=True):
row.link = get_url_to_form("Purchase Order", row.parent)
row.rate = fmt_money(row.rate, 2, row.currency)
row.amount = fmt_money(row.amount, 2, row.currency)
for t in purchase_order_items_overdue_list:
t.link = get_url_to_form("Purchase Order", t.parent)
t.rate = fmt_money(t.rate, 2, t.currency)
t.amount = fmt_money(t.amount, 2, t.currency)
return purchase_order_list, purchase_order_items_overdue_list
items_by_parent.setdefault(row.parent, []).append(row)
return items_by_parent
def send():

View File

@@ -182,7 +182,7 @@
{% endif %}
<!-- Purchase Order Items Overdue -->
{% if purchase_orders_items_overdue_list %}
{% if purchase_orders_items_overdue_map %}
<h4 style="{{ section_head }}" class="text-center">{{ _("Purchase Order Items not received on time") }}</h4>
<div>
<div style="background-color: #fafbfc;">
@@ -206,43 +206,41 @@
<hr>
</div>
<div>
{% for po in purchase_order_list %}
{% for po, po_items in purchase_orders_items_overdue_map.items() %}
<div style="{{ line_item }}">
<table style="width: 100%;">
<tr>
<th>
<span style="padding: 3px 7px; margin-right: 7px; font-weight: bold;">{{ po.po }}</span>
<span style="padding: 3px 7px; margin-right: 7px; font-weight: bold;">{{ po | e }}</span>
</th>
</tr>
<tr>
<td>
{% for t in purchase_orders_items_overdue_list %}
{% if t.parent == po.po %}
<div >
<table style="width: 100%;">
<tr>
<td style="padding-left: 7px;">
<a style="width: 40%; {{ link_css }}" href="{{ t.link }}">{{ _(t.item_code) }}</a>
</td>
<td style="width: 20%; text-align: right">
<span style="{{ label_css }}">
{{ t.missing_qty }}
</span>
</td>
<td style="width: 20%; text-align: right">
<span style="{{ label_css }}">
{{ t.rate }}
</span>
</td>
<td style="width: 20%; text-align: right">
<span style="{{ label_css }}">
{{ t.amount }}
</span>
</td>
</tr>
</table>
</div>
{% endif %}
{% for row in po_items %}
<div >
<table style="width: 100%; table-layout: fixed;">
<tr>
<td style="width: 40%; padding-left: 7px; vertical-align: top;">
<a style="{{ link_css }}" href="{{ row.link | e }}">{{ _(row.item_code) | e }}</a>
</td>
<td style="width: 20%; text-align: right; white-space: nowrap; vertical-align: top;">
<span style="{{ label_css }}">
{{ row.missing_qty | e }}
</span>
</td>
<td style="width: 20%; text-align: right; white-space: nowrap; vertical-align: top;">
<span style="{{ label_css }}">
{{ row.rate | e }}
</span>
</td>
<td style="width: 20%; text-align: right; white-space: nowrap; vertical-align: top;">
<span style="{{ label_css }}">
{{ row.amount | e }}
</span>
</td>
</tr>
</table>
</div>
{% endfor %}
</td>
</tr>

View File

@@ -2,8 +2,79 @@
# See license.txt
import unittest
import frappe
from frappe.utils import add_days, today
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.tests.utils import ERPNextTestSuite
class TestEmailDigest(ERPNextTestSuite):
pass
def test_purchase_orders_items_overdue_list_is_filtered_by_company(self):
digest = create_email_digest(
company="_Test Company",
frequency="Daily",
purchase_orders_items_overdue=1,
name="Test Email Digest PO Company Filter",
)
backdate = add_days(today(), -1)
po1 = create_purchase_order(transaction_date=backdate, do_not_save=True)
po1.schedule_date = backdate
po1.items[0].schedule_date = backdate
po1.insert()
po1.submit()
po2 = create_purchase_order(
company="_Test Company 1",
warehouse="Stores - _TC1",
transaction_date=backdate,
do_not_save=True,
)
po2.schedule_date = backdate
po2.items[0].schedule_date = backdate
po2.insert()
po2.submit()
overdue_items = digest.get_purchase_orders_items_overdue_list()
self.assertIn(po1.name, overdue_items)
self.assertNotIn(po2.name, overdue_items)
def create_email_digest(**args):
args = frappe._dict(args)
doc = frappe.new_doc("Email Digest")
doc.name = args.name or "Test Email Digest"
doc.company = args.company or "_Test Company"
doc.frequency = args.frequency or "Daily"
doc.enabled = args.enabled or 0
doc.bank_balance = args.bank_balance or 0
doc.credit_balance = args.credit_balance or 0
doc.invoiced_amount = args.invoiced_amount or 0
doc.payables = args.payables or 0
doc.sales_orders_to_bill = args.sales_orders_to_bill or 0
doc.purchase_orders_to_bill = args.purchase_orders_to_bill or 0
doc.sales_order = args.sales_order or 0
doc.purchase_order = args.purchase_order or 0
doc.sales_orders_to_deliver = args.sales_orders_to_deliver or 0
doc.purchase_orders_to_receive = args.purchase_orders_to_receive or 0
doc.sales_invoice = args.sales_invoice or 0
doc.purchase_invoice = args.purchase_invoice or 0
doc.new_quotations = args.new_quotations or 0
doc.pending_quotations = args.pending_quotations or 0
doc.issue = args.issue or 0
doc.project = args.project or 0
doc.purchase_orders_items_overdue = args.purchase_orders_items_overdue or 0
doc.calendar_events = args.calendar_events or 0
doc.todo_list = args.todo_list or 0
doc.notifications = args.notifications or 0
doc.add_quote = args.add_quote or 0
for recipient in args.recipients or ["Administrator"]:
doc.append("recipients", {"recipient": recipient})
if not args.do_not_save:
doc.insert()
return doc

View File

@@ -38,6 +38,7 @@ def after_install():
update_pegged_currencies()
set_default_print_formats()
create_letter_head()
toggle_hidden_fields()
frappe.db.commit()
@@ -364,6 +365,21 @@ def create_letter_head():
doc.insert(ignore_permissions=True)
def toggle_hidden_fields():
from erpnext.accounts.doctype.accounts_settings.accounts_settings import (
toggle_accounting_dimension_sections,
toggle_loyalty_point_program_section,
toggle_sales_discount_section,
toggle_subscription_sections,
)
acc_settings = frappe.get_doc("Accounts Settings")
toggle_accounting_dimension_sections(not acc_settings.enable_accounting_dimensions)
toggle_sales_discount_section(not acc_settings.enable_discounts_and_margin)
toggle_subscription_sections(not acc_settings.enable_subscription)
toggle_loyalty_point_program_section(not acc_settings.enable_loyalty_point_program)
DEFAULT_ROLE_PROFILES = {
_("Inventory"): [
"Stock User",

View File

@@ -7,7 +7,7 @@ def get_data():
"transactions": [
{"label": _("Buy"), "items": ["Purchase Invoice", "Purchase Receipt"]},
{"label": _("Sell"), "items": ["Sales Invoice", "Delivery Note"]},
{"label": _("Move"), "items": ["Serial and Batch Bundle"]},
{"label": _("Move"), "items": ["Stock Entry", "Serial and Batch Bundle"]},
{"label": _("Quality"), "items": ["Quality Inspection"]},
],
}

View File

@@ -22,6 +22,8 @@
"is_return",
"issue_credit_note",
"return_against",
"section_break_zxcd",
"title",
"accounting_dimensions_section",
"cost_center",
"column_break_18",
@@ -76,7 +78,6 @@
"base_totals_section",
"base_grand_total",
"base_in_words",
"column_break_ydwe",
"base_rounding_adjustment",
"base_rounded_total",
"section_break_49",
@@ -147,7 +148,7 @@
"total_commission",
"section_break1",
"sales_team",
"subscription_section",
"auto_repeat_section",
"auto_repeat",
"printing_details",
"letter_head",
@@ -1115,11 +1116,6 @@
"oldfieldname": "instructions",
"oldfieldtype": "Text"
},
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
},
{
"fieldname": "auto_repeat",
"fieldtype": "Link",
@@ -1446,13 +1442,30 @@
{
"fieldname": "column_break_neoj",
"fieldtype": "Column Break"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_zxcd",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2026-03-09 17:15:27.932956",
"modified": "2026-04-28 06:37:33.600775",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -145,6 +145,7 @@ class DeliveryNote(SellingController):
tc_name: DF.Link | None
terms: DF.TextEditor | None
territory: DF.Link | None
title: DF.Data | None
total: DF.Currency
total_commission: DF.Currency
total_net_weight: DF.Float

View File

@@ -173,6 +173,8 @@ class InventoryDimension(Document):
mandatory_depends_on = "eval:doc.s_warehouse"
elif doctype == "Subcontracting Receipt Supplied Item":
mandatory_depends_on = "eval:doc.reference_name"
elif doctype == "Packed Item":
mandatory_depends_on = "eval:doc.parent_detail_docname && ['Delivery Note', 'Sales Invoice', 'POS Invoice'].includes(parent.doctype)"
dimension_fields = [
dict(
@@ -193,7 +195,8 @@ class InventoryDimension(Document):
reqd=1
if self.reqd
and not self.mandatory_depends_on
and doctype not in ["Stock Entry Detail", "Subcontracting Receipt Supplied Item"]
and doctype
not in ["Stock Entry Detail", "Subcontracting Receipt Supplied Item", "Packed Item"]
else 0,
mandatory_depends_on=mandatory_depends_on,
),

View File

@@ -459,11 +459,6 @@ class TestItem(ERPNextTestSuite):
frappe.delete_doc_if_exists("Item", "_Test Numeric Template Item")
frappe.delete_doc_if_exists("Item Attribute", "Test Item Length")
frappe.db.sql(
"""delete from `tabItem Variant Attribute`
where attribute='Test Item Length' """
)
frappe.flags.attribute_values = None
# make item attribute
@@ -607,7 +602,6 @@ class TestItem(ERPNextTestSuite):
def test_add_item_barcode(self):
# Clean up
frappe.db.sql("""delete from `tabItem Barcode`""")
item_code = "Test Item Barcode"
if frappe.db.exists("Item", item_code):
frappe.delete_doc("Item", item_code)

View File

@@ -8,6 +8,7 @@
"parent_item",
"item_code",
"item_name",
"delivered_by_supplier",
"column_break_5",
"description",
"section_break_6",
@@ -23,7 +24,6 @@
"use_serial_batch_fields",
"column_break_11",
"serial_and_batch_bundle",
"delivered_by_supplier",
"section_break_bgys",
"serial_no",
"column_break_qlha",
@@ -119,6 +119,7 @@
"read_only": 1
},
{
"depends_on": "eval:!['Sales Order', 'Quotation'].includes(parent.doctype)",
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
@@ -285,7 +286,7 @@
"label": "Use Serial No / Batch Fields"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"depends_on": "eval:doc.use_serial_batch_fields === 1 && !['Sales Order', 'Quotation'].includes(parent.doctype)",
"fieldname": "section_break_bgys",
"fieldtype": "Section Break"
},
@@ -314,7 +315,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-16 18:10:47.511381",
"modified": "2026-04-27 14:12:53.236906",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",

View File

@@ -25,6 +25,8 @@
"apply_putaway_rule",
"is_return",
"return_against",
"section_break_yxar",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -1285,6 +1287,18 @@
{
"fieldname": "column_break_ugyv",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_yxar",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -1292,7 +1306,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2026-04-06 14:11:29.630333",
"modified": "2026-04-28 06:47:12.170270",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -148,6 +148,7 @@ class PurchaseReceipt(BuyingController):
taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
total: DF.Currency
total_net_weight: DF.Float
total_qty: DF.Float
@@ -560,7 +561,14 @@ class PurchaseReceipt(BuyingController):
else flt(item.net_amount, item.precision("net_amount"))
)
outgoing_amount = item.qty * item.base_net_rate
outgoing_amount = (
flt((item.base_net_amount / item.received_qty) * item.qty, item.precision("base_net_amount"))
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
if self.is_internal_transfer() and item.valuation_rate:
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
credit_amount = outgoing_amount

View File

@@ -48,26 +48,14 @@ frappe.ui.form.on("Quality Inspection", {
// item code based on GRN/DN
frm.set_query("item_code", function (doc) {
let doctype = doc.reference_type;
if (doc.reference_type !== "Job Card") {
doctype =
doc.reference_type == "Stock Entry" ? "Stock Entry Detail" : doc.reference_type + " Item";
}
if (doc.reference_type && doc.reference_name) {
let filters = {
from: doctype,
parent_doctype: doc.reference_type,
inspection_type: doc.inspection_type,
};
if (doc.reference_type == doctype) filters["reference_name"] = doc.reference_name;
else filters["parent"] = doc.reference_name;
return {
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
filters: filters,
filters: {
reference_doctype: doc.reference_type,
reference_name: doc.reference_name,
inspection_type: doc.inspection_type,
},
};
}
});

View File

@@ -365,58 +365,63 @@ class QualityInspection(Document):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond
reference_doctype = filters.get("reference_doctype")
from_doctype = cstr(filters.get("from"))
parent_doctype = cstr(filters.get("parent_doctype"))
if not from_doctype or not frappe.db.exists("DocType", from_doctype):
if not reference_doctype:
return []
mcond = get_match_cond(parent_doctype or from_doctype)
cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
if filters.get("parent"):
if (
from_doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]
and filters.get("inspection_type") != "In Process"
):
cond = """and item_code in (select name from `tabItem` where
inspection_required_before_purchase = 1)"""
elif (
from_doctype in ["Sales Invoice Item", "Delivery Note Item"]
and filters.get("inspection_type") != "In Process"
):
cond = """and item_code in (select name from `tabItem` where
inspection_required_before_delivery = 1)"""
elif from_doctype == "Stock Entry Detail":
cond = """and s_warehouse is null"""
if from_doctype in ["Supplier Quotation Item"]:
qi_condition = ""
return frappe.db.sql(
f"""
SELECT distinct item_code, item_name
FROM `tab{from_doctype}`
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond}
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
""",
{"parent": filters.get("parent"), "txt": "%%%s%%" % txt},
elif reference_doctype == "Job Card":
production_item, item_name = frappe.get_value(
"Job Card", filters.get("reference_name"), ["production_item", "item_name"]
)
return ((production_item, item_name),)
else:
my_filters = [
["items.parent", "=", filters.get("reference_name")],
"and",
["items.item_code", "like", f"%{txt}%"],
"and",
["docstatus", "<", 2],
"and",
["items.quality_inspection", "is", "not set"],
]
elif filters.get("reference_name"):
return frappe.db.sql(
f"""
SELECT production_item
FROM `tab{from_doctype}`
WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s
{qi_condition} {cond} {mcond}
ORDER BY production_item
limit {cint(page_len)} offset {cint(start)}
""",
{"reference_name": filters.get("reference_name"), "txt": "%%%s%%" % txt},
)
if reference_doctype == "Stock Entry":
my_filters.extend(
[
"and",
["items.t_warehouse", "is", "not set"],
]
)
elif filters.get("inspection_type") != "In Process":
my_filters.extend(
[
"and",
[
"items.item_code",
"in",
frappe.get_list(
"Item",
filters={
"inspection_required_before_purchase"
if filters.get("inspection_type") == "Incoming"
else "inspection_required_before_delivery": 1
},
pluck="name",
),
],
]
)
return frappe.get_query(
reference_doctype,
fields=["items.item_code, items.item_name"],
filters=my_filters,
offset=start,
limit=page_len,
order_by="items.item_code",
ignore_permissions=False,
distinct=True,
).run()
@frappe.whitelist()

View File

@@ -931,6 +931,7 @@ class SerialandBatchBundle(Document):
parent.voucher_type,
parent.voucher_no,
)
.distinct()
.where(
(child.parent != self.name)
& (parent.item_code == self.item_code)

View File

@@ -278,8 +278,7 @@
"oldfieldname": "transfer_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1,
"reqd": 1
"read_only": 1
},
{
"default": "0",
@@ -680,7 +679,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-03-02 14:05:23.116017",
"modified": "2026-04-27 11:40:38.294196",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -33,18 +33,9 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestStockLedgerEntry(ERPNextTestSuite, StockTestMixin):
def setUp(self):
items = create_items()
create_items()
reset("Stock Entry")
# delete SLE and BINs for all items
frappe.db.sql(
"delete from `tabStock Ledger Entry` where item_code in (%s)" % (", ".join(["%s"] * len(items))),
items,
)
frappe.db.sql(
"delete from `tabBin` where item_code in (%s)" % (", ".join(["%s"] * len(items))), items
)
def test_item_cost_reposting(self):
company = "_Test Company"

View File

@@ -615,5 +615,5 @@ class FIFOSlots:
sr_item = frappe.db.get_value(
"Stock Reconciliation Item", row.voucher_detail_no, ["current_qty", "qty"], as_dict=True
)
if sr_item.qty and sr_item.current_qty:
if sr_item and sr_item.qty and sr_item.current_qty:
self.stock_reco_voucher_wise_count[row.voucher_detail_no] = sr_item.current_qty

View File

@@ -955,6 +955,9 @@ class update_entries_after:
if not self.wh_data.qty_after_transaction:
self.wh_data.stock_value = 0.0
if sle.actual_qty < 0:
sle.incoming_rate = 0
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
self.wh_data.prev_stock_value = self.wh_data.stock_value

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