mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-01 04:28:27 +00:00
Merge pull request #54583 from frappe/version-16-hotfix
This commit is contained in:
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@@ -41,6 +41,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
TZ: 'Asia/Kolkata'
|
||||
NODE_ENV: "production"
|
||||
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -56,6 +57,7 @@ jobs:
|
||||
mysql:
|
||||
image: mariadb:10.6
|
||||
env:
|
||||
TZ: 'Asia/Kolkata'
|
||||
MARIADB_ROOT_PASSWORD: 'root'
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
||||
@@ -5,8 +5,7 @@ frappe.ui.form.on("Account", {
|
||||
setup: function (frm) {
|
||||
frm.add_fetch("parent_account", "report_type", "report_type");
|
||||
frm.add_fetch("parent_account", "root_type", "root_type");
|
||||
},
|
||||
onload: function (frm) {
|
||||
|
||||
frm.set_query("parent_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -15,7 +14,18 @@ frappe.ui.form.on("Account", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("account_category", function () {
|
||||
if (!frm.doc.root_type) return;
|
||||
|
||||
return {
|
||||
filters: {
|
||||
root_type: ["in", [frm.doc.root_type, ""]],
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.toggle_display("account_name", frm.is_new());
|
||||
|
||||
@@ -58,12 +68,20 @@ frappe.ui.form.on("Account", {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
account_type: function (frm) {
|
||||
if (frm.doc.is_group == 0) {
|
||||
frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
|
||||
frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
|
||||
}
|
||||
},
|
||||
|
||||
root_type: function (frm) {
|
||||
if (frm.doc.account_category) {
|
||||
frm.set_value("account_category", "");
|
||||
}
|
||||
},
|
||||
|
||||
add_toolbar_buttons: function (frm) {
|
||||
frm.add_custom_button(
|
||||
__("Chart of Accounts"),
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account_category_name",
|
||||
"root_type",
|
||||
"column_break_qluu",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
@@ -14,6 +16,7 @@
|
||||
"fieldname": "account_category_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Category Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
@@ -22,6 +25,18 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qluu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "root_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Root Type",
|
||||
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -32,7 +47,7 @@
|
||||
"link_fieldname": "account_category"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-23 01:19:49.589393",
|
||||
"modified": "2026-03-05 06:49:34.430723",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Category",
|
||||
@@ -69,7 +84,7 @@
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "account_category_name, description",
|
||||
"search_fields": "account_category_name, root_type",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
|
||||
@@ -21,6 +21,7 @@ class AccountCategory(Document):
|
||||
|
||||
account_category_name: DF.Data
|
||||
description: DF.SmallText | None
|
||||
root_type: DF.Literal["", "Asset", "Liability", "Income", "Expense", "Equity"]
|
||||
# end: auto-generated types
|
||||
|
||||
def after_rename(self, old_name, new_name, merge):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FinancialReportEngine,
|
||||
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
|
||||
@@ -13,9 +13,6 @@ COMPANY_SHORT_NAME = "_TC6"
|
||||
|
||||
class TestBalanceSheet(ERPNextTestSuite):
|
||||
def test_balance_sheet(self):
|
||||
frappe.db.sql(f"delete from `tabJournal Entry` where company='{COMPANY}'")
|
||||
frappe.db.sql(f"delete from `tabGL Entry` where company='{COMPANY}'")
|
||||
|
||||
create_account("VAT Liabilities", f"Duties and Taxes - {COMPANY_SHORT_NAME}", COMPANY)
|
||||
create_account("Advance VAT Paid", f"Duties and Taxes - {COMPANY_SHORT_NAME}", COMPANY)
|
||||
create_account("My Bank", f"Bank Accounts - {COMPANY_SHORT_NAME}", COMPANY)
|
||||
|
||||
@@ -12,6 +12,7 @@ from pypika import Order
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FinancialReportEngine,
|
||||
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
get_columns,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
FinancialReportEngine,
|
||||
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ class TestSupplierScorecard(ERPNextTestSuite):
|
||||
self.assertEqual(doc.name, valid_scorecard[0].get("supplier"))
|
||||
|
||||
def test_criteria_weight(self):
|
||||
delete_test_scorecards()
|
||||
my_doc = make_supplier_scorecard()
|
||||
for d in my_doc.criteria:
|
||||
d.weight = 0
|
||||
@@ -33,26 +32,6 @@ def make_supplier_scorecard():
|
||||
return my_doc
|
||||
|
||||
|
||||
def delete_test_scorecards():
|
||||
my_doc = make_supplier_scorecard()
|
||||
if frappe.db.exists("Supplier Scorecard", my_doc.name):
|
||||
# Delete all the periods, then delete the scorecard
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Period` where scorecard = %(scorecard)s""",
|
||||
{"scorecard": my_doc.name},
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.delete_doc(my_doc.doctype, my_doc.name)
|
||||
|
||||
|
||||
valid_scorecard = [
|
||||
{
|
||||
"standings": [
|
||||
|
||||
@@ -9,41 +9,15 @@ from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
class TestSupplierScorecardCriteria(ERPNextTestSuite):
|
||||
def test_variables_exist(self):
|
||||
delete_test_scorecards()
|
||||
for d in test_good_criteria:
|
||||
frappe.get_doc(d).insert()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[0]).insert)
|
||||
|
||||
def test_formula_validate(self):
|
||||
delete_test_scorecards()
|
||||
self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[1]).insert)
|
||||
|
||||
|
||||
def delete_test_scorecards():
|
||||
# Delete all the periods so we can delete all the criteria
|
||||
frappe.db.sql("""delete from `tabSupplier Scorecard Period`""")
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'"""
|
||||
)
|
||||
|
||||
for d in test_good_criteria:
|
||||
if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")):
|
||||
# Delete all the periods, then delete the scorecard
|
||||
frappe.delete_doc(d.get("doctype"), d.get("name"))
|
||||
|
||||
for d in test_bad_criteria:
|
||||
if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")):
|
||||
# Delete all the periods, then delete the scorecard
|
||||
frappe.delete_doc(d.get("doctype"), d.get("name"))
|
||||
|
||||
|
||||
test_good_criteria = [
|
||||
{
|
||||
"name": "Delivery",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
200
erpnext/edi/doctype/code_list/test_code_list_import.py
Normal file
200
erpnext/edi/doctype/code_list/test_code_list_import.py
Normal 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"]))
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
15
erpnext/patches/v16_0/correct_po_titles.py
Normal file
15
erpnext/patches/v16_0/correct_po_titles.py
Normal 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()
|
||||
27
erpnext/patches/v16_0/packed_item_inv_dimen.py
Normal file
27
erpnext/patches/v16_0/packed_item_inv_dimen.py
Normal 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)",
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
|
||||
32
erpnext/patches/v16_0/set_root_type_in_account_categories.py
Normal file
32
erpnext/patches/v16_0/set_root_type_in_account_categories.py
Normal 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,
|
||||
)
|
||||
@@ -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`")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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],
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ pincode }} {{ city | upper }}<br>
|
||||
{{ country | upper }}
|
||||
4
erpnext/regional/address_template/templates/denmark.html
Normal file
4
erpnext/regional/address_template/templates/denmark.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ pincode }} {{ city | upper }}<br>
|
||||
{{ country | upper }}
|
||||
@@ -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
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user