Merge branch 'develop' into fixing-emp-contacts

This commit is contained in:
mergify[bot]
2026-02-18 10:47:34 +00:00
committed by GitHub
325 changed files with 134547 additions and 111672 deletions

View File

@@ -60,7 +60,7 @@ body:
description: Share exact version number of Frappe and ERPNext you are using. description: Share exact version number of Frappe and ERPNext you are using.
placeholder: | placeholder: |
Frappe version - Frappe version -
ERPNext Verion - ERPNext version -
validations: validations:
required: true required: true

View File

@@ -15,7 +15,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
branch: ["develop"] branch: ["develop", "version-16-hotfix"]
permissions: permissions:
contents: write contents: write

View File

@@ -7,6 +7,7 @@ on:
paths: paths:
- "**.js" - "**.js"
- "**.css" - "**.css"
- "**.svg"
- "**.md" - "**.md"
- "**.html" - "**.html"
- 'crowdin.yml' - 'crowdin.yml'

View File

@@ -1,3 +1,4 @@
<div align="center"> <div align="center">
<a href="https://frappe.io/erpnext"> <a href="https://frappe.io/erpnext">
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/> <img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>

View File

@@ -0,0 +1,50 @@
{
"cards": [
{
"card": "Total Outgoing Bills"
},
{
"card": "Total Incoming Bills"
},
{
"card": "Total Incoming Payment"
},
{
"card": "Total Outgoing Payment"
}
],
"charts": [
{
"chart": "Incoming Bills (Purchase Invoice)",
"width": "Half"
},
{
"chart": "Outgoing Bills (Sales Invoice)",
"width": "Half"
},
{
"chart": "Accounts Receivable Ageing",
"width": "Half"
},
{
"chart": "Accounts Payable Ageing",
"width": "Half"
},
{
"chart": "Bank Balance",
"width": "Full"
}
],
"creation": "2026-01-26 21:25:12.793893",
"dashboard_name": "Payments",
"docstatus": 0,
"doctype": "Dashboard",
"idx": 0,
"is_default": 0,
"is_standard": 1,
"modified": "2026-01-26 21:25:12.793893",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payments",
"owner": "Administrator"
}

View File

@@ -33,6 +33,17 @@
}, },
"account_number": "1151.000" "account_number": "1151.000"
}, },
"Pajak Dibayar di Muka": {
"PPN Masukan": {
"account_number": "1152.001",
"account_type": "Tax"
},
"PPh 23 Dibayar di Muka": {
"account_number": "1152.002",
"account_type": "Tax"
},
"account_number": "1152.000"
},
"account_number": "1150.000" "account_number": "1150.000"
}, },
"Kas": { "Kas": {
@@ -97,17 +108,6 @@
}, },
"account_number": "1130.000" "account_number": "1130.000"
}, },
"Pajak Dibayar di Muka": {
"PPN Masukan": {
"account_number": "1151.001",
"account_type": "Tax"
},
"PPh 23 Dibayar di Muka": {
"account_number": "1152.001",
"account_type": "Tax"
},
"account_number": "1150.000"
},
"account_number": "1100.000" "account_number": "1100.000"
}, },

View File

@@ -97,7 +97,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
if doc.doctype == "Bank Clearance": if doc.doctype == "Bank Clearance":
return return
elif doc.doctype == "Asset": elif doc.doctype == "Asset":
if doc.is_existing_asset: if doc.asset_type == "Existing Asset":
return return
else: else:
date = doc.available_for_use_date date = doc.available_for_use_date

View File

@@ -20,6 +20,10 @@
"enable_common_party_accounting", "enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account", "allow_multi_currency_invoices_against_single_party_account",
"confirm_before_resetting_posting_date", "confirm_before_resetting_posting_date",
"analytics_section",
"enable_accounting_dimensions",
"column_break_vtnr",
"enable_discounts_and_margin",
"journals_section", "journals_section",
"merge_similar_account_heads", "merge_similar_account_heads",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
@@ -51,12 +55,16 @@
"allow_pegged_currencies_exchange_rates", "allow_pegged_currencies_exchange_rates",
"column_break_yuug", "column_break_yuug",
"stale_days", "stale_days",
"payments_tab",
"section_break_jpd0", "section_break_jpd0",
"auto_reconcile_payments", "auto_reconcile_payments",
"auto_reconciliation_job_trigger", "auto_reconciliation_job_trigger",
"reconciliation_queue_size", "reconciliation_queue_size",
"column_break_resa", "column_break_resa",
"exchange_gain_loss_posting_date", "exchange_gain_loss_posting_date",
"payment_options_section",
"enable_loyalty_point_program",
"column_break_ctam",
"invoicing_settings_tab", "invoicing_settings_tab",
"accounts_transactions_settings_section", "accounts_transactions_settings_section",
"over_billing_allowance", "over_billing_allowance",
@@ -281,7 +289,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>", "description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\" rel=\"noopener noreferrer\">Common Party</a>",
"fieldname": "enable_common_party_accounting", "fieldname": "enable_common_party_accounting",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Common Party Accounting" "label": "Enable Common Party Accounting"
@@ -637,16 +645,59 @@
"fieldname": "budget_section", "fieldname": "budget_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Budget" "label": "Budget"
},
{
"fieldname": "analytics_section",
"fieldtype": "Section Break",
"label": "Analytical Accounting"
},
{
"fieldname": "column_break_vtnr",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Apply discounts and margins on products",
"fieldname": "enable_discounts_and_margin",
"fieldtype": "Check",
"label": "Enable Discounts and Margin"
},
{
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"label": "Payments"
},
{
"fieldname": "payment_options_section",
"fieldtype": "Section Break",
"label": "Payment Options"
},
{
"default": "0",
"fieldname": "enable_loyalty_point_program",
"fieldtype": "Check",
"label": "Enable Loyalty Point Program"
},
{
"fieldname": "column_break_ctam",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Enable cost center, projects and other custom accounting dimensions",
"fieldname": "enable_accounting_dimensions",
"fieldtype": "Check",
"label": "Enable Accounting Dimensions"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"hide_toolbar": 1, "hide_toolbar": 0,
"icon": "icon-cog", "icon": "icon-cog",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-01-11 18:30:45.968531", "modified": "2026-02-04 17:15:38.609327",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -12,6 +12,28 @@ from frappe.utils import cint
from erpnext.accounts.utils import sync_auto_reconcile_config from erpnext.accounts.utils import sync_auto_reconcile_config
SELLING_DOCTYPES = [
"Sales Invoice",
"Sales Order",
"Delivery Note",
"Quotation",
"Sales Invoice Item",
"Sales Order Item",
"Delivery Note Item",
"Quotation Item",
"POS Invoice",
"POS Invoice Item",
]
BUYING_DOCTYPES = [
"Purchase Invoice",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice Item",
"Purchase Order Item",
"Purchase Receipt Item",
]
class AccountsSettings(Document): class AccountsSettings(Document):
# begin: auto-generated types # begin: auto-generated types
@@ -43,9 +65,12 @@ class AccountsSettings(Document):
default_ageing_range: DF.Data | None default_ageing_range: DF.Data | None
delete_linked_ledger_entries: DF.Check delete_linked_ledger_entries: DF.Check
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"] determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
enable_accounting_dimensions: DF.Check
enable_common_party_accounting: DF.Check enable_common_party_accounting: DF.Check
enable_discounts_and_margin: DF.Check
enable_fuzzy_matching: DF.Check enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_party_matching: DF.Check enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"] exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_valuation_rate_for_internal_transaction: DF.Check fetch_valuation_rate_for_internal_transaction: DF.Check
@@ -98,6 +123,18 @@ class AccountsSettings(Document):
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print: if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
if old_doc.enable_accounting_dimensions != self.enable_accounting_dimensions:
toggle_accounting_dimension_sections(not self.enable_accounting_dimensions)
clear_cache = True
if old_doc.enable_discounts_and_margin != self.enable_discounts_and_margin:
toggle_sales_discount_section(not self.enable_discounts_and_margin)
clear_cache = True
if old_doc.enable_loyalty_point_program != self.enable_loyalty_point_program:
toggle_loyalty_point_program_section(not self.enable_loyalty_point_program)
clear_cache = True
if clear_cache: if clear_cache:
frappe.clear_cache() frappe.clear_cache()
@@ -154,3 +191,36 @@ class AccountsSettings(Document):
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}") frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}") frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
def toggle_accounting_dimension_sections(hide):
accounting_dimension_doctypes = frappe.get_hooks("accounting_dimension_doctypes")
for doctype in accounting_dimension_doctypes:
create_property_setter_for_hiding_field(doctype, "accounting_dimensions_section", hide)
def toggle_sales_discount_section(hide):
for doctype in SELLING_DOCTYPES + BUYING_DOCTYPES:
meta = frappe.get_meta(doctype)
if meta.has_field("additional_discount_section"):
create_property_setter_for_hiding_field(doctype, "additional_discount_section", hide)
if meta.has_field("discount_and_margin"):
create_property_setter_for_hiding_field(doctype, "discount_and_margin", hide)
def toggle_loyalty_point_program_section(hide):
for doctype in SELLING_DOCTYPES:
meta = frappe.get_meta(doctype)
if meta.has_field("loyalty_points_redemption"):
create_property_setter_for_hiding_field(doctype, "loyalty_points_redemption", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,
field_name,
"hidden",
hide,
"Check",
validate_fields_for_doctype=False,
)

View File

@@ -50,6 +50,7 @@
"fieldname": "amount", "fieldname": "amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount", "label": "Amount",
"options": "currency",
"read_only": 1 "read_only": 1
}, },
{ {

View File

@@ -3,9 +3,6 @@
frappe.provide("erpnext.integrations"); frappe.provide("erpnext.integrations");
frappe.ui.form.on("Bank", { frappe.ui.form.on("Bank", {
onload: function (frm) {
add_fields_to_mapping_table(frm);
},
refresh: function (frm) { refresh: function (frm) {
add_fields_to_mapping_table(frm); add_fields_to_mapping_table(frm);
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal); frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
@@ -37,11 +34,11 @@ let add_fields_to_mapping_table = function (frm) {
}); });
}); });
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property( const grid = frm.fields_dict.bank_transaction_mapping?.grid;
"bank_transaction_field",
"options", if (grid) {
options grid.update_docfield_property("bank_transaction_field", "options", options);
); }
}; };
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
@@ -116,7 +113,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
"There was an issue connecting to Plaid's authentication server. Check browser console for more information" "There was an issue connecting to Plaid's authentication server. Check browser console for more information"
) )
); );
console.log(error); console.error(error);
} }
plaid_success(token, response) { plaid_success(token, response) {

View File

@@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", {
}); });
} }
}, },
is_company_account: function (frm) {
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
},
}); });

View File

@@ -52,6 +52,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Company Account", "label": "Company Account",
"mandatory_depends_on": "is_company_account",
"options": "Account" "options": "Account"
}, },
{ {
@@ -98,6 +99,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Company", "label": "Company",
"mandatory_depends_on": "is_company_account",
"options": "Company" "options": "Company"
}, },
{ {
@@ -252,7 +254,7 @@
"link_fieldname": "default_bank_account" "link_fieldname": "default_bank_account"
} }
], ],
"modified": "2025-08-29 12:32:01.081687", "modified": "2026-01-20 00:46:16.633364",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Account", "name": "Bank Account",

View File

@@ -51,25 +51,29 @@ class BankAccount(Document):
delete_contact_and_address("Bank Account", self.name) delete_contact_and_address("Bank Account", self.name)
def validate(self): def validate(self):
self.validate_company() self.validate_is_company_account()
self.validate_account()
self.update_default_bank_account() self.update_default_bank_account()
def validate_account(self): def validate_is_company_account(self):
if self.account: if self.is_company_account:
if accounts := frappe.db.get_all( if not self.company:
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1 frappe.throw(_("Company is mandatory for company account"))
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
def validate_company(self): if not self.account:
if self.is_company_account and not self.company: frappe.throw(_("Company Account is mandatory"))
frappe.throw(_("Company is mandatory for company account"))
self.validate_account()
def validate_account(self):
if accounts := frappe.db.get_all(
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
def update_default_bank_account(self): def update_default_bank_account(self):
if self.is_default and not self.disabled: if self.is_default and not self.disabled:

View File

@@ -15,7 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import cstr, date_diff, flt, getdate from frappe.utils import cstr, date_diff, flt, getdate
from pypika.terms import LiteralValue from pypika.terms import Bracket, LiteralValue
from erpnext import get_company_currency from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -541,7 +541,7 @@ class FinancialQueryBuilder:
.where(acb_table.period_closing_voucher == closing_voucher) .where(acb_table.period_closing_voucher == closing_voucher)
) )
query = self._apply_standard_filters(query, acb_table) query = self._apply_standard_filters(query, acb_table, "Account Closing Balance")
results = self._execute_with_permissions(query, "Account Closing Balance") results = self._execute_with_permissions(query, "Account Closing Balance")
for row in results: for row in results:
@@ -636,12 +636,15 @@ class FinancialQueryBuilder:
return self._execute_with_permissions(query, "GL Entry") return self._execute_with_permissions(query, "GL Entry")
def _calculate_running_balances(self, balances_data: dict, gl_data: list[dict]) -> dict: def _calculate_running_balances(self, balances_data: dict, gl_data: list[dict]) -> dict:
for row in gl_data: gl_dict = {row["account"]: row for row in gl_data}
account = row["account"] accounts = set(balances_data.keys()) | set(gl_dict.keys())
for account in accounts:
if account not in balances_data: if account not in balances_data:
balances_data[account] = AccountData(account=account, **self._get_account_meta(account)) balances_data[account] = AccountData(account=account, **self._get_account_meta(account))
account_data: AccountData = balances_data[account] account_data: AccountData = balances_data[account]
gl_movement = gl_dict.get(account, {})
if account_data.has_periods(): if account_data.has_periods():
first_period = account_data.get_period(self.periods[0]["key"]) first_period = account_data.get_period(self.periods[0]["key"])
@@ -651,20 +654,13 @@ class FinancialQueryBuilder:
for period in self.periods: for period in self.periods:
period_key = period["key"] period_key = period["key"]
movement = row.get(period_key, 0.0) movement = gl_movement.get(period_key, 0.0)
closing_balance = current_balance + movement closing_balance = current_balance + movement
account_data.add_period(PeriodValue(period_key, current_balance, closing_balance, movement)) account_data.add_period(PeriodValue(period_key, current_balance, closing_balance, movement))
current_balance = closing_balance current_balance = closing_balance
# Accounts with no movements
for account_data in balances_data.values():
for period in self.periods:
period_key = period["key"]
if period_key not in account_data.period_values:
account_data.add_period(PeriodValue(period_key, 0.0, 0.0, 0.0))
def _handle_balance_accumulation(self, balances_data): def _handle_balance_accumulation(self, balances_data):
for account_data in balances_data.values(): for account_data in balances_data.values():
account_data: AccountData account_data: AccountData
@@ -683,12 +679,12 @@ class FinancialQueryBuilder:
else: else:
account_data.unaccumulate_values() account_data.unaccumulate_values()
def _apply_standard_filters(self, query, table): def _apply_standard_filters(self, query, table, doctype: str = "GL Entry"):
if self.filters.get("ignore_closing_entries"): if self.filters.get("ignore_closing_entries"):
if hasattr(table, "is_period_closing_voucher_entry"): if doctype == "GL Entry":
query = query.where(table.is_period_closing_voucher_entry == 0)
else:
query = query.where(table.voucher_type != "Period Closing Voucher") query = query.where(table.voucher_type != "Period Closing Voucher")
else:
query = query.where(table.is_period_closing_voucher_entry == 0)
if self.filters.get("project"): if self.filters.get("project"):
projects = self.filters.get("project") projects = self.filters.get("project")
@@ -736,7 +732,7 @@ class FinancialQueryBuilder:
user_conditions = build_match_conditions(doctype) user_conditions = build_match_conditions(doctype)
if user_conditions: if user_conditions:
query = query.where(LiteralValue(user_conditions)) query = query.where(Bracket(LiteralValue(user_conditions)))
return query.run(as_dict=True) return query.run(as_dict=True)

View File

@@ -7,12 +7,14 @@ from frappe.utils import flt
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import ( from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
DependencyResolver, DependencyResolver,
FilterExpressionParser, FilterExpressionParser,
FinancialQueryBuilder,
FormulaCalculator, FormulaCalculator,
) )
from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import ( from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import (
FinancialReportTemplateTestCase, FinancialReportTemplateTestCase,
) )
from erpnext.accounts.utils import get_currency_precision from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_currency_precision, get_fiscal_year
# On IntegrationTestCase, the doctype test records and all # On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded # link-field test record dependencies are recursively loaded
@@ -1668,3 +1670,360 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
mock_row_invalid = self._create_mock_report_row(invalid_formula) mock_row_invalid = self._create_mock_report_row(invalid_formula)
condition = parser.build_condition(mock_row_invalid, account_table) condition = parser.build_condition(mock_row_invalid, account_table)
self.assertIsNone(condition) self.assertIsNone(condition)
class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
def test_fetch_balances_with_journal_entries(self):
company = "_Test Company"
cash_account = "_Test Cash - _TC"
bank_account = "_Test Bank - _TC"
# Create journal entries in different periods
# October: Transfer 1000 from Bank to Cash
jv_oct = make_journal_entry(
account1=cash_account,
account2=bank_account,
amount=1000,
posting_date="2024-10-15",
company=company,
submit=True,
)
# November: Transfer 500 from Bank to Cash
jv_nov = make_journal_entry(
account1=cash_account,
account2=bank_account,
amount=500,
posting_date="2024-11-20",
company=company,
submit=True,
)
# December: No transactions (test zero movement period)
try:
# Set up filters and periods for Q4 2024
filters = {
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-10-01",
"period_end_date": "2024-12-31",
"filter_based_on": "Date Range",
"periodicity": "Monthly",
}
periods = [
{"key": "2024_oct", "from_date": "2024-10-01", "to_date": "2024-10-31"},
{"key": "2024_nov", "from_date": "2024-11-01", "to_date": "2024-11-30"},
{"key": "2024_dec", "from_date": "2024-12-01", "to_date": "2024-12-31"},
]
query_builder = FinancialQueryBuilder(filters, periods)
# Create account objects as expected by fetch_account_balances
accounts = [
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
frappe._dict({"name": bank_account, "account_name": "Bank", "account_number": "1002"}),
]
# Fetch balances using the full workflow
balances_data = query_builder.fetch_account_balances(accounts)
# Verify Cash account balances
cash_data = balances_data.get(cash_account)
self.assertIsNotNone(cash_data, "Cash account should exist in results")
# October: movement = +1000 (debit)
oct_cash = cash_data.get_period("2024_oct")
self.assertIsNotNone(oct_cash, "October period should exist for cash")
self.assertEqual(oct_cash.movement, 1000.0, "October cash movement should be 1000")
# November: movement = +500
nov_cash = cash_data.get_period("2024_nov")
self.assertIsNotNone(nov_cash, "November period should exist for cash")
self.assertEqual(nov_cash.movement, 500.0, "November cash movement should be 500")
self.assertEqual(
nov_cash.opening, oct_cash.closing, "November opening should equal October closing"
)
# December: movement = 0 (no transactions)
dec_cash = cash_data.get_period("2024_dec")
self.assertIsNotNone(dec_cash, "December period should exist for cash")
self.assertEqual(dec_cash.movement, 0.0, "December cash movement should be 0")
self.assertEqual(
dec_cash.closing,
nov_cash.closing,
"December closing should equal November closing when no movement",
)
# Verify Bank account balances (opposite direction)
bank_data = balances_data.get(bank_account)
self.assertIsNotNone(bank_data, "Bank account should exist in results")
oct_bank = bank_data.get_period("2024_oct")
self.assertEqual(oct_bank.movement, -1000.0, "October bank movement should be -1000")
nov_bank = bank_data.get_period("2024_nov")
self.assertEqual(nov_bank.movement, -500.0, "November bank movement should be -500")
finally:
# Clean up: cancel journal entries
jv_nov.cancel()
jv_oct.cancel()
def test_opening_balance_from_previous_period_closing(self):
company = "_Test Company"
cash_account = "_Test Cash - _TC"
sales_account = "Sales - _TC"
posting_date_2023 = "2023-06-15"
# Create journal entry in prior period (2023)
# Cash Dr 5000, Sales Cr 5000
jv_2023 = make_journal_entry(
account1=cash_account,
account2=sales_account,
amount=5000,
posting_date=posting_date_2023,
company=company,
submit=True,
)
pcv = None
jv_2024 = None
original_pcv_setting = frappe.db.get_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv"
)
try:
# Create Period Closing Voucher for 2023
# This will create Account Closing Balance entries
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(posting_date_2023, 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()
# Now create a small transaction in 2024 to ensure the account appears
jv_2024 = make_journal_entry(
account1=cash_account,
account2=sales_account,
amount=100,
posting_date="2024-01-15",
company=company,
submit=True,
)
# Set up filters for Q1 2024 (after the period closing)
filters = {
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-01-01",
"period_end_date": "2024-03-31",
"filter_based_on": "Date Range",
"periodicity": "Monthly",
"ignore_closing_entries": True, # Don't include PCV entries in movements
}
periods = [
{"key": "2024_jan", "from_date": "2024-01-01", "to_date": "2024-01-31"},
{"key": "2024_feb", "from_date": "2024-02-01", "to_date": "2024-02-29"},
{"key": "2024_mar", "from_date": "2024-03-01", "to_date": "2024-03-31"},
]
query_builder = FinancialQueryBuilder(filters, periods)
accounts = [
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
]
balances_data = query_builder.fetch_account_balances(accounts)
# Verify Cash account has opening balance from 2023 transactions
cash_data = balances_data.get(cash_account)
self.assertIsNotNone(cash_data, "Cash account should exist in results")
jan_cash = cash_data.get_period("2024_jan")
self.assertIsNotNone(jan_cash, "January period should exist")
# Opening balance should be from prior period
# Cash had 5000 debit in 2023, so opening in 2024 should be >= 5000
# (may be higher if there were other test transactions)
self.assertEqual(
jan_cash.opening,
5000.0,
"January opening should equal to balance from 2023 (5000)",
)
# Verify running balance logic
# Movement in January is 100 (from jv_2024)
self.assertEqual(jan_cash.movement, 100.0, "January movement should be 100")
self.assertEqual(
jan_cash.closing, jan_cash.opening + jan_cash.movement, "Closing = Opening + Movement"
)
# February and March should have no movement but carry the balance
feb_cash = cash_data.get_period("2024_feb")
self.assertEqual(feb_cash.opening, jan_cash.closing, "Feb opening = Jan closing")
self.assertEqual(feb_cash.movement, 0.0, "February should have no movement")
self.assertEqual(feb_cash.closing, feb_cash.opening, "Feb closing = opening when no movement")
mar_cash = cash_data.get_period("2024_mar")
self.assertEqual(mar_cash.opening, feb_cash.closing, "Mar opening = Feb closing")
self.assertEqual(mar_cash.movement, 0.0, "March should have no movement")
self.assertEqual(mar_cash.closing, mar_cash.opening, "Mar closing = opening when no movement")
# Set up filters for Q2 2024
filters_q2 = {
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-04-01",
"period_end_date": "2024-06-30",
"filter_based_on": "Date Range",
"periodicity": "Monthly",
"ignore_closing_entries": True,
}
periods_q2 = [
{"key": "2024_apr", "from_date": "2024-04-01", "to_date": "2024-04-30"},
{"key": "2024_may", "from_date": "2024-05-01", "to_date": "2024-05-31"},
{"key": "2024_jun", "from_date": "2024-06-01", "to_date": "2024-06-30"},
]
query_builder_q2 = FinancialQueryBuilder(filters_q2, periods_q2)
balances_data_q2 = query_builder_q2.fetch_account_balances(accounts)
# Verify Cash account in Q2
cash_data_q2 = balances_data_q2.get(cash_account)
self.assertIsNotNone(cash_data_q2, "Cash account should exist in Q2 results")
apr_cash = cash_data_q2.get_period("2024_apr")
self.assertIsNotNone(apr_cash, "April period should exist")
# Opening balance in April should equal closing in March
self.assertEqual(
apr_cash.opening,
mar_cash.closing,
"April opening should equal March closing balance",
)
self.assertEqual(apr_cash.closing, apr_cash.opening, "April closing = opening when no movement")
finally:
# Clean up
frappe.db.set_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
)
if jv_2024:
jv_2024.cancel()
if pcv:
pcv.reload()
if pcv.docstatus == 1:
pcv.cancel()
jv_2023.cancel()
def test_account_with_gl_entries_but_no_prior_closing_balance(self):
company = "_Test Company"
cash_account = "_Test Cash - _TC"
bank_account = "_Test Bank - _TC"
# Create journal entries WITHOUT any prior Period Closing Voucher
# This ensures the account exists in gl_dict but NOT in balances_data
jv = make_journal_entry(
account1=cash_account,
account2=bank_account,
amount=2500,
posting_date="2024-07-15",
company=company,
submit=True,
)
try:
# Set up filters - use a period with no prior PCV
filters = {
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-07-01",
"period_end_date": "2024-09-30",
"filter_based_on": "Date Range",
"periodicity": "Monthly",
}
periods = [
{"key": "2024_jul", "from_date": "2024-07-01", "to_date": "2024-07-31"},
{"key": "2024_aug", "from_date": "2024-08-01", "to_date": "2024-08-31"},
{"key": "2024_sep", "from_date": "2024-09-01", "to_date": "2024-09-30"},
]
query_builder = FinancialQueryBuilder(filters, periods)
# Use accounts that have GL entries but may not have Account Closing Balance
accounts = [
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
frappe._dict({"name": bank_account, "account_name": "Bank", "account_number": "1002"}),
]
balances_data = query_builder.fetch_account_balances(accounts)
# Verify accounts are present in results even without prior closing balance
cash_data = balances_data.get(cash_account)
self.assertIsNotNone(cash_data, "Cash account should exist in results")
bank_data = balances_data.get(bank_account)
self.assertIsNotNone(bank_data, "Bank account should exist in results")
# Verify July has the movement from journal entry
jul_cash = cash_data.get_period("2024_jul")
self.assertIsNotNone(jul_cash, "July period should exist for cash")
self.assertEqual(jul_cash.movement, 2500.0, "July cash movement should be 2500")
jul_bank = bank_data.get_period("2024_jul")
self.assertIsNotNone(jul_bank, "July period should exist for bank")
self.assertEqual(jul_bank.movement, -2500.0, "July bank movement should be -2500")
# Verify subsequent periods exist with zero movement
aug_cash = cash_data.get_period("2024_aug")
self.assertIsNotNone(aug_cash, "August period should exist for cash")
self.assertEqual(aug_cash.movement, 0.0, "August cash movement should be 0")
self.assertEqual(aug_cash.opening, jul_cash.closing, "August opening = July closing")
sep_cash = cash_data.get_period("2024_sep")
self.assertIsNotNone(sep_cash, "September period should exist for cash")
self.assertEqual(sep_cash.movement, 0.0, "September cash movement should be 0")
self.assertEqual(sep_cash.opening, aug_cash.closing, "September opening = August closing")
finally:
jv.cancel()

View File

@@ -277,7 +277,21 @@ frappe.ui.form.on("Journal Entry", {
var update_jv_details = function (doc, r) { var update_jv_details = function (doc, r) {
$.each(r, function (i, d) { $.each(r, function (i, d) {
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts"); var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
frappe.model.set_value(row.doctype, row.name, "account", d.account); const {
idx,
name,
owner,
parent,
parenttype,
parentfield,
creation,
modified,
modified_by,
doctype,
docstatus,
...fields
} = d;
frappe.model.set_value(row.doctype, row.name, fields);
}); });
refresh_field("accounts"); refresh_field("accounts");
}; };

View File

@@ -9,6 +9,7 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"entry_type_and_date", "entry_type_and_date",
"company",
"is_system_generated", "is_system_generated",
"title", "title",
"voucher_type", "voucher_type",
@@ -17,7 +18,6 @@
"reversal_of", "reversal_of",
"column_break1", "column_break1",
"from_template", "from_template",
"company",
"posting_date", "posting_date",
"finance_book", "finance_book",
"apply_tds", "apply_tds",
@@ -638,7 +638,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2025-11-13 17:54:14.542903", "modified": "2026-02-03 14:40:39.944524",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@@ -74,8 +74,8 @@ class JournalEntry(AccountsController):
mode_of_payment: DF.Link | None mode_of_payment: DF.Link | None
multi_currency: DF.Check multi_currency: DF.Check
naming_series: DF.Literal["ACC-JV-.YYYY.-"] naming_series: DF.Literal["ACC-JV-.YYYY.-"]
party_not_required: DF.Check
override_tax_withholding_entries: DF.Check override_tax_withholding_entries: DF.Check
party_not_required: DF.Check
pay_to_recd_from: DF.Data | None pay_to_recd_from: DF.Data | None
payment_order: DF.Link | None payment_order: DF.Link | None
periodic_entry_difference_account: DF.Link | None periodic_entry_difference_account: DF.Link | None
@@ -179,7 +179,7 @@ class JournalEntry(AccountsController):
validate_docs_for_deferred_accounting([self.name], []) validate_docs_for_deferred_accounting([self.name], [])
def submit(self): def submit(self):
if len(self.accounts) > 100: if len(self.accounts) > 100 and not self.meta.queue_in_background:
queue_submission(self, "_submit") queue_submission(self, "_submit")
else: else:
return self._submit() return self._submit()
@@ -1691,6 +1691,10 @@ def get_exchange_rate(
credit=None, credit=None,
exchange_rate=None, exchange_rate=None,
): ):
# Ensure exchange_rate is always numeric to avoid calculation errors
if isinstance(exchange_rate, str):
exchange_rate = flt(exchange_rate) or 1
account_details = frappe.get_cached_value( account_details = frappe.get_cached_value(
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1 "Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
) )

View File

@@ -3,6 +3,7 @@
frappe.ui.form.on("Journal Entry Template", { frappe.ui.form.on("Journal Entry Template", {
onload: function (frm) { onload: function (frm) {
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
if (frm.is_new()) { if (frm.is_new()) {
frappe.call({ frappe.call({
type: "GET", type: "GET",
@@ -37,6 +38,31 @@ frappe.ui.form.on("Journal Entry Template", {
return { filters: filters }; return { filters: filters };
}); });
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
let filters = {
company: doc.company,
};
if (row.party_type == "Customer") {
filters.customer = row.party;
}
return {
query: "erpnext.controllers.queries.get_project_name",
filters,
};
});
frm.set_query("party_type", "accounts", function (doc, cdt, cdn) {
const row = locals[cdt][cdn];
return {
query: "erpnext.setup.doctype.party_type.party_type.get_party_type",
filters: {
account: row.account,
},
};
});
}, },
voucher_type: function (frm) { voucher_type: function (frm) {
var add_accounts = function (doc, r) { var add_accounts = function (doc, r) {

View File

@@ -3,6 +3,7 @@
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
@@ -42,7 +43,29 @@ class JournalEntryTemplate(Document):
] ]
# end: auto-generated types # end: auto-generated types
pass def validate(self):
self.validate_party()
def validate_party(self):
"""
Loop over all accounts and see if party and party type is set correctly
"""
for account in self.accounts:
if account.party_type:
account_type = frappe.get_cached_value("Account", account.account, "account_type")
if account_type not in ["Receivable", "Payable"]:
frappe.throw(
_(
"Check row {0} for account {1}: Party Type is only allowed for Receivable or Payable accounts"
).format(account.idx, account.account)
)
if account.party and not account.party_type:
frappe.throw(
_("Check row {0} for account {1}: Party is only allowed if Party Type is set").format(
account.idx, account.account
)
)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -5,7 +5,13 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"account" "account",
"party_type",
"party",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project"
], ],
"fields": [ "fields": [
{ {
@@ -15,18 +21,55 @@
"label": "Account", "label": "Account",
"options": "Account", "options": "Account",
"reqd": 1 "reqd": 1
},
{
"fieldname": "party_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Party Type",
"options": "DocType"
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Party",
"options": "party_type"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:09:58.986448", "modified": "2026-01-09 13:16:27.615083",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Template Account", "name": "Journal Entry Template Account",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -16,9 +16,13 @@ class JournalEntryTemplateAccount(Document):
from frappe.types import DF from frappe.types import DF
account: DF.Link account: DF.Link
cost_center: DF.Link | None
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
party: DF.DynamicLink | None
party_type: DF.Link | None
project: DF.Link | None
# end: auto-generated types # end: auto-generated types
pass pass

View File

@@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", {
); );
frm.refresh_fields(); frm.refresh_fields();
const party_currency =
frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
var reference_grid = frm.fields_dict["references"].grid;
["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
reference_grid.update_docfield_property(fieldname, "options", party_currency);
});
reference_grid.refresh();
}, },
show_general_ledger: function (frm) { show_general_ledger: function (frm) {
@@ -1108,7 +1118,7 @@ frappe.ui.form.on("Payment Entry", {
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", { await frm.call("allocate_amount_to_references", {
paid_amount: paid_amount, paid_amount: flt(paid_amount),
paid_amount_change: paid_amount_change, paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false, allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
}); });
@@ -1444,16 +1454,15 @@ frappe.ui.form.on("Payment Entry", {
callback: function (r) { callback: function (r) {
if (!r.exc && r.message) { if (!r.exc && r.message) {
// set taxes table // set taxes table
if (r.message) { let taxes = r.message;
for (let tax of r.message) { taxes.forEach((tax) => {
if (tax.charge_type === "On Net Total") { if (tax.charge_type === "On Net Total") {
tax.charge_type = "On Paid Amount"; tax.charge_type = "On Paid Amount";
}
frm.add_child("taxes", tax);
} }
frm.events.apply_taxes(frm); });
frm.events.set_unallocated_amount(frm); frm.set_value("taxes", taxes);
} frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
} }
}, },
}); });

View File

@@ -132,6 +132,12 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center" "options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}, },
{ {
"fieldname": "due_date", "fieldname": "due_date",

View File

@@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document):
amount_in_account_currency: DF.Currency amount_in_account_currency: DF.Currency
company: DF.Link | None company: DF.Link | None
cost_center: DF.Link | None cost_center: DF.Link | None
project: DF.Link | None
delinked: DF.Check delinked: DF.Check
due_date: DF.Date | None due_date: DF.Date | None
finance_book: DF.Link | None finance_book: DF.Link | None

View File

@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions: for x in self.dimensions:
dimension = x.fieldname dimension = x.fieldname
if self.get(dimension): if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension)) self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):

View File

@@ -535,7 +535,7 @@ class PaymentRequest(Document):
row_number += TO_SKIP_NEW_ROW row_number += TO_SKIP_NEW_ROW
@frappe.whitelist(allow_guest=True) @frappe.whitelist()
def make_payment_request(**args): def make_payment_request(**args):
"""Make payment request""" """Make payment request"""
@@ -546,6 +546,9 @@ def make_payment_request(**args):
if args.dn and not isinstance(args.dn, str): if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type str")) frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
frappe.has_permission("Payment Request", "create", throw=True)
frappe.has_permission(args.dt, "read", args.dn, throw=True)
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn) ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"): if not args.get("company"):
args.company = ref_doc.company args.company = ref_doc.company
@@ -819,7 +822,7 @@ def get_print_format_list(ref_doctype):
return {"print_format": print_format_list} return {"print_format": print_format_list}
@frappe.whitelist(allow_guest=True) @frappe.whitelist()
def resend_payment_email(docname): def resend_payment_email(docname):
return frappe.get_doc("Payment Request", docname).send_email() return frappe.get_doc("Payment Request", docname).send_email()

View File

@@ -159,15 +159,16 @@
"language", "language",
"column_break_84", "column_break_84",
"select_print_heading", "select_print_heading",
"utm_analytics_section",
"utm_source",
"utm_medium",
"column_break_bhao",
"utm_campaign",
"more_information", "more_information",
"inter_company_invoice_reference", "inter_company_invoice_reference",
"customer_group", "customer_group",
"is_discounted", "is_discounted",
"col_break23", "col_break23",
"utm_source",
"utm_campaign",
"utm_medium",
"column_break_gpiw",
"status", "status",
"more_info", "more_info",
"debit_to", "debit_to",
@@ -1541,10 +1542,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note" "label": "Update Billed Amount in Delivery Note"
}, },
{
"fieldname": "column_break_gpiw",
"fieldtype": "Column Break"
},
{ {
"fieldname": "utm_medium", "fieldname": "utm_medium",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1610,13 +1607,24 @@
"hidden": 1, "hidden": 1,
"label": "Item Wise Tax Details", "label": "Item Wise Tax Details",
"no_copy": 1, "no_copy": 1,
"options": "Item Wise Tax Detail" "options": "Item Wise Tax Detail",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "UTM Analytics"
},
{
"fieldname": "column_break_bhao",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-08-04 22:22:31.471752", "modified": "2026-02-10 14:23:07.181782",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -897,6 +897,53 @@ class TestPOSInvoice(IntegrationTestCase):
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC": if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
self.assertEqual(batch.qty, 5) self.assertEqual(batch.qty, 5)
def test_pos_batch_reservation_with_return_qty(self):
"""
Test POS Invoice reserved qty for batch without bundle with return invoices.
"""
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_batch_item_with_batch,
)
create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
se = make_stock_entry(
target="_Test Warehouse - _TC",
item_code="_Batch Item Reserve Return",
qty=30,
basic_rate=100,
)
se.reload()
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
# POS Invoice for the batch without bundle
pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
pos_inv.append(
"payments",
{"mode_of_payment": "Cash", "amount": 4500},
)
pos_inv.items[0].batch_no = batch_no
pos_inv.save()
pos_inv.submit()
# POS Invoice return
pos_return = make_sales_return(pos_inv.name)
pos_return.insert()
pos_return.submit()
batches = get_auto_batch_nos(
frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
)
for batch in batches:
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
self.assertEqual(batch.qty, 30)
def test_pos_batch_item_qty_validation(self): def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
BatchNegativeStockError, BatchNegativeStockError,

View File

@@ -12,56 +12,78 @@
"disabled", "disabled",
"column_break_9", "column_break_9",
"warehouse", "warehouse",
"utm_source",
"utm_campaign",
"utm_medium",
"company_address", "company_address",
"section_break_15", "accounting_tab",
"applicable_for_users",
"section_break_11", "section_break_11",
"payments", "payments",
"set_grand_total_to_default_mop",
"price_list_and_currency_section",
"currency",
"column_break_bptt",
"selling_price_list",
"write_off_section",
"write_off_account",
"column_break_ukpz",
"write_off_cost_center",
"column_break_pkca",
"write_off_limit",
"income_and_expense_account",
"income_account",
"column_break_byzk",
"expense_account",
"taxes_section",
"taxes_and_charges",
"column_break_cjpp",
"tax_category",
"section_break_19",
"account_for_change_amount",
"disable_rounded_total",
"column_break_23",
"apply_discount_on",
"allow_partial_payment",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"pos_configurations_tab",
"section_break_14", "section_break_14",
"hide_images",
"hide_unavailable_items",
"auto_add_item_to_cart",
"validate_stock_on_save",
"print_receipt_on_order_complete",
"action_on_new_invoice", "action_on_new_invoice",
"validate_stock_on_save",
"column_break_16", "column_break_16",
"update_stock", "update_stock",
"ignore_pricing_rule", "ignore_pricing_rule",
"print_receipt_on_order_complete",
"pos_item_selector_section",
"hide_images",
"column_break_rpny",
"hide_unavailable_items",
"column_break_stcl",
"auto_add_item_to_cart",
"pos_item_details_section",
"allow_rate_change", "allow_rate_change",
"column_break_hwfg",
"allow_discount_change", "allow_discount_change",
"set_grand_total_to_default_mop", "column_break_egpi",
"allow_partial_payment", "allow_warehouse_change",
"section_break_15",
"applicable_for_users",
"section_break_23", "section_break_23",
"item_groups", "item_groups",
"column_break_25", "column_break_25",
"customer_groups", "customer_groups",
"more_info_tab",
"section_break_16", "section_break_16",
"print_format", "print_format",
"letter_head", "letter_head",
"column_break0", "column_break0",
"tc_name", "tc_name",
"select_print_heading", "select_print_heading",
"section_break_19", "utm_analytics_section",
"selling_price_list", "utm_source",
"currency", "column_break_tvls",
"write_off_account", "utm_campaign",
"write_off_cost_center", "column_break_xygw",
"write_off_limit", "utm_medium"
"account_for_change_amount",
"disable_rounded_total",
"column_break_23",
"income_account",
"expense_account",
"taxes_and_charges",
"tax_category",
"apply_discount_on",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project"
], ],
"fields": [ "fields": [
{ {
@@ -130,8 +152,7 @@
}, },
{ {
"fieldname": "section_break_14", "fieldname": "section_break_14",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Configuration"
}, },
{ {
"description": "Only show Items from these Item Groups", "description": "Only show Items from these Item Groups",
@@ -152,6 +173,7 @@
"options": "POS Customer Group" "options": "POS Customer Group"
}, },
{ {
"collapsible": 1,
"fieldname": "section_break_16", "fieldname": "section_break_16",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Print Settings" "label": "Print Settings"
@@ -191,7 +213,7 @@
{ {
"fieldname": "section_break_19", "fieldname": "section_break_19",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting" "label": "Miscellaneous"
}, },
{ {
"fieldname": "selling_price_list", "fieldname": "selling_price_list",
@@ -427,9 +449,111 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Applicable only on Transactions made using POS",
"fieldname": "allow_partial_payment", "fieldname": "allow_partial_payment",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Partial Payment" "label": "Allow Partial Payment"
},
{
"fieldname": "column_break_tvls",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_xygw",
"fieldtype": "Column Break"
},
{
"fieldname": "accounting_tab",
"fieldtype": "Tab Break",
"label": "Accounting"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "pos_configurations_tab",
"fieldtype": "Tab Break",
"label": "POS Configurations"
},
{
"fieldname": "price_list_and_currency_section",
"fieldtype": "Section Break",
"label": "Price List & Currency"
},
{
"fieldname": "column_break_bptt",
"fieldtype": "Column Break"
},
{
"fieldname": "write_off_section",
"fieldtype": "Section Break",
"label": "Write Off"
},
{
"fieldname": "column_break_ukpz",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_pkca",
"fieldtype": "Column Break"
},
{
"fieldname": "income_and_expense_account",
"fieldtype": "Section Break",
"label": "Income and Expense"
},
{
"fieldname": "column_break_byzk",
"fieldtype": "Column Break"
},
{
"fieldname": "taxes_section",
"fieldtype": "Section Break",
"label": "Taxes"
},
{
"fieldname": "column_break_cjpp",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_item_selector_section",
"fieldtype": "Section Break",
"label": "POS Item Selector"
},
{
"fieldname": "column_break_rpny",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_stcl",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_item_details_section",
"fieldtype": "Section Break",
"label": "POS Item Details"
},
{
"fieldname": "column_break_hwfg",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_egpi",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "allow_warehouse_change",
"fieldtype": "Check",
"label": "Allow User to Edit Warehouse"
},
{
"collapsible": 1,
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "Campaign"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -458,7 +582,7 @@
"link_fieldname": "pos_profile" "link_fieldname": "pos_profile"
} }
], ],
"modified": "2025-06-24 11:19:19.834905", "modified": "2026-02-10 14:24:48.597412",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@@ -34,6 +34,7 @@ class POSProfile(Document):
allow_discount_change: DF.Check allow_discount_change: DF.Check
allow_partial_payment: DF.Check allow_partial_payment: DF.Check
allow_rate_change: DF.Check allow_rate_change: DF.Check
allow_warehouse_change: DF.Check
applicable_for_users: DF.Table[POSProfileUser] applicable_for_users: DF.Table[POSProfileUser]
apply_discount_on: DF.Literal["Grand Total", "Net Total"] apply_discount_on: DF.Literal["Grand Total", "Net Total"]
auto_add_item_to_cart: DF.Check auto_add_item_to_cart: DF.Check

View File

@@ -98,8 +98,7 @@ def get_customers_list(pos_profile=None):
return ( return (
frappe.db.sql( frappe.db.sql(
f""" select name, customer_name, customer_group, f""" select name, customer_name, customer_group, territory from tabCustomer where disabled = 0
territory, customer_pos_id from tabCustomer where disabled = 0
and {cond}""", and {cond}""",
tuple(customer_groups), tuple(customer_groups),
as_dict=1, as_dict=1,

View File

@@ -121,7 +121,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Apply On", "label": "Apply On",
"options": "\nItem Code\nItem Group\nBrand\nTransaction", "options": "Item Code\nItem Group\nBrand\nTransaction",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -657,7 +657,7 @@
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2025-08-20 11:40:07.096854", "modified": "2026-02-17 12:24:07.553505",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",
@@ -714,9 +714,10 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"title_field": "title" "title_field": "title"
} }

View File

@@ -45,7 +45,7 @@ class PricingRule(Document):
apply_discount_on: DF.Literal["Grand Total", "Net Total"] apply_discount_on: DF.Literal["Grand Total", "Net Total"]
apply_discount_on_rate: DF.Check apply_discount_on_rate: DF.Check
apply_multiple_pricing_rules: DF.Check apply_multiple_pricing_rules: DF.Check
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"] apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
apply_recursion_over: DF.Float apply_recursion_over: DF.Float
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"] apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
brands: DF.Table[PricingRuleBrand] brands: DF.Table[PricingRuleBrand]

View File

@@ -10,7 +10,7 @@
], ],
"fields": [ "fields": [
{ {
"depends_on": "eval:parent.apply_on == 'Item Code'", "depends_on": "eval:parent.apply_on == 'Brand'",
"fieldname": "brand", "fieldname": "brand",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@@ -28,14 +28,15 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:10:17.857046", "modified": "2026-02-17 12:17:13.073587",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule Brand", "name": "Pricing Rule Brand",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -10,7 +10,7 @@
], ],
"fields": [ "fields": [
{ {
"depends_on": "eval:parent.apply_on == 'Item Code'", "depends_on": "eval:parent.apply_on == 'Item Group'",
"fieldname": "item_group", "fieldname": "item_group",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@@ -28,14 +28,15 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:10:18.221095", "modified": "2026-02-17 12:16:57.778471",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule Item Group", "name": "Pricing Rule Item Group",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -415,8 +415,9 @@ def reconcile(doc: None | str = None) -> None:
for x in allocations: for x in allocations:
pr.append("allocation", x) pr.append("allocation", x)
skip_ref_details_update_for_pe = check_multi_currency(pr)
# reconcile # reconcile
pr.reconcile_allocations(skip_ref_details_update_for_pe=True) pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
# If Payment Entry, update details only for newly linked references # If Payment Entry, update details only for newly linked references
# This is for performance # This is for performance
@@ -504,6 +505,37 @@ def reconcile(doc: None | str = None) -> None:
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed") frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
def check_multi_currency(pr_doc):
GL = frappe.qb.DocType("GL Entry")
Account = frappe.qb.DocType("Account")
def get_account_currency(voucher_type, voucher_no):
currency = (
frappe.qb.from_(GL)
.join(Account)
.on(GL.account == Account.name)
.select(Account.account_currency)
.where(
(GL.voucher_type == voucher_type)
& (GL.voucher_no == voucher_no)
& (Account.account_type.isin(["Payable", "Receivable"]))
)
.limit(1)
).run(as_dict=True)
return currency[0].account_currency if currency else None
for allocation in pr_doc.allocation:
reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
if reference_currency != invoice_currency:
return True
return False
@frappe.whitelist() @frappe.whitelist()
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None: def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
running_doc = None running_doc = None

View File

@@ -534,7 +534,7 @@ cur_frm.fields_dict["select_print_heading"].get_query = function (doc, cdt, cdn)
cur_frm.set_query("wip_composite_asset", "items", function () { cur_frm.set_query("wip_composite_asset", "items", function () {
return { return {
filters: { is_composite_asset: 1, docstatus: 0 }, filters: { asset_type: "Composite Asset", docstatus: 0 },
}; };
}); });

View File

@@ -85,20 +85,24 @@
"taxes_and_charges_added", "taxes_and_charges_added",
"taxes_and_charges_deducted", "taxes_and_charges_deducted",
"total_taxes_and_charges", "total_taxes_and_charges",
"section_break_49", "totals_section",
"grand_total",
"disable_rounded_total",
"rounding_adjustment",
"column_break8",
"use_company_roundoff_cost_center",
"in_words",
"rounded_total",
"base_totals_section",
"base_grand_total", "base_grand_total",
"base_rounding_adjustment", "base_rounding_adjustment",
"base_rounded_total", "column_break_hcca",
"base_in_words", "base_in_words",
"column_break8", "base_rounded_total",
"grand_total", "section_break_ttrv",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance", "total_advance",
"column_break_peap",
"outstanding_amount", "outstanding_amount",
"disable_rounded_total",
"section_tax_withholding_entry", "section_tax_withholding_entry",
"tax_withholding_group", "tax_withholding_group",
"ignore_tax_withholding_threshold", "ignore_tax_withholding_threshold",
@@ -606,6 +610,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:doc.items.every((item) => !item.pr_detail)",
"fieldname": "update_stock", "fieldname": "update_stock",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Update Stock", "label": "Update Stock",
@@ -882,15 +887,10 @@
"options": "currency", "options": "currency",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "section_break_49",
"fieldtype": "Section Break",
"label": "Totals"
},
{ {
"fieldname": "base_grand_total", "fieldname": "base_grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Grand Total (Company Currency)", "label": "Grand Total",
"oldfieldname": "grand_total", "oldfieldname": "grand_total",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
@@ -901,7 +901,7 @@
"depends_on": "eval:!doc.disable_rounded_total", "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)", "label": "Rounding Adjustment",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
@@ -911,7 +911,7 @@
"depends_on": "eval:!doc.disable_rounded_total", "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounded_total", "fieldname": "base_rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounded Total (Company Currency)", "label": "Rounded Total",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
@@ -920,7 +920,7 @@
{ {
"fieldname": "base_in_words", "fieldname": "base_in_words",
"fieldtype": "Data", "fieldtype": "Data",
"label": "In Words (Company Currency)", "label": "In Words",
"length": 240, "length": 240,
"oldfieldname": "in_words", "oldfieldname": "in_words",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@@ -1660,6 +1660,28 @@
"fieldname": "override_tax_withholding_entries", "fieldname": "override_tax_withholding_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Edit Tax Withholding Entries" "label": "Edit Tax Withholding Entries"
},
{
"fieldname": "column_break_hcca",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ttrv",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_peap",
"fieldtype": "Column Break"
},
{
"fieldname": "base_totals_section",
"fieldtype": "Section Break",
"label": "Totals (Company Currency)"
},
{
"fieldname": "totals_section",
"fieldtype": "Section Break",
"label": "Totals"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -1667,7 +1689,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-12-15 06:41:38.237728", "modified": "2026-02-05 20:45:16.964500",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@@ -36,7 +36,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
from erpnext.controllers.buying_controller import BuyingController from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
update_billed_amount_based_on_po, update_billed_amount_based_on_po,
@@ -2005,9 +2005,17 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
args = json.loads(args) args = json.loads(args)
def post_parent_process(source_parent, target_parent): def post_parent_process(source_parent, target_parent):
for row in target_parent.get("items"): remove_items_with_zero_qty(target_parent)
if row.get("qty") == 0: set_missing_values(source_parent, target_parent)
target_parent.remove(row)
def remove_items_with_zero_qty(target_parent):
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
def set_missing_values(source_parent, target_parent):
target_parent.run_method("set_missing_values")
if args and args.get("merge_taxes"):
merge_taxes(source_parent, target_parent)
target_parent.run_method("calculate_taxes_and_totals")
def update_item(obj, target, source_parent): def update_item(obj, target, source_parent):
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
@@ -2059,7 +2067,11 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc), "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
}, },
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"}, "Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
"reset_value": not (args and args.get("merge_taxes")),
"ignore": args.get("merge_taxes") if args else 0,
},
}, },
target_doc, target_doc,
post_parent_process, post_parent_process,

View File

@@ -52,6 +52,7 @@
"stock_uom_rate", "stock_uom_rate",
"is_free_item", "is_free_item",
"apply_tds", "apply_tds",
"allow_zero_valuation_rate",
"section_break_22", "section_break_22",
"net_rate", "net_rate",
"net_amount", "net_amount",
@@ -97,7 +98,6 @@
"service_start_date", "service_start_date",
"service_end_date", "service_end_date",
"reference", "reference",
"allow_zero_valuation_rate",
"item_tax_rate", "item_tax_rate",
"bom", "bom",
"include_exploded_items", "include_exploded_items",
@@ -420,6 +420,7 @@
"options": "UOM" "options": "UOM"
}, },
{ {
"depends_on": "eval:parent.update_stock",
"fieldname": "warehouse_section", "fieldname": "warehouse_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Warehouse" "label": "Warehouse"
@@ -447,7 +448,6 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
@@ -459,14 +459,12 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Serial No", "label": "Serial No",
"no_copy": 1 "no_copy": 1
}, },
{ {
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "rejected_serial_no", "fieldname": "rejected_serial_no",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Rejected Serial No", "label": "Rejected Serial No",
@@ -577,6 +575,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:parent.update_stock",
"fieldname": "allow_zero_valuation_rate", "fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Zero Valuation Rate", "label": "Allow Zero Valuation Rate",
@@ -800,7 +799,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.is_internal_supplier && parent.update_stock", "depends_on": "eval:parent.is_internal_supplier",
"fieldname": "from_warehouse", "fieldname": "from_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
@@ -896,7 +895,7 @@
"label": "Consider for Tax Withholding" "label": "Consider for Tax Withholding"
}, },
{ {
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle", "fieldname": "serial_and_batch_bundle",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Serial and Batch Bundle", "label": "Serial and Batch Bundle",
@@ -906,7 +905,7 @@
"search_index": 1 "search_index": 1
}, },
{ {
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "rejected_serial_and_batch_bundle", "fieldname": "rejected_serial_and_batch_bundle",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Rejected Serial and Batch Bundle", "label": "Rejected Serial and Batch Bundle",
@@ -922,7 +921,7 @@
"options": "Asset" "options": "Asset"
}, },
{ {
"depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
"fieldname": "add_serial_batch_bundle", "fieldname": "add_serial_batch_bundle",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Add Serial / Batch No" "label": "Add Serial / Batch No"
@@ -992,7 +991,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-12-13 14:10:02.379392", "modified": "2026-02-15 21:07:49.455930",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -44,6 +44,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
"Unreconcile Payment Entries", "Unreconcile Payment Entries",
"Serial and Batch Bundle", "Serial and Batch Bundle",
"Bank Transaction", "Bank Transaction",
"Packing Slip",
]; ];
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
@@ -115,18 +116,21 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
} }
if (cint(doc.update_stock) != 1) { if (cint(doc.update_stock) != 1) {
// show Make Delivery Note button only if Sales Invoice is not created from Delivery Note if (!is_delivered_by_supplier) {
var from_delivery_note = false; const should_create_delivery_note = doc.items.some(
from_delivery_note = this.frm.doc.items.some(function (item) { (item) =>
return item.delivery_note ? true : false; item.qty - item.delivered_qty > 0 &&
}); !item.scio_detail &&
!item.dn_detail &&
if (!from_delivery_note && !is_delivered_by_supplier) { !item.delivered_by_supplier
this.frm.add_custom_button(
__("Delivery"),
this.frm.cscript["Make Delivery Note"],
__("Create")
); );
if (should_create_delivery_note) {
this.frm.add_custom_button(
__("Delivery Note"),
this.frm.cscript["Make Delivery Note"],
__("Create")
);
}
} }
} }

View File

@@ -7,12 +7,12 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"customer_section", "customer_section",
"company",
"company_tax_id",
"naming_series", "naming_series",
"customer", "customer",
"customer_name", "customer_name",
"tax_id", "tax_id",
"company",
"company_tax_id",
"column_break1", "column_break1",
"posting_date", "posting_date",
"posting_time", "posting_time",
@@ -77,34 +77,36 @@
"base_total_taxes_and_charges", "base_total_taxes_and_charges",
"column_break_47", "column_break_47",
"total_taxes_and_charges", "total_taxes_and_charges",
"totals", "totals_section",
"base_grand_total",
"base_rounding_adjustment",
"base_rounded_total",
"base_in_words",
"column_break5",
"grand_total", "grand_total",
"rounding_adjustment", "rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words", "in_words",
"column_break5",
"rounded_total",
"disable_rounded_total",
"total_advance", "total_advance",
"outstanding_amount", "outstanding_amount",
"disable_rounded_total", "use_company_roundoff_cost_center",
"base_totals_section",
"base_grand_total",
"base_rounding_adjustment",
"base_in_words",
"column_break_xjag",
"base_rounded_total",
"section_tax_withholding_entry", "section_tax_withholding_entry",
"tax_withholding_group", "tax_withholding_group",
"ignore_tax_withholding_threshold", "ignore_tax_withholding_threshold",
"override_tax_withholding_entries", "override_tax_withholding_entries",
"tax_withholding_entries", "tax_withholding_entries",
"section_break_49", "additional_discount_section",
"apply_discount_on", "apply_discount_on",
"base_discount_amount", "base_discount_amount",
"coupon_code", "coupon_code",
"is_cash_or_non_trade_discount",
"additional_discount_account",
"column_break_51", "column_break_51",
"additional_discount_percentage", "additional_discount_percentage",
"discount_amount", "discount_amount",
"is_cash_or_non_trade_discount",
"additional_discount_account",
"sec_tax_breakup", "sec_tax_breakup",
"other_charges_calculation", "other_charges_calculation",
"item_wise_tax_details", "item_wise_tax_details",
@@ -194,13 +196,13 @@
"column_break8", "column_break8",
"unrealized_profit_loss_account", "unrealized_profit_loss_account",
"against_income_account", "against_income_account",
"sales_team_section_break", "commission_section",
"sales_partner", "sales_partner",
"amount_eligible_for_commission", "amount_eligible_for_commission",
"column_break10", "column_break10",
"commission_rate", "commission_rate",
"total_commission", "total_commission",
"section_break2", "sales_team_section",
"sales_team", "sales_team",
"edit_printing_settings", "edit_printing_settings",
"letter_head", "letter_head",
@@ -215,20 +217,21 @@
"column_break_140", "column_break_140",
"to_date", "to_date",
"update_auto_repeat_reference", "update_auto_repeat_reference",
"utm_analytics_section",
"utm_source",
"utm_medium",
"column_break_ixxw",
"utm_campaign",
"utm_content",
"more_information", "more_information",
"status", "status",
"inter_company_invoice_reference", "remarks",
"represents_company",
"customer_group", "customer_group",
"column_break_imbx", "column_break_imbx",
"utm_source",
"utm_campaign",
"utm_medium",
"utm_content",
"col_break23",
"is_internal_customer", "is_internal_customer",
"represents_company",
"inter_company_invoice_reference",
"is_discounted", "is_discounted",
"remarks",
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
@@ -703,6 +706,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
"fieldname": "update_stock", "fieldname": "update_stock",
"fieldtype": "Check", "fieldtype": "Check",
"hide_days": 1, "hide_days": 1,
@@ -793,7 +797,8 @@
"hide_seconds": 1, "hide_seconds": 1,
"label": "Time Sheets", "label": "Time Sheets",
"options": "Sales Invoice Timesheet", "options": "Sales Invoice Timesheet",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"default": "0", "default": "0",
@@ -1072,14 +1077,6 @@
"no_copy": 1, "no_copy": 1,
"options": "Cost Center" "options": "Cost Center"
}, },
{
"collapsible": 1,
"fieldname": "section_break_49",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Additional Discount"
},
{ {
"default": "Grand Total", "default": "Grand Total",
"fieldname": "apply_discount_on", "fieldname": "apply_discount_on",
@@ -1124,22 +1121,12 @@
"options": "currency", "options": "currency",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "totals",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Totals",
"oldfieldtype": "Section Break",
"options": "fa fa-money",
"print_hide": 1
},
{ {
"fieldname": "base_grand_total", "fieldname": "base_grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Grand Total (Company Currency)", "label": "Grand Total",
"oldfieldname": "grand_total", "oldfieldname": "grand_total",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
@@ -1153,9 +1140,8 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Rounding Adjustment (Company Currency)", "label": "Rounding Adjustment",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -1165,10 +1151,9 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Rounded Total (Company Currency)", "label": "Rounded Total",
"oldfieldname": "rounded_total", "oldfieldname": "rounded_total",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -1178,7 +1163,7 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "In Words (Company Currency)", "label": "In Words",
"length": 240, "length": 240,
"oldfieldname": "in_words", "oldfieldname": "in_words",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@@ -1271,7 +1256,6 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible": 1,
"collapsible_depends_on": "advances", "collapsible_depends_on": "advances",
"fieldname": "advances_section", "fieldname": "advances_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -1647,13 +1631,6 @@
"no_copy": 1, "no_copy": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "col_break23",
"fieldtype": "Column Break",
"hide_days": 1,
"hide_seconds": 1,
"width": "50%"
},
{ {
"default": "Draft", "default": "Draft",
"fieldname": "status", "fieldname": "status",
@@ -1705,10 +1682,10 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"default": "No", "default": "No",
"fieldname": "is_opening", "fieldname": "is_opening",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 1,
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Is Opening Entry", "label": "Is Opening Entry",
@@ -1737,18 +1714,6 @@
"oldfieldtype": "Text", "oldfieldtype": "Text",
"print_hide": 1 "print_hide": 1
}, },
{
"collapsible": 1,
"collapsible_depends_on": "sales_partner",
"fieldname": "sales_team_section_break",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Commission",
"oldfieldtype": "Section Break",
"options": "fa fa-group",
"print_hide": 1
},
{ {
"fieldname": "sales_partner", "fieldname": "sales_partner",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1792,16 +1757,6 @@
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"print_hide": 1 "print_hide": 1
}, },
{
"collapsible": 1,
"collapsible_depends_on": "sales_team",
"fieldname": "section_break2",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Team",
"print_hide": 1
},
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "sales_team", "fieldname": "sales_team",
@@ -2250,7 +2205,8 @@
"hidden": 1, "hidden": 1,
"label": "Item Wise Tax Details", "label": "Item Wise Tax Details",
"no_copy": 1, "no_copy": 1,
"options": "Item Wise Tax Detail" "options": "Item Wise Tax Detail",
"print_hide": 1
}, },
{ {
"default": "0", "default": "0",
@@ -2291,6 +2247,66 @@
"fieldname": "override_tax_withholding_entries", "fieldname": "override_tax_withholding_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Edit Tax Withholding Entries" "label": "Edit Tax Withholding Entries"
},
{
"fieldname": "totals_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Totals",
"oldfieldtype": "Section Break",
"options": "fa fa-money",
"print_hide": 1
},
{
"fieldname": "base_totals_section",
"fieldtype": "Section Break",
"label": "Totals (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fieldname": "column_break_xjag",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "additional_discount_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Additional Discount"
},
{
"collapsible": 1,
"collapsible_depends_on": "sales_team",
"fieldname": "sales_team_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Team",
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "sales_partner",
"fieldname": "commission_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Commission",
"oldfieldtype": "Section Break",
"options": "fa fa-group",
"print_hide": 1
},
{
"fieldname": "column_break_ixxw",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "UTM Analytics"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -2304,7 +2320,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2025-12-24 18:29:50.242618", "modified": "2026-02-10 11:59:07.819903",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -2470,7 +2470,10 @@ def make_delivery_note(source_name, target_doc=None):
"cost_center": "cost_center", "cost_center": "cost_center",
}, },
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail, "condition": lambda doc: doc.delivered_by_supplier != 1
and not doc.scio_detail
and not doc.dn_detail
and doc.qty - doc.delivered_qty > 0,
}, },
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": { "Sales Team": {

View File

@@ -4739,6 +4739,66 @@ class TestSalesInvoice(ERPNextTestSuite):
doc.db_set("do_not_use_batchwise_valuation", original_value) doc.db_set("do_not_use_batchwise_valuation", original_value)
@change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True})
def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self):
item_code = "_Test Item for Expiry Batch Zero Valuation"
make_item_for_si(
item_code,
{
"is_stock_item": 1,
"has_batch_no": 1,
"has_expiry_date": 1,
"shelf_life_in_days": 2,
"create_new_batch": 1,
"batch_number_series": "TBATCH-EBZV.####",
},
)
se = make_stock_entry(
item_code=item_code,
qty=10,
target="_Test Warehouse - _TC",
rate=100,
)
# fetch batch no from bundle
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
si = create_sales_invoice(
posting_date=add_days(nowdate(), 3),
item=item_code,
qty=-10,
rate=100,
is_return=1,
update_stock=1,
use_serial_batch_fields=1,
do_not_save=1,
do_not_submit=1,
)
si.items[0].batch_no = batch_no
si.save()
si.submit()
si.reload()
# check zero incoming rate in voucher
self.assertEqual(si.items[0].incoming_rate, 0.0)
# chekc zero incoming rate in stock ledger
stock_ledger_entry = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Sales Invoice",
"voucher_no": si.name,
"item_code": item_code,
"warehouse": "_Test Warehouse - _TC",
},
["incoming_rate", "valuation_rate"],
as_dict=True,
)
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
def make_item_for_si(item_code, properties=None): def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -52,6 +52,7 @@
"is_free_item", "is_free_item",
"apply_tds", "apply_tds",
"grant_commission", "grant_commission",
"allow_zero_valuation_rate",
"section_break_21", "section_break_21",
"net_rate", "net_rate",
"net_amount", "net_amount",
@@ -88,7 +89,6 @@
"serial_and_batch_bundle", "serial_and_batch_bundle",
"use_serial_batch_fields", "use_serial_batch_fields",
"col_break5", "col_break5",
"allow_zero_valuation_rate",
"incoming_rate", "incoming_rate",
"item_tax_rate", "item_tax_rate",
"actual_batch_qty", "actual_batch_qty",
@@ -580,6 +580,7 @@
{ {
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "eval:doc.serial_no || doc.batch_no", "collapsible_depends_on": "eval:doc.serial_no || doc.batch_no",
"depends_on": "eval:parent.update_stock",
"fieldname": "warehouse_and_reference", "fieldname": "warehouse_and_reference",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Stock Details" "label": "Stock Details"
@@ -595,7 +596,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"depends_on": "eval: parent.is_internal_customer && parent.update_stock", "depends_on": "eval: parent.is_internal_customer",
"fieldname": "target_warehouse", "fieldname": "target_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1, "hidden": 1,
@@ -613,7 +614,6 @@
"options": "Quality Inspection" "options": "Quality Inspection"
}, },
{ {
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
@@ -626,6 +626,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:parent.update_stock",
"fieldname": "allow_zero_valuation_rate", "fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Zero Valuation Rate", "label": "Allow Zero Valuation Rate",
@@ -633,7 +634,6 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Serial No", "label": "Serial No",
@@ -906,7 +906,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)", "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
"fieldname": "serial_and_batch_bundle", "fieldname": "serial_and_batch_bundle",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Serial and Batch Bundle", "label": "Serial and Batch Bundle",
@@ -916,7 +916,7 @@
"search_index": 1 "search_index": 1
}, },
{ {
"depends_on": "eval:parent.update_stock === 1", "depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
"fieldname": "pick_serial_and_batch", "fieldname": "pick_serial_and_batch",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Pick Serial / Batch No" "label": "Pick Serial / Batch No"
@@ -1009,7 +1009,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-09-04 11:08:25.583561", "modified": "2026-02-15 21:08:57.341638",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -26,7 +26,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:parent.doctype == 'Sales Invoice'", "depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)",
"fieldname": "amount", "fieldname": "amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
@@ -85,14 +85,15 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:10:36.427565", "modified": "2026-02-16 20:46:34.592604",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Payment", "name": "Sales Invoice Payment",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -43,16 +43,18 @@
"read_only": 1 "read_only": 1
} }
], ],
"grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:10:55.008837", "modified": "2025-11-14 16:17:25.584675",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Transaction Deletion Record Details", "name": "Transaction Deletion Record Details",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -17,7 +17,7 @@
</div> </div>
<div class="col-xs-6"> <div class="col-xs-6">
<table> <table>
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr> <tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.posting_date) }}</td></tr>
</table> </table>
</div> </div>
</div> </div>

View File

@@ -151,6 +151,8 @@ frappe.query_reports["Accounts Payable"] = {
fieldtype: "Check", fieldtype: "Check",
}, },
], ],
collapsible_filters: true,
separate_check_filters: true,
formatter: function (value, row, column, data, default_formatter) { formatter: function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);

View File

@@ -108,6 +108,8 @@ frappe.query_reports["Accounts Payable Summary"] = {
fieldtype: "Check", fieldtype: "Check",
}, },
], ],
collapsible_filters: true,
separate_check_filters: true,
onload: function (report) { onload: function (report) {
report.page.add_inner_button(__("Accounts Payable"), function () { report.page.add_inner_button(__("Accounts Payable"), function () {

View File

@@ -178,6 +178,8 @@ frappe.query_reports["Accounts Receivable"] = {
fieldtype: "Check", fieldtype: "Check",
}, },
], ],
collapsible_filters: true,
separate_check_filters: true,
formatter: function (value, row, column, data, default_formatter) { formatter: function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);

View File

@@ -131,6 +131,8 @@ frappe.query_reports["Accounts Receivable Summary"] = {
fieldtype: "Check", fieldtype: "Check",
}, },
], ],
collapsible_filters: true,
separate_check_filters: true,
onload: function (report) { onload: function (report) {
report.page.add_inner_button(__("Accounts Receivable"), function () { report.page.add_inner_button(__("Accounts Receivable"), function () {

View File

@@ -232,11 +232,11 @@ def get_report_summary(
def get_chart_data(filters, columns, asset, liability, equity, currency): def get_chart_data(filters, columns, asset, liability, equity, currency):
labels = [d.get("label") for d in columns[2:]] labels = [d.get("label") for d in columns[4:]]
asset_data, liability_data, equity_data = [], [], [] asset_data, liability_data, equity_data = [], [], []
for p in columns[2:]: for p in columns[4:]:
if asset: if asset:
asset_data.append(asset[-2].get(p.get("fieldname"))) asset_data.append(asset[-2].get(p.get("fieldname")))
if liability: if liability:

View File

@@ -8,7 +8,7 @@ from frappe.query_builder import Criterion, Tuple
from frappe.query_builder.functions import IfNull from frappe.query_builder.functions import IfNull
from frappe.utils import getdate, nowdate from frappe.utils import getdate, nowdate
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from pypika.terms import LiteralValue from pypika.terms import Bracket, LiteralValue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@@ -84,10 +84,8 @@ class PartyLedgerSummaryReport:
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions(party_type) if match_conditions := build_match_conditions(party_type):
query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query = query.where(LiteralValue(match_conditions))
party_details = query.run(as_dict=True) party_details = query.run(as_dict=True)

View File

@@ -11,7 +11,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Max, Min, Sum from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
from pypika.terms import ExistsCriterion from pypika.terms import Bracket, ExistsCriterion, LiteralValue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
@@ -564,18 +564,15 @@ def get_accounting_entries(
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry) account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
query = query.where(ExistsCriterion(account_filter_query)) query = query.where(ExistsCriterion(account_filter_query))
if group_by_account:
query = query.groupby("account")
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions: return query.run(as_dict=True)
query += "and" + match_conditions
if group_by_account:
query += " GROUP BY `account`"
return frappe.db.sql(query, params, as_dict=True)
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry): def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):

View File

@@ -224,7 +224,7 @@ frappe.query_reports["General Ledger"] = {
}, },
], ],
collapsible_filters: true, collapsible_filters: true,
seperate_check_filters: true, separate_check_filters: true,
}; };
erpnext.utils.add_dimensions("General Ledger", 15); erpnext.utils.add_dimensions("General Ledger", 15);

View File

@@ -324,10 +324,8 @@ def get_conditions(filters):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
match_conditions = build_match_conditions("GL Entry") if match_conditions := build_match_conditions("GL Entry"):
conditions.append(f"({match_conditions})")
if match_conditions:
conditions.append(match_conditions)
accounting_dimensions = get_accounting_dimensions(as_list=False) accounting_dimensions = get_accounting_dimensions(as_list=False)

View File

@@ -5,15 +5,16 @@ from collections import OrderedDict
import frappe import frappe
from frappe import _, qb, scrub from frappe import _, qb, scrub
from frappe.query_builder import Order from frappe.query_builder import Case, Order
from frappe.query_builder.functions import Coalesce
from frappe.utils import cint, flt, formatdate from frappe.utils import cint, flt, formatdate
from pypika.terms import ExistsCriterion
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
get_dimension_with_children, get_dimension_with_children,
) )
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
from erpnext.controllers.queries import get_match_cond
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@@ -176,7 +177,9 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
column_names = get_column_names() column_names = get_column_names()
# to display item as Item Code: Item Name # to display item as Item Code: Item Name
columns[0] = "Sales Invoice:Link/Item:300" columns[0]["fieldname"] = "sales_invoice"
columns[0]["options"] = "Item"
columns[0]["width"] = 300
# removing Item Code and Item Name columns # removing Item Code and Item Name columns
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name") supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name") customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
@@ -203,7 +206,11 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
data.append(row) data.append(row)
total_gross_profit = total_base_amount - total_buying_amount total_gross_profit = flt(
total_base_amount + abs(total_buying_amount)
if total_buying_amount < 0
else total_base_amount - total_buying_amount,
)
data.append( data.append(
frappe._dict( frappe._dict(
{ {
@@ -215,7 +222,7 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
"buying_amount": total_buying_amount, "buying_amount": total_buying_amount,
"gross_profit": total_gross_profit, "gross_profit": total_gross_profit,
"gross_profit_%": flt( "gross_profit_%": flt(
(total_gross_profit / total_base_amount) * 100.0, (total_gross_profit / abs(total_base_amount)) * 100.0,
cint(frappe.db.get_default("currency_precision")) or 3, cint(frappe.db.get_default("currency_precision")) or 3,
) )
if total_base_amount if total_base_amount
@@ -248,9 +255,13 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
data.append(row) data.append(row)
total_gross_profit = total_base_amount - total_buying_amount total_gross_profit = flt(
total_base_amount + abs(total_buying_amount)
if total_buying_amount < 0
else total_base_amount - total_buying_amount,
)
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3 currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0 gross_profit_percent = (total_gross_profit / abs(total_base_amount) * 100.0) if total_base_amount else 0
total_row = { total_row = {
group_columns[0]: "Total", group_columns[0]: "Total",
@@ -581,10 +592,15 @@ class GrossProfitGenerator:
base_amount += row.base_amount base_amount += row.base_amount
# calculate gross profit # calculate gross profit
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision) row.gross_profit = flt(
row.base_amount + abs(row.buying_amount)
if row.buying_amount < 0
else row.base_amount - row.buying_amount,
self.currency_precision,
)
if row.base_amount: if row.base_amount:
row.gross_profit_percent = flt( row.gross_profit_percent = flt(
(row.gross_profit / row.base_amount) * 100.0, (row.gross_profit / abs(row.base_amount)) * 100.0,
self.currency_precision, self.currency_precision,
) )
else: else:
@@ -673,9 +689,14 @@ class GrossProfitGenerator:
return new_row return new_row
def set_average_gross_profit(self, new_row): def set_average_gross_profit(self, new_row):
new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) new_row.gross_profit = flt(
new_row.base_amount + abs(new_row.buying_amount)
if new_row.buying_amount < 0
else new_row.base_amount - new_row.buying_amount,
self.currency_precision,
)
new_row.gross_profit_percent = ( new_row.gross_profit_percent = (
flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) flt(((new_row.gross_profit / abs(new_row.base_amount)) * 100.0), self.currency_precision)
if new_row.base_amount if new_row.base_amount
else 0 else 0
) )
@@ -851,129 +872,173 @@ class GrossProfitGenerator:
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
def load_invoice_items(self): def load_invoice_items(self):
conditions = "" self.si_list = []
if self.filters.company:
conditions += " and `tabSales Invoice`.company = %(company)s" SalesInvoice = frappe.qb.DocType("Sales Invoice")
if self.filters.from_date: base_query = self.prepare_invoice_query()
conditions += " and posting_date >= %(from_date)s"
if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s"
if self.filters.include_returned_invoices: if self.filters.include_returned_invoices:
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))" invoice_query = base_query.where(
(SalesInvoice.is_return == 0)
| ((SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnull())
)
else: else:
conditions += " and is_return = 0" invoice_query = base_query.where(SalesInvoice.is_return == 0)
if self.filters.item_group: self.si_list += invoice_query.run(as_dict=True)
conditions += f" and {get_item_group_condition(self.filters.item_group)}" self.prepare_vouchers_to_ignore()
if self.filters.sales_person: ret_invoice_query = base_query.where(
conditions += """ (SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnotnull()
and exists(select 1 )
from `tabSales Team` st if self.vouchers_to_ignore:
where st.parent = `tabSales Invoice`.name ret_invoice_query = ret_invoice_query.where(
and st.sales_person = %(sales_person)s) SalesInvoice.return_against.notin(self.vouchers_to_ignore)
""" )
self.si_list += ret_invoice_query.run(as_dict=True)
def prepare_invoice_query(self):
SalesInvoice = frappe.qb.DocType("Sales Invoice")
SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item")
Item = frappe.qb.DocType("Item")
SalesTeam = frappe.qb.DocType("Sales Team")
PaymentSchedule = frappe.qb.DocType("Payment Schedule")
query = (
frappe.qb.from_(SalesInvoice)
.join(SalesInvoiceItem)
.on(SalesInvoiceItem.parent == SalesInvoice.name)
.join(Item)
.on(Item.name == SalesInvoiceItem.item_code)
.where((SalesInvoice.docstatus == 1) & (SalesInvoice.is_opening != "Yes"))
)
query = self.apply_common_filters(query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item)
query = query.select(
SalesInvoiceItem.parenttype,
SalesInvoiceItem.parent,
SalesInvoice.posting_date,
SalesInvoice.posting_time,
SalesInvoice.project,
SalesInvoice.update_stock,
SalesInvoice.customer,
SalesInvoice.customer_group,
SalesInvoice.customer_name,
SalesInvoice.territory,
SalesInvoiceItem.item_code,
SalesInvoice.base_net_total.as_("invoice_base_net_total"),
SalesInvoiceItem.item_name,
SalesInvoiceItem.description,
SalesInvoiceItem.warehouse,
SalesInvoiceItem.item_group,
SalesInvoiceItem.brand,
SalesInvoiceItem.so_detail,
SalesInvoiceItem.sales_order,
SalesInvoiceItem.dn_detail,
SalesInvoiceItem.delivery_note,
SalesInvoiceItem.stock_qty.as_("qty"),
SalesInvoiceItem.base_net_rate,
SalesInvoiceItem.base_net_amount,
SalesInvoiceItem.name.as_("item_row"),
SalesInvoice.is_return,
SalesInvoiceItem.cost_center,
SalesInvoiceItem.serial_and_batch_bundle,
)
if self.filters.group_by == "Sales Person": if self.filters.group_by == "Sales Person":
sales_person_cols = """, sales.sales_person, query = query.select(
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount, SalesTeam.sales_person,
sales.incentives (SalesTeam.allocated_percentage * SalesInvoiceItem.base_net_amount / 100).as_(
""" "allocated_amount"
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" ),
else: SalesTeam.incentives,
sales_person_cols = "" )
sales_team_table = ""
query = query.left_join(SalesTeam).on(SalesTeam.parent == SalesInvoice.name)
if self.filters.group_by == "Payment Term": if self.filters.group_by == "Payment Term":
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1, query = query.select(
'{}', Case()
coalesce(schedule.payment_term, '{}')) as payment_term, .when(SalesInvoice.is_return == 1, _("Sales Return"))
schedule.invoice_portion, .else_(Coalesce(PaymentSchedule.payment_term, _("No Terms")))
schedule.payment_amount """.format(_("Sales Return"), _("No Terms")) .as_("payment_term"),
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and PaymentSchedule.invoice_portion,
`tabSales Invoice`.is_return = 0 """ PaymentSchedule.payment_amount,
else: )
payment_term_cols = ""
payment_term_table = ""
if self.filters.get("sales_invoice"): query = query.left_join(PaymentSchedule).on(
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s" (PaymentSchedule.parent == SalesInvoice.name) & (SalesInvoice.is_return == 0)
)
if self.filters.get("item_code"): query = query.orderby(SalesInvoice.posting_date, order=Order.desc).orderby(
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" SalesInvoice.posting_time, order=Order.desc
)
if self.filters.get("cost_center"): return query
def apply_common_filters(self, query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item):
if self.filters.company:
query = query.where(SalesInvoice.company == self.filters.company)
if self.filters.from_date:
query = query.where(SalesInvoice.posting_date >= self.filters.from_date)
if self.filters.to_date:
query = query.where(SalesInvoice.posting_date <= self.filters.to_date)
if self.filters.item_group:
query = query.where(get_item_group_condition(self.filters.item_group, Item))
if self.filters.sales_person:
query = query.where(
ExistsCriterion(
frappe.qb.from_(SalesTeam)
.select(1)
.where(
(SalesTeam.parent == SalesInvoice.name)
& (SalesTeam.sales_person == self.filters.sales_person)
)
)
)
if self.filters.sales_invoice:
query = query.where(SalesInvoice.name == self.filters.sales_invoice)
if self.filters.item_code:
query = query.where(SalesInvoiceItem.item_code == self.filters.item_code)
if self.filters.cost_center:
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center")) self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center) self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
conditions += " and `tabSales Invoice Item`.cost_center in %(cost_center)s" query = query.where(SalesInvoiceItem.cost_center.isin(self.filters.cost_center))
if self.filters.get("project"): if self.filters.project:
self.filters.project = frappe.parse_json(self.filters.get("project")) self.filters.project = frappe.parse_json(self.filters.get("project"))
conditions += " and `tabSales Invoice Item`.project in %(project)s" query = query.where(SalesInvoiceItem.project.isin(self.filters.project))
accounting_dimensions = get_accounting_dimensions(as_list=False) for dim in get_accounting_dimensions(as_list=False) or []:
if accounting_dimensions: if self.filters.get(dim.fieldname):
for dimension in accounting_dimensions: if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
if self.filters.get(dimension.fieldname): self.filters[dim.fieldname] = get_dimension_with_children(
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): dim.document_type, self.filters.get(dim.fieldname)
self.filters[dimension.fieldname] = get_dimension_with_children( )
dimension.document_type, self.filters.get(dimension.fieldname) query = query.where(SalesInvoiceItem[dim.fieldname].isin(self.filters[dim.fieldname]))
)
conditions += (
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
)
else:
conditions += (
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
)
if self.filters.get("warehouse"): if self.filters.warehouse:
warehouse_details = frappe.db.get_value( lft, rgt = frappe.db.get_value("Warehouse", self.filters.warehouse, ["lft", "rgt"])
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1 WH = frappe.qb.DocType("Warehouse")
query = query.where(
SalesInvoiceItem.warehouse.isin(
frappe.qb.from_(WH).select(WH.name).where((WH.lft >= lft) & (WH.rgt <= rgt))
)
) )
if warehouse_details:
conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
self.si_list = frappe.db.sql( return query
"""
select def prepare_vouchers_to_ignore(self):
`tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, self.vouchers_to_ignore = tuple(row["parent"] for row in self.si_list)
`tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time,
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer_name,
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail,
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
`tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.serial_and_batch_bundle
{sales_person_cols}
{payment_term_cols}
from
`tabSales Invoice` inner join `tabSales Invoice Item`
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
join `tabItem` item on item.name = `tabSales Invoice Item`.item_code
{sales_team_table}
{payment_term_table}
where
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
order by
`tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format(
conditions=conditions,
sales_person_cols=sales_person_cols,
sales_team_table=sales_team_table,
payment_term_cols=payment_term_cols,
payment_term_table=payment_term_table,
match_cond=get_match_cond("Sales Invoice"),
),
self.filters,
as_dict=1,
)
def get_delivery_notes(self): def get_delivery_notes(self):
self.delivery_notes = frappe._dict({}) self.delivery_notes = frappe._dict({})

View File

@@ -470,7 +470,7 @@ class TestGrossProfit(IntegrationTestCase):
"selling_amount": -100.0, "selling_amount": -100.0,
"buying_amount": 0.0, "buying_amount": 0.0,
"gross_profit": -100.0, "gross_profit": -100.0,
"gross_profit_%": 100.0, "gross_profit_%": -100.0,
} }
gp_entry = [x for x in data if x.parent_invoice == sinv.name] gp_entry = [x for x in data if x.parent_invoice == sinv.name]
report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry} report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry}
@@ -649,21 +649,24 @@ class TestGrossProfit(IntegrationTestCase):
def test_profit_for_later_period_return(self): def test_profit_for_later_period_return(self):
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
sales_inv_date = month_start_date
return_inv_date = add_days(month_end_date, 1)
# create sales invoice on month start date # create sales invoice on month start date
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True) sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
sinv.set_posting_time = 1 sinv.set_posting_time = 1
sinv.posting_date = month_start_date sinv.posting_date = sales_inv_date
sinv.save().submit() sinv.save().submit()
# create credit note on next month start date # create credit note on next month start date
cr_note = make_sales_return(sinv.name) cr_note = make_sales_return(sinv.name)
cr_note.set_posting_time = 1 cr_note.set_posting_time = 1
cr_note.posting_date = add_days(month_end_date, 1) cr_note.posting_date = return_inv_date
cr_note.save().submit() cr_note.save().submit()
# apply filters for invoiced period # apply filters for invoiced period
filters = frappe._dict( filters = frappe._dict(
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice" company=self.company, from_date=month_start_date, to_date=month_start_date, group_by="Invoice"
) )
_, data = execute(filters=filters) _, data = execute(filters=filters)
@@ -675,7 +678,7 @@ class TestGrossProfit(IntegrationTestCase):
self.assertEqual(total.get("gross_profit_%"), 100.0) self.assertEqual(total.get("gross_profit_%"), 100.0)
# extend filters upto returned period # extend filters upto returned period
filters.update(to_date=add_days(month_end_date, 1)) filters.update({"to_date": return_inv_date})
_, data = execute(filters=filters) _, data = execute(filters=filters)
total = data[-1] total = data[-1]
@@ -684,3 +687,63 @@ class TestGrossProfit(IntegrationTestCase):
self.assertEqual(total.buying_amount, 0.0) self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 0.0) self.assertEqual(total.gross_profit, 0.0)
self.assertEqual(total.get("gross_profit_%"), 0.0) self.assertEqual(total.get("gross_profit_%"), 0.0)
# apply filters only on returned period
filters.update({"from_date": return_inv_date, "to_date": return_inv_date})
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, -100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, -100.0)
self.assertEqual(total.get("gross_profit_%"), -100.0)
def test_sales_person_wise_gross_profit(self):
sales_person = make_sales_person("_Test Sales Person")
posting_date = get_first_day(nowdate())
qty = 10
rate = 100
sinv = self.create_sales_invoice(qty=qty, rate=rate, do_not_save=True, do_not_submit=True)
sinv.set_posting_time = 1
sinv.posting_date = posting_date
sinv.append(
"sales_team",
{
"sales_person": sales_person.name,
"allocated_percentage": 100,
"allocated_amount": 1000.0,
"commission_rate": 5,
"incentives": 5,
},
)
sinv.save().submit()
filters = frappe._dict(
company=self.company, from_date=posting_date, to_date=posting_date, group_by="Sales Person"
)
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total[5], 1000.0)
self.assertEqual(total[6], 0.0)
self.assertEqual(total[7], 1000.0)
self.assertEqual(total[8], 100.0)
def make_sales_person(sales_person_name="_Test Sales Person"):
if not frappe.db.exists("Sales Person", {"sales_person_name": sales_person_name}):
sales_person_doc = frappe.get_doc(
{
"doctype": "Sales Person",
"is_group": 0,
"parent_sales_person": "Sales Team",
"sales_person_name": sales_person_name,
}
).insert(ignore_permissions=True)
else:
sales_person_doc = frappe.get_doc("Sales Person", {"sales_person_name": sales_person_name})
return sales_person_doc

View File

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from pypika.terms import Bracket, LiteralValue
import erpnext import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import ( from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
@@ -361,15 +362,12 @@ def get_items(filters, additional_table_columns):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(doctype, query, filters) query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True) return query.run(as_dict=True)
def get_aii_accounts(): def get_aii_accounts():

View File

@@ -8,6 +8,7 @@ from frappe.query_builder import functions as fn
from frappe.utils import flt from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html from frappe.utils.xlsxutils import handle_html
from pypika.terms import Bracket, LiteralValue, Order
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_values_for_columns from erpnext.accounts.report.utils import get_values_for_columns
@@ -390,20 +391,21 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
def apply_order_by_conditions(doctype, query, filters): def apply_order_by_conditions(doctype, query, filters):
invoice = f"`tab{doctype}`" invoice = frappe.qb.DocType(doctype)
invoice_item = f"`tab{doctype} Item`" invoice_item = frappe.qb.DocType(f"{doctype} Item")
if not filters.get("group_by"): if not filters.get("group_by"):
query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc" query = query.orderby(invoice.posting_date, order=Order.desc)
query = query.orderby(invoice_item.item_group, order=Order.desc)
elif filters.get("group_by") == "Invoice": elif filters.get("group_by") == "Invoice":
query += f" order by {invoice_item}.parent desc" query = query.orderby(invoice_item.parent, order=Order.desc)
elif filters.get("group_by") == "Item": elif filters.get("group_by") == "Item":
query += f" order by {invoice_item}.item_code" query = query.orderby(invoice_item.item_code)
elif filters.get("group_by") == "Item Group": elif filters.get("group_by") == "Item Group":
query += f" order by {invoice_item}.item_group" query = query.orderby(invoice_item.item_group)
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"): elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by")) filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc" query = query.orderby(filter_field, order=Order.desc)
return query return query
@@ -481,15 +483,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions(doctype):
match_conditions = build_match_conditions(doctype) query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(doctype, query, filters) query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True) return query.run(as_dict=True)
def get_delivery_notes_against_sales_order(item_list): def get_delivery_notes_against_sales_order(item_list):

View File

@@ -163,11 +163,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency): def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
labels = [d.get("label") for d in columns[2:]] labels = [d.get("label") for d in columns[4:]]
income_data, expense_data, net_profit = [], [], [] income_data, expense_data, net_profit = [], [], []
for p in columns[2:]: for p in columns[4:]:
if income: if income:
income_data.append(income[-2].get(p.get("fieldname"))) income_data.append(income[-2].get(p.get("fieldname")))
if expense: if expense:

View File

@@ -3,7 +3,8 @@
import frappe import frappe
from frappe import _ from frappe import _, qb
from frappe.query_builder import Criterion
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
@@ -33,11 +34,19 @@ def execute(filters=None):
def get_accounts_data(based_on, company): def get_accounts_data(based_on, company):
if based_on == "Cost Center": if based_on == "Cost Center":
return frappe.db.sql( cc = qb.DocType("Cost Center")
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt return (
from `tabCost Center` where company=%s order by name""", qb.from_(cc)
company, .select(
as_dict=True, cc.name,
cc.parent_cost_center.as_("parent_account"),
cc.cost_center_name.as_("account_name"),
cc.lft,
cc.rgt,
)
.where(cc.company.eq(company))
.orderby(cc.name)
.run(as_dict=True)
) )
elif based_on == "Project": elif based_on == "Project":
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name") return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
@@ -206,27 +215,38 @@ def set_gl_entries_by_account(
company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False
): ):
"""Returns a dict like { "account": [gl entries], ... }""" """Returns a dict like { "account": [gl entries], ... }"""
additional_conditions = [] gl = qb.DocType("GL Entry")
acc = qb.DocType("Account")
conditions = []
conditions.append(gl.company.eq(company))
conditions.append(gl[based_on].notnull())
conditions.append(gl.is_cancelled.eq(0))
if from_date and to_date:
conditions.append(gl.posting_date.between(from_date, to_date))
elif from_date and not to_date:
conditions.append(gl.posting_date.gte(from_date))
elif not from_date and to_date:
conditions.append(gl.posting_date.lte(to_date))
if ignore_closing_entries: if ignore_closing_entries:
additional_conditions.append("and voucher_type !='Period Closing Voucher'") conditions.append(gl.voucher_type.ne("Period Closing Voucher"))
if from_date: root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account))
additional_conditions.append("and posting_date >= %(from_date)s") gl_entries = (
qb.from_(gl)
gl_entries = frappe.db.sql( .select(
"""select posting_date, {based_on} as based_on, debit, credit, gl.posting_date,
is_opening, (select root_type from `tabAccount` where name = account) as type gl[based_on].as_("based_on"),
from `tabGL Entry` where company=%(company)s gl.debit,
{additional_conditions} gl.credit,
and posting_date <= %(to_date)s gl.is_opening,
and {based_on} is not null root_subquery.as_("type"),
and is_cancelled = 0 )
order by {based_on}, posting_date""".format( .where(Criterion.all(conditions))
additional_conditions="\n".join(additional_conditions), based_on=based_on .orderby(gl[based_on], gl.posting_date)
), .run(as_dict=True)
{"company": company, "from_date": from_date, "to_date": to_date},
as_dict=True,
) )
for entry in gl_entries: for entry in gl_entries:

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, getdate from frappe.utils import flt, getdate
from pypika.terms import Bracket, LiteralValue, Order
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.utils import ( from erpnext.accounts.report.utils import (
@@ -421,15 +422,13 @@ def get_invoices(filters, additional_query_columns):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions("Purchase Invoice"):
match_conditions = build_match_conditions("Purchase Invoice") query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions: query = query.orderby("posting_date", order=Order.desc)
query += " and " + match_conditions query = query.orderby("name", order=Order.desc)
query += " order by posting_date desc, name desc" return query.run(as_dict=True)
return frappe.db.sql(query, params, as_dict=True)
def get_conditions(filters, query, doctype): def get_conditions(filters, query, doctype):

View File

@@ -7,6 +7,7 @@ from frappe import _, msgprint
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, getdate from frappe.utils import flt, getdate
from pypika.terms import Bracket, LiteralValue, Order
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.utils import ( from erpnext.accounts.report.utils import (
@@ -457,15 +458,13 @@ def get_invoices(filters, additional_query_columns):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() if match_conditions := build_match_conditions("Sales Invoice"):
match_conditions = build_match_conditions("Sales Invoice") query = query.where(Bracket(LiteralValue(match_conditions)))
if match_conditions: query = query.orderby("posting_date", order=Order.desc)
query += " and " + match_conditions query = query.orderby("name", order=Order.desc)
query += " order by posting_date desc, name desc" return query.run(as_dict=True)
return frappe.db.sql(query, params, as_dict=True)
def get_conditions(filters, query, doctype): def get_conditions(filters, query, doctype):

View File

@@ -154,17 +154,11 @@ def get_columns(filters):
"width": 60, "width": 60,
}, },
{ {
"label": _("Total Amount"), "label": _("Taxable Amount"),
"fieldname": "total_amount", "fieldname": "total_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 120, "width": 120,
}, },
{
"label": _("Base Total"),
"fieldname": "base_total",
"fieldtype": "Currency",
"width": 120,
},
{ {
"label": _("Tax Amount"), "label": _("Tax Amount"),
"fieldname": "tax_amount", "fieldname": "tax_amount",
@@ -172,10 +166,16 @@ def get_columns(filters):
"width": 120, "width": 120,
}, },
{ {
"label": _("Grand Total"), "label": _("Grand Total (Company Currency)"),
"fieldname": "base_total",
"fieldtype": "Currency",
"width": 150,
},
{
"label": _("Grand Total (Transaction Currency)"),
"fieldname": "grand_total", "fieldname": "grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 120, "width": 170,
}, },
{ {
"label": _("Reference Date"), "label": _("Reference Date"),

View File

@@ -106,7 +106,7 @@ def get_columns(filters):
"width": 120, "width": 120,
}, },
{ {
"label": _("Total Amount"), "label": _("Total Taxable Amount"),
"fieldname": "total_amount", "fieldname": "total_amount",
"fieldtype": "Float", "fieldtype": "Float",
"width": 120, "width": 120,

View File

@@ -11,6 +11,7 @@ import frappe.defaults
from frappe import _, qb, throw from frappe import _, qb, throw
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.model.naming import determine_consecutive_week_number
from frappe.query_builder import AliasedQuery, Case, Criterion, Field, Table from frappe.query_builder import AliasedQuery, Case, Criterion, Field, Table
from frappe.query_builder.functions import Count, IfNull, Max, Round, Sum from frappe.query_builder.functions import Count, IfNull, Max, Round, Sum
from frappe.query_builder.utils import DocType from frappe.query_builder.utils import DocType
@@ -25,6 +26,7 @@ from frappe.utils import (
get_number_format_info, get_number_format_info,
getdate, getdate,
now, now,
now_datetime,
nowdate, nowdate,
) )
from frappe.utils.caching import site_cache from frappe.utils.caching import site_cache
@@ -66,6 +68,7 @@ def get_fiscal_year(
as_dict=False, as_dict=False,
boolean=None, boolean=None,
raise_on_missing=True, raise_on_missing=True,
truncate=False,
): ):
if isinstance(raise_on_missing, str): if isinstance(raise_on_missing, str):
raise_on_missing = loads(raise_on_missing) raise_on_missing = loads(raise_on_missing)
@@ -79,7 +82,14 @@ def get_fiscal_year(
fiscal_years = get_fiscal_years( fiscal_years = get_fiscal_years(
date, fiscal_year, label, verbose, company, as_dict=as_dict, raise_on_missing=raise_on_missing date, fiscal_year, label, verbose, company, as_dict=as_dict, raise_on_missing=raise_on_missing
) )
return False if not fiscal_years else fiscal_years[0]
if fiscal_years:
fiscal_year = fiscal_years[0]
if truncate:
return ("-".join(y[-2:] for y in fiscal_year[0].split("-")), fiscal_year[1], fiscal_year[2])
return fiscal_year
return False
def get_fiscal_years( def get_fiscal_years(
@@ -547,6 +557,7 @@ def reconcile_against_document(
doc.make_advance_gl_entries(entry=row) doc.make_advance_gl_entries(entry=row)
else: else:
_delete_pl_entries(voucher_type, voucher_no) _delete_pl_entries(voucher_type, voucher_no)
_delete_adv_pl_entries(voucher_type, voucher_no)
gl_map = doc.build_gl_map() gl_map = doc.build_gl_map()
# Make sure there is no overallocation # Make sure there is no overallocation
from erpnext.accounts.general_ledger import process_debit_credit_difference from erpnext.accounts.general_ledger import process_debit_credit_difference
@@ -662,6 +673,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
d["allocated_amount"] = d["allocated_amount"] * -1 d["allocated_amount"] = d["allocated_amount"] * -1
d["unadjusted_amount"] = d["unadjusted_amount"] * -1 d["unadjusted_amount"] = d["unadjusted_amount"] * -1
insert_position = -1
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance # adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@@ -673,9 +685,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
) )
else: else:
journal_entry.remove(jv_detail) journal_entry.remove(jv_detail)
insert_position += jv_detail.idx
# new row with references # new row with references
new_row = journal_entry.append("accounts") new_row = journal_entry.append("accounts", position=insert_position)
# Copy field values into new row # Copy field values into new row
[ [
@@ -1500,14 +1513,14 @@ def get_autoname_with_number(number_value, doc_title, company):
def parse_naming_series_variable(doc, variable): def parse_naming_series_variable(doc, variable):
if variable == "FY": if variable in ["FY", "TFY"]:
if doc: if doc:
date = doc.get("posting_date") or doc.get("transaction_date") or getdate() date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
company = doc.get("company") company = doc.get("company")
else: else:
date = getdate() date = getdate()
company = None company = None
return get_fiscal_year(date=date, company=company)[0] return get_fiscal_year(date=date, company=company, truncate=variable == "TFY")[0]
elif variable == "ABBR": elif variable == "ABBR":
if doc: if doc:
@@ -1517,6 +1530,18 @@ def parse_naming_series_variable(doc, variable):
return frappe.db.get_value("Company", company, "abbr") if company else "" return frappe.db.get_value("Company", company, "abbr") if company else ""
else:
data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"}
date = (
(
getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime"))
or now_datetime()
)
if frappe.get_single_value("Global Defaults", "use_posting_datetime_for_naming_documents")
else now_datetime()
)
return date.strftime(data[variable]) if variable in data else determine_consecutive_week_number(date)
@frappe.whitelist() @frappe.whitelist()
def get_coa(doctype, parent, is_root=None, chart=None): def get_coa(doctype, parent, is_root=None, chart=None):
@@ -1946,6 +1971,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
account=gle.account, account=gle.account,
party_type=gle.party_type, party_type=gle.party_type,
party=gle.party, party=gle.party,
project=gle.project,
cost_center=gle.cost_center, cost_center=gle.cost_center,
finance_book=gle.finance_book, finance_book=gle.finance_book,
due_date=gle.due_date, due_date=gle.due_date,

View File

@@ -14,10 +14,10 @@
"for_user": "", "for_user": "",
"hide_custom": 0, "hide_custom": 0,
"icon": "accounting", "icon": "accounting",
"idx": 3, "idx": 4,
"indicator_color": "", "indicator_color": "",
"is_hidden": 0, "is_hidden": 0,
"label": "Accounting", "label": "Invoicing",
"links": [ "links": [
{ {
"hidden": 0, "hidden": 0,
@@ -587,10 +587,10 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2025-12-24 13:20:34.857205", "modified": "2026-01-23 11:05:47.246213",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Invoicing",
"number_cards": [ "number_cards": [
{ {
"label": "Outgoing Bills", "label": "Outgoing Bills",
@@ -617,6 +617,6 @@
"roles": [], "roles": [],
"sequence_id": 2.0, "sequence_id": 2.0,
"shortcuts": [], "shortcuts": [],
"title": "Accounting", "title": "Invoicing",
"type": "Workspace" "type": "Workspace"
} }

View File

@@ -6,11 +6,11 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Asset Category\",\"is_existing_asset\":0}", "filters_json": "{\"status\":\"In Location\",\"group_by\":\"Asset Category\",\"asset_type\":[\"!=\",\"Existing Asset\"]}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-10-28 23:16:16.939070", "modified": "2026-02-03 15:48:13.407835",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Category-wise Asset Value", "name": "Category-wise Asset Value",

View File

@@ -6,11 +6,11 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Location\",\"is_existing_asset\":0}", "filters_json": "{\"status\":\"In Location\",\"group_by\":\"Location\",\"asset_type\":[\"!=\",\"Existing Asset\"]}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-10-28 23:16:07.883312", "modified": "2026-02-03 15:48:13.407835",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Location-wise Asset Value", "name": "Location-wise Asset Value",

View File

@@ -100,7 +100,7 @@ def get_charts(fiscal_year, year_start_date, year_end_date):
"company": company, "company": company,
"status": "In Location", "status": "In Location",
"group_by": "Asset Category", "group_by": "Asset Category",
"is_existing_asset": 0, "asset_type": ["!=", "Existing Asset"],
} }
), ),
"type": "Donut", "type": "Donut",
@@ -126,7 +126,12 @@ def get_charts(fiscal_year, year_start_date, year_end_date):
"x_field": "location", "x_field": "location",
"timeseries": 0, "timeseries": 0,
"filters_json": json.dumps( "filters_json": json.dumps(
{"company": company, "status": "In Location", "group_by": "Location", "is_existing_asset": 0} {
"company": company,
"status": "In Location",
"group_by": "Location",
"asset_type": ["!=", "Existing Asset"],
}
), ),
"type": "Donut", "type": "Donut",
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",

View File

@@ -81,23 +81,79 @@ frappe.ui.form.on("Asset", {
}, },
before_submit: function (frm) { before_submit: function (frm) {
if (frm.doc.is_composite_asset && !frm.has_active_capitalization) { if (frm.doc.asset_type == "Composite Asset" && !frm.has_active_capitalization) {
frappe.throw(__("Please capitalize this asset before submitting.")); frappe.throw(__("Please capitalize this asset before submitting."));
} }
}, },
refresh: function (frm) { refresh: async function (frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset"); frappe.ui.form.trigger("Asset", "asset_type");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
let has_create_buttons = false;
if (frm.doc.docstatus == 1) { if (frm.doc.docstatus == 1) {
if (["Submitted", "Partially Depreciated"].includes(frm.doc.status)) {
frm.add_custom_button(
__("Asset Value Adjustment"),
function () {
frm.trigger("create_asset_value_adjustment");
},
__("Create")
);
frm.add_custom_button(
__("Asset Repair"),
function () {
frm.trigger("create_asset_repair");
},
__("Create")
);
has_create_buttons = true;
}
if (
!frm.doc.calculate_depreciation &&
["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)
) {
frm.add_custom_button(
__("Depreciation Entry"),
function () {
frm.trigger("make_journal_entry");
},
__("Create")
);
has_create_buttons = true;
}
if (has_create_buttons) {
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) { if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) {
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
frm.add_custom_button(
__("Maintain Asset"),
function () {
frm.trigger("create_asset_maintenance");
},
__("Actions")
);
}
frm.add_custom_button(
__("Split Asset"),
function () {
frm.trigger("split_asset");
},
__("Actions")
);
frm.add_custom_button( frm.add_custom_button(
__("Transfer Asset"), __("Transfer Asset"),
function () { function () {
erpnext.asset.transfer_asset(frm); erpnext.asset.transfer_asset(frm);
}, },
__("Manage") __("Actions")
); );
frm.add_custom_button( frm.add_custom_button(
@@ -105,7 +161,7 @@ frappe.ui.form.on("Asset", {
function () { function () {
erpnext.asset.scrap_asset(frm); erpnext.asset.scrap_asset(frm);
}, },
__("Manage") __("Actions")
); );
frm.add_custom_button( frm.add_custom_button(
@@ -113,23 +169,7 @@ frappe.ui.form.on("Asset", {
function () { function () {
frm.trigger("sell_asset"); frm.trigger("sell_asset");
}, },
__("Manage") __("Actions")
);
frm.add_custom_button(
__("Repair Asset"),
function () {
frm.trigger("create_asset_repair");
},
__("Manage")
);
frm.add_custom_button(
__("Split Asset"),
function () {
frm.trigger("split_asset");
},
__("Manage")
); );
} else if (frm.doc.status == "Scrapped") { } else if (frm.doc.status == "Scrapped") {
frm.add_custom_button(__("Restore Asset"), function () { frm.add_custom_button(__("Restore Asset"), function () {
@@ -137,39 +177,9 @@ frappe.ui.form.on("Asset", {
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { if (await frm.events.should_show_accounting_ledger(frm)) {
frm.add_custom_button( frm.add_custom_button(
__("Maintain Asset"), __("Accounting Ledger"),
function () {
frm.trigger("create_asset_maintenance");
},
__("Manage")
);
}
if (["Submitted", "Partially Depreciated"].includes(frm.doc.status)) {
frm.add_custom_button(
__("Adjust Asset Value"),
function () {
frm.trigger("create_asset_value_adjustment");
},
__("Manage")
);
}
if (!frm.doc.calculate_depreciation) {
frm.add_custom_button(
__("Create Depreciation Entry"),
function () {
frm.trigger("make_journal_entry");
},
__("Manage")
);
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
frm.add_custom_button(
__("View General Ledger"),
function () { function () {
frappe.route_options = { frappe.route_options = {
voucher_no: frm.doc.name, voucher_no: frm.doc.name,
@@ -179,7 +189,7 @@ frappe.ui.form.on("Asset", {
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");
}, },
__("Manage") __("View")
); );
} }
@@ -195,7 +205,7 @@ frappe.ui.form.on("Asset", {
if (frm.doc.docstatus == 0) { if (frm.doc.docstatus == 0) {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
if (frm.doc.is_composite_asset) { if (frm.doc.asset_type == "Composite Asset") {
frappe.call({ frappe.call({
method: "erpnext.assets.doctype.asset.asset.has_active_capitalization", method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
args: { args: {
@@ -217,6 +227,28 @@ frappe.ui.form.on("Asset", {
} }
}, },
should_show_accounting_ledger: async function (frm) {
if (["Capitalized"].includes(frm.doc.status)) {
return false;
}
if (
!frm.doc.purchase_receipt &&
!frm.doc.purchase_invoice &&
["Existing Asset", "Composite Component"].includes(frm.doc.asset_type)
) {
return false;
}
const asset_category = await frappe.db.get_value(
"Asset Category",
frm.doc.asset_category,
"enable_cwip_accounting"
);
return !!asset_category.message?.enable_cwip_accounting;
},
set_depr_posting_failure_alert: function (frm) { set_depr_posting_failure_alert: function (frm) {
const alert = ` const alert = `
<div class="row"> <div class="row">
@@ -232,7 +264,8 @@ frappe.ui.form.on("Asset", {
toggle_reference_doc: function (frm) { toggle_reference_doc: function (frm) {
const is_submitted = frm.doc.docstatus === 1; const is_submitted = frm.doc.docstatus === 1;
const is_special_asset = frm.doc.is_existing_asset || frm.doc.is_composite_asset; const is_special_asset =
frm.doc.asset_type == "Existing Asset" || frm.doc.asset_type == "Composite Asset";
const clear_field = (field) => { const clear_field = (field) => {
if (frm.doc[field]) { if (frm.doc[field]) {
@@ -508,15 +541,13 @@ frappe.ui.form.on("Asset", {
}); });
}, },
is_existing_asset: function (frm) { asset_type: function (frm) {
frm.trigger("toggle_reference_doc"); if (frm.doc.docstatus == 0) {
}, if (frm.doc.asset_type == "Composite Asset") {
frm.set_value("net_purchase_amount", 0);
is_composite_asset: function (frm) { } else {
if (frm.doc.is_composite_asset) { frm.set_df_property("net_purchase_amount", "read_only", 0);
frm.set_value("net_purchase_amount", 0); }
} else {
frm.set_df_property("net_purchase_amount", "read_only", 0);
} }
frm.trigger("toggle_reference_doc"); frm.trigger("toggle_reference_doc");
}, },

View File

@@ -9,20 +9,17 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series", "naming_series",
"company",
"item_code", "item_code",
"item_name", "item_name",
"asset_name", "asset_name",
"asset_category",
"location",
"image", "image",
"column_break_3", "column_break_3",
"status", "location",
"company", "asset_category",
"asset_owner", "asset_type",
"asset_owner_company", "maintenance_required",
"is_existing_asset", "calculate_depreciation",
"is_composite_asset",
"is_composite_component",
"purchase_details_section", "purchase_details_section",
"purchase_receipt", "purchase_receipt",
"purchase_receipt_item", "purchase_receipt_item",
@@ -30,31 +27,44 @@
"purchase_invoice_item", "purchase_invoice_item",
"purchase_date", "purchase_date",
"available_for_use_date", "available_for_use_date",
"disposal_date",
"column_break_23", "column_break_23",
"net_purchase_amount", "net_purchase_amount",
"purchase_amount", "purchase_amount",
"asset_quantity", "asset_quantity",
"additional_asset_cost", "additional_asset_cost",
"section_break_uiyd",
"column_break_bbwr",
"column_break_bfkm",
"total_asset_cost", "total_asset_cost",
"disposal_date",
"depreciation_tab", "depreciation_tab",
"calculate_depreciation", "column_break_wqzi",
"column_break_33",
"opening_accumulated_depreciation", "opening_accumulated_depreciation",
"opening_number_of_booked_depreciations",
"is_fully_depreciated", "is_fully_depreciated",
"column_break_33",
"opening_number_of_booked_depreciations",
"section_break_36", "section_break_36",
"finance_books", "finance_books",
"section_break_33", "section_break_33",
"depreciation_method", "depreciation_method",
"value_after_depreciation", "value_after_depreciation",
"total_number_of_depreciations",
"column_break_24",
"frequency_of_depreciation", "frequency_of_depreciation",
"column_break_24",
"next_depreciation_date", "next_depreciation_date",
"total_number_of_depreciations",
"depreciation_schedule_sb", "depreciation_schedule_sb",
"depreciation_schedule_view", "depreciation_schedule_view",
"insurance_details_tab", "other_info_tab",
"accounting_dimensions_section",
"cost_center",
"column_break_rjyw",
"asset_owner_section",
"asset_owner",
"column_break_yeds",
"asset_owner_company",
"customer",
"supplier",
"insurance_section",
"policy_number", "policy_number",
"insurer", "insurer",
"insured_value", "insured_value",
@@ -62,22 +72,17 @@
"insurance_start_date", "insurance_start_date",
"insurance_end_date", "insurance_end_date",
"comprehensive_insurance", "comprehensive_insurance",
"other_info_tab",
"accounting_dimensions_section",
"cost_center",
"section_break_jtou", "section_break_jtou",
"status",
"custodian", "custodian",
"department",
"default_finance_book", "default_finance_book",
"depr_entry_posting_status", "depr_entry_posting_status",
"booked_fixed_asset",
"customer",
"supplier",
"column_break_51", "column_break_51",
"department",
"split_from",
"journal_entry_for_scrap", "journal_entry_for_scrap",
"split_from",
"amended_from", "amended_from",
"maintenance_required", "booked_fixed_asset",
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
@@ -106,13 +111,6 @@
"options": "Item", "options": "Item",
"reqd": 1 "reqd": 1
}, },
{
"depends_on": "item_code",
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Read Only",
"label": "Item Name"
},
{ {
"depends_on": "item_code", "depends_on": "item_code",
"fetch_from": "item_code.asset_category", "fetch_from": "item_code.asset_category",
@@ -207,7 +205,7 @@
"fieldname": "purchase_date", "fieldname": "purchase_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Purchase Date", "label": "Purchase Date",
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "read_only_depends_on": "eval:doc.asset_type != \"Existing Asset\" && doc.asset_type != \"Composite Asset\"",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -229,25 +227,18 @@
{ {
"fieldname": "available_for_use_date", "fieldname": "available_for_use_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Available-for-use Date", "label": "Available for Use Date",
"mandatory_depends_on": "eval:(!(doc.is_composite_component || doc.is_composite_asset) || doc.docstatus==1)" "mandatory_depends_on": "eval:(!(doc.asset_type == \"Composite Component\" || doc.asset_type == \"Composite Asset\") || doc.docstatus==1)"
}, },
{ {
"default": "0", "default": "0",
"fieldname": "calculate_depreciation", "fieldname": "calculate_depreciation",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Calculate Depreciation", "label": "Calculate Depreciation",
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.net_purchase_amount) || doc.is_composite_component" "read_only_depends_on": "eval:(doc.asset_type == \"Composite Asset\" && !doc.net_purchase_amount) || doc.asset_type == \"Composite Component\""
}, },
{ {
"default": "0", "depends_on": "eval:(doc.asset_type == \"Existing Asset\")",
"depends_on": "eval:(!doc.is_composite_asset && !doc.is_composite_component)",
"fieldname": "is_existing_asset",
"fieldtype": "Check",
"label": "Is Existing Asset"
},
{
"depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "opening_accumulated_depreciation", "fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Opening Accumulated Depreciation", "label": "Opening Accumulated Depreciation",
@@ -257,18 +248,20 @@
"columns": 10, "columns": 10,
"fieldname": "finance_books", "fieldname": "finance_books",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Finance Books",
"options": "Asset Finance Book" "options": "Asset Finance Book"
}, },
{ {
"fieldname": "section_break_33", "fieldname": "section_break_33",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 1 "hidden": 1,
"label": "Depreciation Details"
}, },
{ {
"fieldname": "depreciation_method", "fieldname": "depreciation_method",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Depreciation Method", "label": "Depreciation Method",
"options": "\nStraight Line\nDouble Declining Balance\nManual" "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual"
}, },
{ {
"fieldname": "value_after_depreciation", "fieldname": "value_after_depreciation",
@@ -295,6 +288,7 @@
{ {
"fieldname": "next_depreciation_date", "fieldname": "next_depreciation_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 1,
"label": "Next Depreciation Date", "label": "Next Depreciation Date",
"no_copy": 1 "no_copy": 1
}, },
@@ -364,7 +358,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset", "depends_on": "eval:doc.asset_type != \"Composite Asset\" && doc.asset_type != \"Existing Asset\"",
"fieldname": "purchase_receipt", "fieldname": "purchase_receipt",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Purchase Receipt", "label": "Purchase Receipt",
@@ -373,7 +367,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset", "depends_on": "eval:doc.asset_type != \"Composite Asset\" && doc.asset_type != \"Existing Asset\"",
"fieldname": "purchase_invoice", "fieldname": "purchase_invoice",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Purchase Invoice", "label": "Purchase Invoice",
@@ -399,7 +393,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible_depends_on": "is_existing_asset", "collapsible_depends_on": "eval:doc.asset_type == \"Existing Asset\"",
"fieldname": "purchase_details_section", "fieldname": "purchase_details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Purchase Details" "label": "Purchase Details"
@@ -413,10 +407,9 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "calculate_depreciation", "depends_on": "eval: doc.calculate_depreciation",
"fieldname": "section_break_36", "fieldname": "section_break_36",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Finance Books"
}, },
{ {
"fieldname": "split_from", "fieldname": "split_from",
@@ -455,18 +448,11 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:(doc.is_existing_asset)",
"fieldname": "is_fully_depreciated", "fieldname": "is_fully_depreciated",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"label": "Is Fully Depreciated" "label": "Is Fully Depreciated"
}, },
{
"default": "0",
"depends_on": "eval:(!doc.is_existing_asset && !doc.is_composite_component)",
"fieldname": "is_composite_asset",
"fieldtype": "Check",
"label": "Is Composite Asset"
},
{ {
"depends_on": "eval:doc.docstatus > 0", "depends_on": "eval:doc.docstatus > 0",
"fieldname": "total_asset_cost", "fieldname": "total_asset_cost",
@@ -496,7 +482,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:(doc.is_existing_asset)", "depends_on": "eval:(doc.asset_type == \"Existing Asset\")",
"fieldname": "opening_number_of_booked_depreciations", "fieldname": "opening_number_of_booked_depreciations",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Opening Number of Booked Depreciations" "label": "Opening Number of Booked Depreciations"
@@ -513,15 +499,10 @@
"hidden": 1, "hidden": 1,
"label": "Purchase Invoice Item" "label": "Purchase Invoice Item"
}, },
{
"fieldname": "insurance_details_tab",
"fieldtype": "Tab Break",
"label": "Insurance"
},
{ {
"fieldname": "other_info_tab", "fieldname": "other_info_tab",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Other Info" "label": "More Info"
}, },
{ {
"fieldname": "connections_tab", "fieldname": "connections_tab",
@@ -530,6 +511,7 @@
"show_dashboard": 1 "show_dashboard": 1
}, },
{ {
"depends_on": "eval: doc.calculate_depreciation || doc.asset_type == \"Existing Asset\"",
"fieldname": "depreciation_tab", "fieldname": "depreciation_tab",
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Depreciation" "label": "Depreciation"
@@ -544,20 +526,61 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Additional Info" "label": "Additional Info"
}, },
{
"default": "0",
"depends_on": "eval:(!doc.is_existing_asset && !doc.is_composite_asset)",
"fieldname": "is_composite_component",
"fieldtype": "Check",
"label": "Is Composite Component"
},
{ {
"fieldname": "net_purchase_amount", "fieldname": "net_purchase_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Net Purchase Amount", "label": "Net Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)", "mandatory_depends_on": "eval:(doc.asset_type != \"Composite Asset\" || doc.docstatus==1)",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only_depends_on": "eval: doc.is_composite_asset" "read_only_depends_on": "eval: doc.asset_type == \"Composite Asset\""
},
{
"fieldname": "asset_type",
"fieldtype": "Select",
"label": "Asset Type",
"options": "\nExisting Asset\nComposite Asset\nComposite Component"
},
{
"fieldname": "column_break_wqzi",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_rjyw",
"fieldtype": "Column Break"
},
{
"fieldname": "insurance_section",
"fieldtype": "Section Break",
"label": "Insurance"
},
{
"fieldname": "section_break_uiyd",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_bbwr",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_bfkm",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
"fieldname": "item_name",
"fieldtype": "Read Only",
"hidden": 1,
"label": "Item Name"
},
{
"fieldname": "asset_owner_section",
"fieldtype": "Section Break",
"label": "Ownership"
},
{
"fieldname": "column_break_yeds",
"fieldtype": "Column Break"
} }
], ],
"idx": 72, "idx": 72,
@@ -601,7 +624,7 @@
"link_fieldname": "target_asset" "link_fieldname": "target_asset"
} }
], ],
"modified": "2025-12-18 16:36:40.904246", "modified": "2026-02-05 12:42:45.350216",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@@ -56,6 +56,7 @@ class Asset(AccountsController):
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"] asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
asset_owner_company: DF.Link | None asset_owner_company: DF.Link | None
asset_quantity: DF.Int asset_quantity: DF.Int
asset_type: DF.Literal["", "Existing Asset", "Composite Asset", "Composite Component"]
available_for_use_date: DF.Date | None available_for_use_date: DF.Date | None
booked_fixed_asset: DF.Check booked_fixed_asset: DF.Check
calculate_depreciation: DF.Check calculate_depreciation: DF.Check
@@ -67,7 +68,9 @@ class Asset(AccountsController):
default_finance_book: DF.Link | None default_finance_book: DF.Link | None
department: DF.Link | None department: DF.Link | None
depr_entry_posting_status: DF.Literal["", "Successful", "Failed"] depr_entry_posting_status: DF.Literal["", "Successful", "Failed"]
depreciation_method: DF.Literal["", "Straight Line", "Double Declining Balance", "Manual"] depreciation_method: DF.Literal[
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"
]
disposal_date: DF.Date | None disposal_date: DF.Date | None
finance_books: DF.Table[AssetFinanceBook] finance_books: DF.Table[AssetFinanceBook]
frequency_of_depreciation: DF.Int frequency_of_depreciation: DF.Int
@@ -76,9 +79,6 @@ class Asset(AccountsController):
insurance_start_date: DF.Date | None insurance_start_date: DF.Date | None
insured_value: DF.Data | None insured_value: DF.Data | None
insurer: DF.Data | None insurer: DF.Data | None
is_composite_asset: DF.Check
is_composite_component: DF.Check
is_existing_asset: DF.Check
is_fully_depreciated: DF.Check is_fully_depreciated: DF.Check
item_code: DF.Link item_code: DF.Link
item_name: DF.ReadOnly | None item_name: DF.ReadOnly | None
@@ -243,14 +243,20 @@ class Asset(AccountsController):
self.set_total_booked_depreciations() self.set_total_booked_depreciations()
def before_submit(self): def before_submit(self):
if self.is_composite_asset and not has_active_capitalization(self.name): if self.asset_type == "Composite Asset" and not has_active_capitalization(self.name):
if self.split_from and has_active_capitalization(self.split_from):
return
frappe.throw(_("Please capitalize this asset before submitting.")) frappe.throw(_("Please capitalize this asset before submitting."))
def on_submit(self): def on_submit(self):
self.validate_in_use_date() self.validate_in_use_date()
self.make_asset_movement() self.make_asset_movement()
self.reload() self.reload()
if not self.booked_fixed_asset and not self.is_composite_component and self.validate_make_gl_entry(): if (
not self.booked_fixed_asset
and self.asset_type != "Composite Component"
and self.validate_make_gl_entry()
):
self.make_gl_entries() self.make_gl_entries()
if self.calculate_depreciation and not self.split_from: if self.calculate_depreciation and not self.split_from:
convert_draft_asset_depr_schedules_into_active(self) convert_draft_asset_depr_schedules_into_active(self)
@@ -265,7 +271,7 @@ class Asset(AccountsController):
cancel_asset_depr_schedules(self) cancel_asset_depr_schedules(self)
self.set_status() self.set_status()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
if not self.is_composite_component: if self.asset_type != "Composite Component":
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name) make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
self.db_set("booked_fixed_asset", 0) self.db_set("booked_fixed_asset", 0)
add_asset_activity(self.name, _("Asset cancelled")) add_asset_activity(self.name, _("Asset cancelled"))
@@ -283,7 +289,7 @@ class Asset(AccountsController):
add_asset_activity(self.name, _("Asset deleted")) add_asset_activity(self.name, _("Asset deleted"))
def set_purchase_doc_row_item(self): def set_purchase_doc_row_item(self):
if self.is_existing_asset or self.is_composite_asset: if self.asset_type == "Existing Asset" or self.asset_type == "Composite Asset":
return return
self.purchase_amount = self.net_purchase_amount self.purchase_amount = self.net_purchase_amount
@@ -326,7 +332,7 @@ class Asset(AccountsController):
) )
) )
if self.is_existing_asset and self.purchase_invoice: if self.asset_type == "Existing Asset" and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
def validate_item(self): def validate_item(self):
@@ -372,7 +378,7 @@ class Asset(AccountsController):
) )
def validate_in_use_date(self): def validate_in_use_date(self):
if not self.available_for_use_date and not self.is_composite_component: if not self.available_for_use_date and self.asset_type != "Composite Component":
frappe.throw(_("Available for use date is required")) frappe.throw(_("Available for use date is required"))
for d in self.finance_books: for d in self.finance_books:
@@ -420,12 +426,15 @@ class Asset(AccountsController):
non_depreciable_category = frappe.db.get_value( non_depreciable_category = frappe.db.get_value(
"Asset Category", self.asset_category, "non_depreciable_category" "Asset Category", self.asset_category, "non_depreciable_category"
) )
if self.calculate_depreciation and non_depreciable_category: if self.calculate_depreciation:
frappe.throw( if non_depreciable_category:
_( frappe.throw(
"This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category." _(
"This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category."
)
) )
) # validate accounts required for asset depreciation
get_depreciation_accounts(self.asset_category, self.company)
def validate_precision(self): def validate_precision(self):
if self.net_purchase_amount: if self.net_purchase_amount:
@@ -440,13 +449,13 @@ class Asset(AccountsController):
if not self.asset_category: if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
if not flt(self.net_purchase_amount) and not self.is_composite_asset: if not flt(self.net_purchase_amount) and self.asset_type != "Composite Asset":
frappe.throw(_("Net Purchase Amount is mandatory"), frappe.MandatoryError) frappe.throw(_("Net Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category): if is_cwip_accounting_enabled(self.asset_category):
if ( if (
not self.is_existing_asset not self.asset_type == "Existing Asset"
and not self.is_composite_asset and not self.asset_type == "Composite Asset"
and not self.purchase_receipt and not self.purchase_receipt
and not self.purchase_invoice and not self.purchase_invoice
): ):
@@ -475,7 +484,7 @@ class Asset(AccountsController):
if self.is_fully_depreciated: if self.is_fully_depreciated:
frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets")) frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
if self.is_existing_asset: if self.asset_type == "Existing Asset":
return return
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
@@ -547,7 +556,7 @@ class Asset(AccountsController):
) )
def validate_gross_and_purchase_amount(self): def validate_gross_and_purchase_amount(self):
if self.is_existing_asset: if self.asset_type == "Existing Asset":
return return
if self.net_purchase_amount and self.net_purchase_amount != self.purchase_amount: if self.net_purchase_amount and self.net_purchase_amount != self.purchase_amount:
@@ -613,7 +622,7 @@ class Asset(AccountsController):
self.validate_depreciation_start_date(row) self.validate_depreciation_start_date(row)
self.validate_total_number_of_depreciations_and_frequency(row) self.validate_total_number_of_depreciations_and_frequency(row)
if not self.is_existing_asset: if self.asset_type != "Existing Asset":
self.opening_accumulated_depreciation = 0 self.opening_accumulated_depreciation = 0
self.opening_number_of_booked_depreciations = 0 self.opening_number_of_booked_depreciations = 0
else: else:
@@ -766,7 +775,7 @@ class Asset(AccountsController):
def get_status(self): def get_status(self):
"""Returns status based on whether it is draft, submitted, scrapped or depreciated""" """Returns status based on whether it is draft, submitted, scrapped or depreciated"""
if self.docstatus == 0: if self.docstatus == 0:
if self.is_composite_asset: if self.asset_type == "Composite Asset":
status = "Work In Progress" status = "Work In Progress"
else: else:
status = "Draft" status = "Draft"
@@ -786,13 +795,12 @@ class Asset(AccountsController):
].expected_value_after_useful_life ].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation value_after_depreciation = self.finance_books[idx].value_after_depreciation
if ( if flt(value_after_depreciation) <= expected_value_after_useful_life:
flt(value_after_depreciation) <= expected_value_after_useful_life
or self.is_fully_depreciated
):
status = "Fully Depreciated" status = "Fully Depreciated"
elif flt(value_after_depreciation) < flt(self.net_purchase_amount): elif flt(value_after_depreciation) < flt(self.net_purchase_amount):
status = "Partially Depreciated" status = "Partially Depreciated"
elif self.is_fully_depreciated:
status = "Fully Depreciated"
elif self.docstatus == 2: elif self.docstatus == 2:
status = "Cancelled" status = "Cancelled"
return status return status
@@ -839,7 +847,7 @@ class Asset(AccountsController):
return records return records
def validate_make_gl_entry(self): def validate_make_gl_entry(self):
if self.is_composite_asset: if self.asset_type == "Composite Asset":
return True return True
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
@@ -920,7 +928,7 @@ class Asset(AccountsController):
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
if (self.is_composite_asset or (purchase_document and self.purchase_amount)) and getdate( if (self.asset_type == "Composite Asset" or (purchase_document and self.purchase_amount)) and getdate(
self.available_for_use_date self.available_for_use_date
) <= getdate(): ) <= getdate():
gl_entries.append( gl_entries.append(
@@ -960,7 +968,7 @@ class Asset(AccountsController):
self.db_set("booked_fixed_asset", 1) self.db_set("booked_fixed_asset", 1)
def check_asset_capitalization_gl_entries(self): def check_asset_capitalization_gl_entries(self):
if self.is_composite_asset: if self.asset_type == "Composite Asset":
result = frappe.db.get_value( result = frappe.db.get_value(
"Asset Capitalization", "Asset Capitalization",
{"target_asset": self.name, "docstatus": 1}, {"target_asset": self.name, "docstatus": 1},
@@ -1084,7 +1092,7 @@ def get_asset_naming_series():
@frappe.whitelist() @frappe.whitelist()
def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None): def make_sales_invoice(asset: str, item_code: str, company: str, sell_qty: int, serial_no: str | None = None):
asset_doc = frappe.get_doc("Asset", asset) asset_doc = frappe.get_doc("Asset", asset)
si = frappe.new_doc("Sales Invoice") si = frappe.new_doc("Sales Invoice")
si.company = company si.company = company
@@ -1117,7 +1125,13 @@ def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
@frappe.whitelist() @frappe.whitelist()
def create_asset_maintenance(asset, item_code, item_name, asset_category, company): def create_asset_maintenance(
asset: str,
item_code: str,
item_name: str,
asset_category: str,
company: str,
):
asset_maintenance = frappe.new_doc("Asset Maintenance") asset_maintenance = frappe.new_doc("Asset Maintenance")
asset_maintenance.update( asset_maintenance.update(
{ {
@@ -1132,14 +1146,23 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
@frappe.whitelist() @frappe.whitelist()
def create_asset_repair(company, asset, asset_name): def create_asset_repair(
company: str,
asset: str,
asset_name: str,
):
asset_repair = frappe.new_doc("Asset Repair") asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name}) asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
return asset_repair return asset_repair
@frappe.whitelist() @frappe.whitelist()
def create_asset_capitalization(company, asset, asset_name, item_code): def create_asset_capitalization(
company: str,
asset: str,
asset_name: str,
item_code: str,
):
asset_capitalization = frappe.new_doc("Asset Capitalization") asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update( asset_capitalization.update(
{ {
@@ -1153,35 +1176,22 @@ def create_asset_capitalization(company, asset, asset_name, item_code):
@frappe.whitelist() @frappe.whitelist()
def create_asset_value_adjustment(asset, asset_category, company): def create_asset_value_adjustment(
asset: str,
asset_category: str,
company: str,
):
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
asset_value_adjustment.update({"asset": asset, "company": company, "asset_category": asset_category}) asset_value_adjustment.update({"asset": asset, "company": company, "asset_category": asset_category})
return asset_value_adjustment return asset_value_adjustment
@frappe.whitelist() @frappe.whitelist()
def transfer_asset(args): def get_item_details(
args = json.loads(args) item_code: str,
asset_category: str,
if args.get("serial_no"): net_purchase_amount: float,
args["quantity"] = len(args.get("serial_no").split("\n")) ):
movement_entry = frappe.new_doc("Asset Movement")
movement_entry.update(args)
movement_entry.insert()
movement_entry.submit()
frappe.db.commit()
frappe.msgprint(
_("Asset Movement record {0} created")
.format("<a href='/app/Form/Asset Movement/{0}'>{0}</a>")
.format(movement_entry.name)
)
@frappe.whitelist()
def get_item_details(item_code, asset_category, net_purchase_amount):
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category) asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
books = [] books = []
for d in asset_category_doc.finance_books: for d in asset_category_doc.finance_books:
@@ -1231,7 +1241,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
@frappe.whitelist() @frappe.whitelist()
def make_journal_entry(asset_name): def make_journal_entry(asset_name: str):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
( (
fixed_asset_account, fixed_asset_account,
@@ -1273,7 +1283,10 @@ def make_journal_entry(asset_name):
@frappe.whitelist() @frappe.whitelist()
def make_asset_movement(assets, purpose=None): def make_asset_movement(
assets: list[dict] | str,
purpose: str = "Transfer",
):
import json import json
if isinstance(assets, str): if isinstance(assets, str):
@@ -1283,7 +1296,7 @@ def make_asset_movement(assets, purpose=None):
frappe.throw(_("At least one asset has to be selected.")) frappe.throw(_("At least one asset has to be selected."))
asset_movement = frappe.new_doc("Asset Movement") asset_movement = frappe.new_doc("Asset Movement")
asset_movement.quantity = len(assets) asset_movement.purpose = purpose
for asset in assets: for asset in assets:
asset = frappe.get_doc("Asset", asset.get("name")) asset = frappe.get_doc("Asset", asset.get("name"))
asset_movement.company = asset.get("company") asset_movement.company = asset.get("company")
@@ -1305,7 +1318,10 @@ def is_cwip_accounting_enabled(asset_category):
@frappe.whitelist() @frappe.whitelist()
def get_asset_value_after_depreciation(asset_name, finance_book=None): def get_asset_value_after_depreciation(
asset_name: str,
finance_book: str | None = None,
):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
if not asset.calculate_depreciation: if not asset.calculate_depreciation:
return flt(asset.value_after_depreciation) return flt(asset.value_after_depreciation)
@@ -1314,7 +1330,7 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
@frappe.whitelist() @frappe.whitelist()
def has_active_capitalization(asset): def has_active_capitalization(asset: str):
active_capitalizations = frappe.db.count( active_capitalizations = frappe.db.count(
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1} "Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
) )
@@ -1322,7 +1338,11 @@ def has_active_capitalization(asset):
@frappe.whitelist() @frappe.whitelist()
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype): def get_values_from_purchase_doc(
purchase_doc_name: str,
item_code: str,
doctype: str,
):
purchase_doc = frappe.get_doc(doctype, purchase_doc_name) purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
matching_items = [item for item in purchase_doc.items if item.item_code == item_code] matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
@@ -1344,7 +1364,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
@frappe.whitelist() @frappe.whitelist()
def split_asset(asset_name, split_qty): def split_asset(asset_name: str, split_qty: int):
"""Split an asset into two based on the given quantity.""" """Split an asset into two based on the given quantity."""
existing_asset = frappe.get_doc("Asset", asset_name) existing_asset = frappe.get_doc("Asset", asset_name)
split_qty = cint(split_qty) split_qty = cint(split_qty)
@@ -1391,7 +1411,7 @@ def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_a
def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset): def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
asset_doc.purchase_amount = existing_asset.net_purchase_amount asset_doc.purchase_amount = existing_asset.net_purchase_amount * scaling_factor
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
asset_doc.opening_accumulated_depreciation = ( asset_doc.opening_accumulated_depreciation = (

View File

@@ -7,6 +7,7 @@ from frappe import _
from frappe.query_builder import Order from frappe.query_builder import Order
from frappe.query_builder.functions import Max, Min from frappe.query_builder.functions import Max, Min
from frappe.utils import ( from frappe.utils import (
DateTimeLikeObject,
add_months, add_months,
cint, cint,
flt, flt,
@@ -161,11 +162,11 @@ def get_depr_cost_center_and_series():
@frappe.whitelist() @frappe.whitelist()
def make_depreciation_entry( def make_depreciation_entry(
depr_schedule_name, depr_schedule_name: str,
date=None, date: DateTimeLikeObject | None = None,
sch_start_idx=None, sch_start_idx: int | None = None,
sch_end_idx=None, sch_end_idx: int | None = None,
accounting_dimensions=None, accounting_dimensions: list[dict] | None = None,
): ):
frappe.has_permission("Journal Entry", throw=True) frappe.has_permission("Journal Entry", throw=True)
date = date or today() date = date or today()
@@ -246,7 +247,9 @@ def _make_journal_entry_for_depreciation(
def setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset): def setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset):
je.voucher_type = "Depreciation Entry" je.voucher_type = "Depreciation Entry"
je.naming_series = depr_series if depr_series:
je.naming_series = depr_series
je.posting_date = depr_schedule.schedule_date je.posting_date = depr_schedule.schedule_date
je.company = asset.company je.company = asset.company
je.finance_book = depr_schedule_doc.finance_book je.finance_book = depr_schedule_doc.finance_book
@@ -354,7 +357,7 @@ def get_message_for_depr_entry_posting_error(asset_links, error_log_links):
@frappe.whitelist() @frappe.whitelist()
def scrap_asset(asset_name, scrap_date=None): def scrap_asset(asset_name: str, scrap_date: DateTimeLikeObject | None = None):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
scrap_date = getdate(scrap_date) or getdate(today()) scrap_date = getdate(scrap_date) or getdate(today())
asset.db_set("disposal_date", scrap_date) asset.db_set("disposal_date", scrap_date)
@@ -443,7 +446,7 @@ def create_journal_entry_for_scrap(asset, scrap_date):
@frappe.whitelist() @frappe.whitelist()
def restore_asset(asset_name): def restore_asset(asset_name: str):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_on_disposal(asset) reverse_depreciation_entry_made_on_disposal(asset)
reset_depreciation_schedule(asset, get_note_for_restore(asset)) reset_depreciation_schedule(asset, get_note_for_restore(asset))
@@ -770,7 +773,7 @@ def get_profit_gl_entries(
@frappe.whitelist() @frappe.whitelist()
def get_disposal_account_and_cost_center(company): def get_disposal_account_and_cost_center(company: str):
disposal_account, depreciation_cost_center = frappe.get_cached_value( disposal_account, depreciation_cost_center = frappe.get_cached_value(
"Company", company, ["disposal_account", "depreciation_cost_center"] "Company", company, ["disposal_account", "depreciation_cost_center"]
) )
@@ -784,10 +787,14 @@ def get_disposal_account_and_cost_center(company):
@frappe.whitelist() @frappe.whitelist()
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None): def get_value_after_depreciation_on_disposal_date(
asset: str,
disposal_date: DateTimeLikeObject,
finance_book: str | None = None,
):
asset_doc = frappe.get_doc("Asset", asset) asset_doc = frappe.get_doc("Asset", asset)
if asset_doc.is_composite_component: if asset_doc.asset_type == "Composite Component":
validate_disposal_date(asset_doc.purchase_date, getdate(disposal_date), "purchase") validate_disposal_date(asset_doc.purchase_date, getdate(disposal_date), "purchase")
return flt(asset_doc.value_after_depreciation) return flt(asset_doc.value_after_depreciation)

View File

@@ -70,16 +70,16 @@ class TestAsset(AssetSetup):
self.assertRaises(frappe.MandatoryError, asset.save) self.assertRaises(frappe.MandatoryError, asset.save)
def test_pr_or_pi_mandatory_if_not_existing_asset(self): def test_pr_or_pi_mandatory_if_not_existing_asset(self):
"""Tests if either PI or PR is present if CWIP is enabled and is_existing_asset=0.""" """Tests if either PI or PR is present if CWIP is enabled and asset_type == Existing Asset."""
asset = create_asset(item_code="Macbook Pro", do_not_save=1) asset = create_asset(item_code="Macbook Pro", do_not_save=1)
asset.is_existing_asset = 0 asset.asset_type = ""
self.assertRaises(frappe.ValidationError, asset.save) self.assertRaises(frappe.ValidationError, asset.save)
def test_available_for_use_date_is_after_purchase_date(self): def test_available_for_use_date_is_after_purchase_date(self):
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, do_not_save=1) asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, do_not_save=1)
asset.is_existing_asset = 0 asset.asset_type = ""
asset.purchase_date = getdate("2021-10-10") asset.purchase_date = getdate("2021-10-10")
asset.available_for_use_date = getdate("2021-10-1") asset.available_for_use_date = getdate("2021-10-1")
@@ -182,7 +182,7 @@ class TestAsset(AssetSetup):
asset.submit() asset.submit()
def test_is_fixed_asset_set(self): def test_is_fixed_asset_set(self):
asset = create_asset(is_existing_asset=1) asset = create_asset(asset_type="Existing Asset")
doc = frappe.new_doc("Purchase Invoice") doc = frappe.new_doc("Purchase Invoice")
doc.company = "_Test Company" doc.company = "_Test Company"
doc.supplier = "_Test Supplier" doc.supplier = "_Test Supplier"
@@ -709,7 +709,7 @@ class TestAsset(AssetSetup):
# create an asset # create an asset
asset = create_asset( asset = create_asset(
item_code="Macbook Pro", item_code="Macbook Pro",
is_existing_asset=1, asset_type="Existing Asset",
calculate_depreciation=1, calculate_depreciation=1,
available_for_use_date=purchase_date, available_for_use_date=purchase_date,
purchase_date=purchase_date, purchase_date=purchase_date,
@@ -823,6 +823,92 @@ class TestAsset(AssetSetup):
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 0) frappe.db.set_value("Item", asset_item, "is_grouped_asset", 0)
def test_is_fully_depreciated_asset_status(self):
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
asset.is_fully_depreciated = 1
asset.save().submit()
self.assertEqual(asset.status, "Fully Depreciated")
def test_depreciation_accounts_is_set_for_depreciable_assets(self):
company_depreciation_accounts = frappe.db.get_value(
"Company",
"_Test Company",
[
"accumulated_depreciation_account",
"depreciation_expense_account",
],
as_dict=True,
)
frappe.db.set_value(
"Company",
"_Test Company",
{
"accumulated_depreciation_account": "",
"depreciation_expense_account": "",
},
)
asset_category_name = "Computers"
asset_category_account = None
if frappe.db.exists("Asset Category", asset_category_name):
filters = {
"parent": asset_category_name,
"company_name": "_Test Company",
}
fieldname = [
"name",
"accumulated_depreciation_account",
"depreciation_expense_account",
]
asset_category_account = frappe.db.get_value(
"Asset Category Account",
filters=filters,
fieldname=fieldname,
as_dict=True,
)
if asset_category_account and (
asset_category_account.accumulated_depreciation_account
or asset_category_account.depreciation_expense_account
):
frappe.db.set_value(
"Asset Category Account",
asset_category_account.name,
{
"accumulated_depreciation_account": "",
"depreciation_expense_account": "",
},
)
else:
asset_category = frappe.new_doc("Asset Category")
asset_category.asset_category_name = asset_category_name
asset_category.append(
"accounts",
{
"company_name": "_Test Company",
"fixed_asset_account": "_Test Fixed Asset - _TC",
},
)
asset_category.insert()
try:
asset = create_asset(asset_category=asset_category_name, calculate_depreciation=1, do_not_save=1)
with self.assertRaises(frappe.ValidationError) as err:
asset.save()
self.assertTrue(
"Please set Depreciation related Accounts in Asset Category Computers or Company"
in str(err.exception)
)
finally:
frappe.db.set_value("Company", "_Test Company", company_depreciation_accounts)
if asset_category_account:
frappe.db.set_value(
"Asset Category Account",
asset_category_account.name,
{
"accumulated_depreciation_account": asset_category_account.accumulated_depreciation_account,
"depreciation_expense_account": asset_category_account.depreciation_expense_account,
},
)
class TestDepreciationMethods(AssetSetup): class TestDepreciationMethods(AssetSetup):
@classmethod @classmethod
@@ -901,7 +987,7 @@ class TestDepreciationMethods(AssetSetup):
asset = create_asset( asset = create_asset(
calculate_depreciation=1, calculate_depreciation=1,
available_for_use_date="2030-06-06", available_for_use_date="2030-06-06",
is_existing_asset=1, asset_type="Existing Asset",
opening_number_of_booked_depreciations=2, opening_number_of_booked_depreciations=2,
opening_accumulated_depreciation=47178.08, opening_accumulated_depreciation=47178.08,
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
@@ -950,7 +1036,7 @@ class TestDepreciationMethods(AssetSetup):
asset = create_asset( asset = create_asset(
calculate_depreciation=1, calculate_depreciation=1,
available_for_use_date="2030-01-01", available_for_use_date="2030-01-01",
is_existing_asset=1, asset_type="Existing Asset",
depreciation_method="Double Declining Balance", depreciation_method="Double Declining Balance",
opening_number_of_booked_depreciations=1, opening_number_of_booked_depreciations=1,
opening_accumulated_depreciation=50000, opening_accumulated_depreciation=50000,
@@ -1691,7 +1777,7 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(asset.finance_books[0].value_after_depreciation, 100000.0) self.assertEqual(asset.finance_books[0].value_after_depreciation, 100000.0)
def test_asset_cost_center(self): def test_asset_cost_center(self):
asset = create_asset(is_existing_asset=1, do_not_save=1) asset = create_asset(asset_type="Existing Asset", do_not_save=1)
asset.cost_center = "Main - WP" asset.cost_center = "Main - WP"
self.assertRaises(frappe.ValidationError, asset.submit) self.assertRaises(frappe.ValidationError, asset.submit)
@@ -1728,7 +1814,7 @@ class TestDepreciationBasics(AssetSetup):
def test_manual_depreciation_for_existing_asset(self): def test_manual_depreciation_for_existing_asset(self):
asset = create_asset( asset = create_asset(
item_code="Macbook Pro", item_code="Macbook Pro",
is_existing_asset=1, asset_type="Existing Asset",
purchase_date="2020-01-30", purchase_date="2020-01-30",
available_for_use_date="2020-01-30", available_for_use_date="2020-01-30",
submit=1, submit=1,
@@ -1828,6 +1914,71 @@ class TestDepreciationBasics(AssetSetup):
pr.submit() pr.submit()
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
def test_split_asset_created_via_capitalization(self):
"""Test that assets created via Asset Capitalization can be split without capitalization error"""
from erpnext.assets.doctype.asset_capitalization.test_asset_capitalization import (
create_asset_capitalization,
create_asset_capitalization_data,
)
# Ensure test data exists
create_asset_capitalization_data()
company = "_Test Company with perpetual inventory"
set_depreciation_settings_in_company(company=company)
name = frappe.db.get_value(
"Asset Category Account",
filters={"parent": "Computers", "company_name": company},
fieldname=["name"],
)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
stock_rate = 1000
stock_qty = 2
total_amount = 2000
# Create composite asset
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset for Split",
asset_type="Composite Asset",
warehouse="Stores - TCP1",
company=company,
asset_quantity=2, # Set quantity > 1 to allow splitting
)
# Create and submit Asset Capitalization
asset_capitalization = create_asset_capitalization(
target_asset=wip_composite_asset.name,
stock_qty=stock_qty,
stock_rate=stock_rate,
company=company,
submit=1,
)
# Verify asset was capitalized
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.net_purchase_amount, total_amount)
self.assertEqual(target_asset.status, "Work In Progress")
# Submit the capitalized asset
target_asset.submit()
self.assertEqual(target_asset.status, "Submitted")
# Split the asset - this should work without capitalization error
split_qty = 1
splitted_asset = split_asset(target_asset.name, split_qty)
# Verify split asset was created and submitted successfully
self.assertIsNotNone(splitted_asset)
self.assertEqual(splitted_asset.asset_quantity, split_qty)
self.assertEqual(splitted_asset.split_from, target_asset.name)
self.assertEqual(splitted_asset.docstatus, 1) # Should be submitted
self.assertEqual(splitted_asset.status, "Submitted")
# Verify original asset was updated
target_asset.reload()
self.assertEqual(target_asset.asset_quantity, 1) # Remaining quantity
def get_gl_entries(doctype, docname): def get_gl_entries(doctype, docname):
gl_entry = frappe.qb.DocType("GL Entry") gl_entry = frappe.qb.DocType("GL Entry")
@@ -1883,9 +2034,7 @@ def create_asset(**args):
"available_for_use_date": args.available_for_use_date or "2020-06-06", "available_for_use_date": args.available_for_use_date or "2020-06-06",
"location": args.location or "Test Location", "location": args.location or "Test Location",
"asset_owner": args.asset_owner or "Company", "asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1, "asset_type": args.asset_type or "Existing Asset",
"is_composite_asset": args.is_composite_asset or 0,
"is_composite_component": args.is_composite_component or 0,
"asset_quantity": args.get("asset_quantity") or 1, "asset_quantity": args.get("asset_quantity") or 1,
"depr_entry_posting_status": args.depr_entry_posting_status or "", "depr_entry_posting_status": args.depr_entry_posting_status or "",
} }
@@ -1907,7 +2056,7 @@ def create_asset(**args):
}, },
) )
if asset.is_composite_asset: if asset.asset_type == "Composite Asset":
asset.net_purchase_amount = 0 asset.net_purchase_amount = 0
asset.purchase_amount = 0 asset.purchase_amount = 0

View File

@@ -17,10 +17,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
refresh() { refresh() {
this.show_general_ledger(); this.show_general_ledger();
if ( if (this.frm.doc.stock_items && this.frm.doc.stock_items.length) {
(this.frm.doc.stock_items && this.frm.doc.stock_items.length) ||
!this.frm.doc.target_is_fixed_asset
) {
this.show_stock_ledger(); this.show_stock_ledger();
} }
@@ -41,7 +38,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
me.frm.set_query("target_asset", function () { me.frm.set_query("target_asset", function () {
return { return {
filters: { is_composite_asset: 1, docstatus: 0 }, filters: { asset_type: "Composite Asset", docstatus: 0 },
}; };
}); });
@@ -240,10 +237,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
this.calculate_totals(); this.calculate_totals();
} }
target_qty() {
this.calculate_totals();
}
rate() { rate() {
this.calculate_totals(); this.calculate_totals();
} }
@@ -403,7 +396,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details", method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details",
child: item, child: item,
args: { args: {
args: { ctx: {
item_code: item.item_code, item_code: item.item_code,
warehouse: cstr(item.warehouse), warehouse: cstr(item.warehouse),
qty: -1 * flt(item.stock_qty), qty: -1 * flt(item.stock_qty),
@@ -485,10 +478,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total; me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total;
me.frm.doc.total_value = flt(me.frm.doc.total_value, precision("total_value")); me.frm.doc.total_value = flt(me.frm.doc.total_value, precision("total_value"));
me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision("target_qty")); me.frm.doc.target_incoming_rate = me.frm.doc.total_value;
me.frm.doc.target_incoming_rate = me.frm.doc.target_qty
? me.frm.doc.total_value / flt(me.frm.doc.target_qty)
: me.frm.doc.total_value;
me.frm.refresh_fields(); me.frm.refresh_fields();
} }

View File

@@ -9,30 +9,33 @@
"field_order": [ "field_order": [
"title", "title",
"naming_series", "naming_series",
"company",
"target_asset", "target_asset",
"target_asset_name", "target_asset_name",
"target_item_code",
"finance_book",
"target_qty",
"column_break_9", "column_break_9",
"company", "finance_book",
"posting_date", "posting_date",
"posting_time", "posting_time",
"set_posting_time", "set_posting_time",
"target_batch_no", "target_item_code",
"target_serial_no",
"amended_from", "amended_from",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"section_break_16", "section_break_16",
"stock_items", "stock_items",
"section_break_urtz",
"column_break_gqep",
"column_break_yvlx",
"stock_items_total", "stock_items_total",
"section_break_26", "section_break_26",
"asset_items", "asset_items",
"section_break_arbh",
"column_break_boeu",
"column_break_qecy",
"asset_items_total", "asset_items_total",
"service_expenses_section", "service_expenses_section",
"service_items", "service_items",
"section_break_ptna",
"column_break_szvh",
"column_break_katv",
"service_items_total", "service_items_total",
"totals_section", "totals_section",
"total_value", "total_value",
@@ -55,20 +58,12 @@
"depends_on": "eval:(doc.target_item_code && !doc.__islocal)", "depends_on": "eval:(doc.target_item_code && !doc.__islocal)",
"fieldname": "target_item_code", "fieldname": "target_item_code",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Target Item Code", "label": "Target Item Code",
"options": "Item", "options": "Item",
"read_only": 1 "read_only": 1
}, },
{
"default": "0",
"fetch_from": "target_item_code.is_fixed_asset",
"fieldname": "target_is_fixed_asset",
"fieldtype": "Check",
"hidden": 1,
"label": "Target Is Fixed Asset",
"read_only": 1
},
{ {
"fieldname": "target_asset", "fieldname": "target_asset",
"fieldtype": "Link", "fieldtype": "Link",
@@ -143,6 +138,7 @@
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)", "depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
"fieldname": "section_break_16", "fieldname": "section_break_16",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"label": "Consumed Stock Items" "label": "Consumed Stock Items"
}, },
{ {
@@ -151,49 +147,11 @@
"label": "Stock Items", "label": "Stock Items",
"options": "Asset Capitalization Stock Item" "options": "Asset Capitalization Stock Item"
}, },
{
"depends_on": "target_has_batch_no",
"fieldname": "target_batch_no",
"fieldtype": "Link",
"label": "Target Batch No",
"options": "Batch"
},
{
"default": "1",
"fieldname": "target_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Target Qty",
"read_only": 1
},
{
"default": "0",
"fetch_from": "target_item_code.has_batch_no",
"fieldname": "target_has_batch_no",
"fieldtype": "Check",
"hidden": 1,
"label": "Target Has Batch No",
"read_only": 1
},
{
"default": "0",
"fetch_from": "target_item_code.has_serial_no",
"fieldname": "target_has_serial_no",
"fieldtype": "Check",
"hidden": 1,
"label": "Target Has Serial No",
"read_only": 1
},
{
"depends_on": "target_has_serial_no",
"fieldname": "target_serial_no",
"fieldtype": "Small Text",
"label": "Target Serial No"
},
{ {
"depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)", "depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)",
"fieldname": "section_break_26", "fieldname": "section_break_26",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"label": "Consumed Assets" "label": "Consumed Assets"
}, },
{ {
@@ -203,6 +161,7 @@
"options": "Asset Capitalization Asset Item" "options": "Asset Capitalization Asset Item"
}, },
{ {
"depends_on": "eval: doc.stock_items_total",
"fieldname": "stock_items_total", "fieldname": "stock_items_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Consumed Stock Total Value", "label": "Consumed Stock Total Value",
@@ -210,6 +169,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: doc.asset_items_total",
"fieldname": "asset_items_total", "fieldname": "asset_items_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Consumed Asset Total Value", "label": "Consumed Asset Total Value",
@@ -226,6 +186,7 @@
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)", "depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
"fieldname": "service_expenses_section", "fieldname": "service_expenses_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1,
"label": "Service Expenses" "label": "Service Expenses"
}, },
{ {
@@ -235,6 +196,7 @@
"options": "Asset Capitalization Service Item" "options": "Asset Capitalization Service Item"
}, },
{ {
"depends_on": "eval: doc.service_items_total",
"fieldname": "service_items_total", "fieldname": "service_items_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Service Expense Total Amount", "label": "Service Expense Total Amount",
@@ -277,10 +239,10 @@
"options": "Cost Center" "options": "Cost Center"
}, },
{ {
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "Project" "options": "Project"
}, },
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
@@ -292,12 +254,48 @@
"label": "Target Fixed Asset Account", "label": "Target Fixed Asset Account",
"options": "Account", "options": "Account",
"read_only": 1 "read_only": 1
},
{
"fieldname": "section_break_urtz",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_gqep",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_yvlx",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_arbh",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_boeu",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_qecy",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ptna",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_szvh",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_katv",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2025-05-20 15:15:12.110035", "modified": "2026-02-06 01:52:41.890713",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Capitalization", "name": "Asset Capitalization",

View File

@@ -2,6 +2,7 @@
# For license information, please see license.txt # For license information, please see license.txt
import json import json
from typing import Any
import frappe import frappe
@@ -38,9 +39,6 @@ force_fields = [
"target_asset_name", "target_asset_name",
"item_name", "item_name",
"asset_name", "asset_name",
"target_is_fixed_asset",
"target_has_serial_no",
"target_has_batch_no",
"stock_uom", "stock_uom",
"fixed_asset_account", "fixed_asset_account",
"valuation_rate", "valuation_rate",
@@ -75,6 +73,7 @@ class AssetCapitalization(StockController):
naming_series: DF.Literal["ACC-ASC-.YYYY.-"] naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
posting_date: DF.Date posting_date: DF.Date
posting_time: DF.Time posting_time: DF.Time
project: DF.Link | None
service_items: DF.Table[AssetCapitalizationServiceItem] service_items: DF.Table[AssetCapitalizationServiceItem]
service_items_total: DF.Currency service_items_total: DF.Currency
set_posting_time: DF.Check set_posting_time: DF.Check
@@ -82,15 +81,9 @@ class AssetCapitalization(StockController):
stock_items_total: DF.Currency stock_items_total: DF.Currency
target_asset: DF.Link | None target_asset: DF.Link | None
target_asset_name: DF.Data | None target_asset_name: DF.Data | None
target_batch_no: DF.Link | None
target_fixed_asset_account: DF.Link | None target_fixed_asset_account: DF.Link | None
target_has_batch_no: DF.Check
target_has_serial_no: DF.Check
target_incoming_rate: DF.Currency target_incoming_rate: DF.Currency
target_is_fixed_asset: DF.Check
target_item_code: DF.Link | None target_item_code: DF.Link | None
target_qty: DF.Float
target_serial_no: DF.SmallText | None
title: DF.Data | None title: DF.Data | None
total_value: DF.Currency total_value: DF.Currency
# end: auto-generated types # end: auto-generated types
@@ -189,22 +182,13 @@ class AssetCapitalization(StockController):
if not target_item.is_fixed_asset: if not target_item.is_fixed_asset:
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name)) frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
if target_item.is_fixed_asset:
self.target_qty = 1
if flt(self.target_qty) <= 0:
frappe.throw(_("Target Qty must be a positive number"))
if not target_item.has_batch_no:
self.target_batch_no = None
if not target_item.has_serial_no:
self.target_serial_no = ""
self.validate_item(target_item) self.validate_item(target_item)
def validate_target_asset(self): def validate_target_asset(self):
if self.target_asset: if self.target_asset:
target_asset = self.get_asset_for_validation(self.target_asset) target_asset = self.get_asset_for_validation(self.target_asset)
if not target_asset.is_composite_asset: if not target_asset.asset_type == "Composite Asset":
frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name)) frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name))
if target_asset.item_code != self.target_item_code: if target_asset.item_code != self.target_item_code:
@@ -313,7 +297,7 @@ class AssetCapitalization(StockController):
return frappe.db.get_value( return frappe.db.get_value(
"Asset", "Asset",
asset, asset,
["name", "item_code", "company", "status", "docstatus", "is_composite_asset"], ["name", "item_code", "company", "status", "docstatus", "asset_type"],
as_dict=1, as_dict=1,
) )
@@ -379,8 +363,7 @@ class AssetCapitalization(StockController):
self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total
self.total_value = flt(self.total_value, self.precision("total_value")) self.total_value = flt(self.total_value, self.precision("total_value"))
self.target_qty = flt(self.target_qty, self.precision("target_qty")) self.target_incoming_rate = self.total_value
self.target_incoming_rate = self.total_value / self.target_qty
def update_stock_ledger(self): def update_stock_ledger(self):
sl_entries = [] sl_entries = []
@@ -488,7 +471,7 @@ class AssetCapitalization(StockController):
for item in self.asset_items: for item in self.asset_items:
asset = frappe.get_doc("Asset", item.asset) asset = frappe.get_doc("Asset", item.asset)
if not asset.is_composite_component: if asset.asset_type != "Composite Component":
if asset.calculate_depreciation: if asset.calculate_depreciation:
notes = _( notes = _(
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}." "This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
@@ -541,30 +524,29 @@ class AssetCapitalization(StockController):
def get_composite_component_value(self): def get_composite_component_value(self):
composite_component_value = 0 composite_component_value = 0
for item in self.asset_items: for item in self.asset_items:
asset = frappe.db.get_value("Asset", item.asset, ["is_composite_component"], as_dict=True) asset = frappe.db.get_value("Asset", item.asset, ["asset_type"], as_dict=True)
if asset and asset.is_composite_component: if asset and asset.asset_type == "Composite Component":
composite_component_value += flt(item.asset_value, item.precision("asset_value")) composite_component_value += flt(item.asset_value, item.precision("asset_value"))
return composite_component_value return composite_component_value
def get_gl_entries_for_target_item( def get_gl_entries_for_target_item(
self, gl_entries, target_account, target_against, precision, composite_component_value self, gl_entries, target_account, target_against, precision, composite_component_value
): ):
if self.target_is_fixed_asset: total_value = flt(self.total_value - composite_component_value, precision)
total_value = flt(self.total_value - composite_component_value, precision) if total_value:
if total_value: # Capitalization
# Capitalization gl_entries.append(
gl_entries.append( self.get_gl_dict(
self.get_gl_dict( {
{ "account": target_account,
"account": target_account, "against": ", ".join(target_against),
"against": ", ".join(target_against), "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"remarks": self.get("remarks") or _("Accounting Entry for Asset"), "debit": total_value,
"debit": total_value, "cost_center": self.get("cost_center"),
"cost_center": self.get("cost_center"), },
}, item=self,
item=self,
)
) )
)
def update_target_asset(self): def update_target_asset(self):
total_target_asset_value = flt(self.total_value, self.precision("total_value")) total_target_asset_value = flt(self.total_value, self.precision("total_value"))
@@ -573,13 +555,19 @@ class AssetCapitalization(StockController):
if self.docstatus == 2: if self.docstatus == 2:
net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value
purchase_amount = asset_doc.purchase_amount - total_target_asset_value purchase_amount = asset_doc.purchase_amount - total_target_asset_value
asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value) total_asset_cost = asset_doc.total_asset_cost - total_target_asset_value
else: else:
net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value
purchase_amount = asset_doc.purchase_amount + total_target_asset_value purchase_amount = asset_doc.purchase_amount + total_target_asset_value
total_asset_cost = asset_doc.total_asset_cost + total_target_asset_value
asset_doc.db_set("net_purchase_amount", net_purchase_amount) asset_doc.db_set(
asset_doc.db_set("purchase_amount", purchase_amount) {
"net_purchase_amount": net_purchase_amount,
"purchase_amount": purchase_amount,
"total_asset_cost": total_asset_cost,
}
)
frappe.msgprint( frappe.msgprint(
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format( _("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(
@@ -604,14 +592,13 @@ class AssetCapitalization(StockController):
def set_consumed_asset_status(self, asset): def set_consumed_asset_status(self, asset):
if self.docstatus == 1: if self.docstatus == 1:
if self.target_is_fixed_asset: asset.set_status("Capitalized")
asset.set_status("Capitalized") add_asset_activity(
add_asset_activity( asset.name,
asset.name, _("Asset capitalized after Asset Capitalization {0} was submitted").format(
_("Asset capitalized after Asset Capitalization {0} was submitted").format( get_link_to_form("Asset Capitalization", self.name)
get_link_to_form("Asset Capitalization", self.name) ),
), )
)
else: else:
asset.set_status() asset.set_status()
add_asset_activity( add_asset_activity(
@@ -623,7 +610,7 @@ class AssetCapitalization(StockController):
@frappe.whitelist() @frappe.whitelist()
def get_target_item_details(item_code=None, company=None): def get_target_item_details(item_code: str | None = None, company: str | None = None):
out = frappe._dict() out = frappe._dict()
# Get Item Details # Get Item Details
@@ -633,17 +620,6 @@ def get_target_item_details(item_code=None, company=None):
# Set Item Details # Set Item Details
out.target_item_name = item.item_name out.target_item_name = item.item_name
out.target_is_fixed_asset = cint(item.is_fixed_asset)
out.target_has_batch_no = cint(item.has_batch_no)
out.target_has_serial_no = cint(item.has_serial_no)
if out.target_is_fixed_asset:
out.target_qty = 1
if not out.target_has_batch_no:
out.target_batch_no = None
if not out.target_has_serial_no:
out.target_serial_no = ""
# Cost Center # Cost Center
item_defaults = get_item_defaults(item.name, company) item_defaults = get_item_defaults(item.name, company)
@@ -660,7 +636,7 @@ def get_target_item_details(item_code=None, company=None):
@frappe.whitelist() @frappe.whitelist()
def get_target_asset_details(asset=None, company=None): def get_target_asset_details(asset: str | None = None, company: str | None = None):
out = frappe._dict() out = frappe._dict()
# Get Asset Details # Get Asset Details
@@ -735,24 +711,22 @@ def get_consumed_stock_item_details(ctx: ItemDetailsCtx):
@frappe.whitelist() @frappe.whitelist()
def get_warehouse_details(args): @erpnext.normalize_ctx_input(ItemDetailsCtx)
if isinstance(args, str): def get_warehouse_details(ctx: ItemDetailsCtx) -> frappe._dict:
args = json.loads(args) out = frappe._dict()
if ctx.warehouse and ctx.item_code:
args = frappe._dict(args) out = frappe._dict(
{
out = {} "actual_qty": get_previous_sle(ctx).get("qty_after_transaction") or 0,
if args.warehouse and args.item_code: "valuation_rate": get_incoming_rate(ctx, raise_error_if_no_rate=False),
out = { }
"actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0, )
"valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False),
}
return out return out
@frappe.whitelist() @frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx) @erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_consumed_asset_details(ctx): def get_consumed_asset_details(ctx: ItemDetailsCtx) -> frappe._dict:
out = frappe._dict() out = frappe._dict()
asset_details = frappe._dict() asset_details = frappe._dict()
@@ -798,7 +772,7 @@ def get_consumed_asset_details(ctx):
@frappe.whitelist() @frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx) @erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_service_item_details(ctx): def get_service_item_details(ctx: ItemDetailsCtx) -> frappe._dict:
out = frappe._dict() out = frappe._dict()
item = frappe._dict() item = frappe._dict()
@@ -820,7 +794,7 @@ def get_service_item_details(ctx):
@frappe.whitelist() @frappe.whitelist()
def get_items_tagged_to_wip_composite_asset(params): def get_items_tagged_to_wip_composite_asset(params: dict | str):
if isinstance(params, str): if isinstance(params, str):
params = json.loads(params) params = json.loads(params)

View File

@@ -9,9 +9,11 @@ from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import ( from erpnext.assets.doctype.asset.test_asset import (
create_asset, create_asset,
create_asset_data, create_asset_data,
create_fixed_asset_item,
set_depreciation_settings_in_company, set_depreciation_settings_in_company,
) )
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
make_serial_batch_bundle, make_serial_batch_bundle,
) )
@@ -57,7 +59,7 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset( wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset", asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1, asset_type="Composite Asset",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
company=company, company=company,
) )
@@ -77,7 +79,6 @@ class TestAssetCapitalization(IntegrationTestCase):
) )
# Test Asset Capitalization values # Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
@@ -152,7 +153,7 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset( wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset", asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1, asset_type="Composite Asset",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
company=company, company=company,
) )
@@ -172,8 +173,6 @@ class TestAssetCapitalization(IntegrationTestCase):
) )
# Test Asset Capitalization values # Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
self.assertEqual(asset_capitalization.stock_items_total, stock_amount) self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
@@ -241,7 +240,7 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset( wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset", asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1, asset_type="Composite Asset",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
company=company, company=company,
) )
@@ -258,8 +257,6 @@ class TestAssetCapitalization(IntegrationTestCase):
) )
# Test Asset Capitalization values # Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
self.assertEqual(asset_capitalization.stock_items_total, stock_amount) self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
@@ -309,7 +306,7 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset( wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset", asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1, asset_type="Composite Asset",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
company=company, company=company,
) )
@@ -357,33 +354,45 @@ class TestAssetCapitalization(IntegrationTestCase):
wip_composite_asset = create_asset( wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset", asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1, asset_type="Composite Asset",
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
company=company, company=company,
) )
consumed_asset_value = 100000 consumed_asset_value = 100000
consumed_asset = create_asset( item = create_fixed_asset_item("Asset Capitalization Consumable Asset")
asset_name="Asset Capitalization Consumable Asset",
asset_value=consumed_asset_value, pr = make_purchase_receipt(
submit=1, item_code=item.item_code,
warehouse="Stores - _TC", qty=1,
is_composite_component=1, rate=consumed_asset_value,
company=company, company=company,
warehouse="Stores - TCP1",
) )
consumed_asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
consumed_asset_doc = frappe.get_doc("Asset", consumed_asset_name)
consumed_asset_doc.update(
{
"asset_type": "Composite Component",
"purchase_date": pr.posting_date,
"available_for_use_date": pr.posting_date,
}
)
consumed_asset_doc.save()
consumed_asset_doc.submit()
# Create and submit Asset Captitalization # Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization( asset_capitalization = create_asset_capitalization(
target_asset=wip_composite_asset.name, target_asset=wip_composite_asset.name,
target_asset_location="Test Location", target_asset_location="Test Location",
consumed_asset=consumed_asset.name, consumed_asset=consumed_asset_doc.name,
company=company, company=company,
submit=1, submit=1,
) )
# Test Asset Capitalization values # Test Asset Capitalization values
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value) self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
actual_gle = get_actual_gle_dict(asset_capitalization.name) actual_gle = get_actual_gle_dict(asset_capitalization.name)
@@ -417,9 +426,6 @@ def create_asset_capitalization(**args):
"target_item_code": target_item_code, "target_item_code": target_item_code,
"target_asset": target_asset.name, "target_asset": target_asset.name,
"target_asset_location": "Test Location", "target_asset_location": "Test Location",
"target_qty": flt(args.target_qty) or 1,
"target_batch_no": args.target_batch_no,
"target_serial_no": args.target_serial_no,
"finance_book": args.finance_book, "finance_book": args.finance_book,
} }
) )
@@ -512,7 +518,7 @@ def create_depreciation_asset(**args):
args = frappe._dict(args) args = frappe._dict(args)
asset = frappe.new_doc("Asset") asset = frappe.new_doc("Asset")
asset.is_existing_asset = 1 asset.asset_type = args.asset_type or "Existing Asset"
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.asset_owner = "Company" asset.asset_owner = "Company"

View File

@@ -31,7 +31,7 @@ class AssetCategory(Document):
self.validate_finance_books() self.validate_finance_books()
self.validate_account_types() self.validate_account_types()
self.validate_account_currency() self.validate_account_currency()
self.valide_cwip_account() self.validate_accounts()
def validate_finance_books(self): def validate_finance_books(self):
for d in self.finance_books: for d in self.finance_books:
@@ -97,11 +97,21 @@ class AssetCategory(Document):
title=_("Invalid Account"), title=_("Invalid Account"),
) )
def valide_cwip_account(self): def validate_accounts(self):
self.validate_duplicate_rows()
self.validate_cwip_accounts()
self.validate_depreciation_accounts()
def validate_duplicate_rows(self):
companies = {row.company_name for row in self.accounts}
if len(companies) != len(self.accounts):
frappe.throw(_("Cannot set multiple account rows for the same company"))
def validate_cwip_accounts(self):
if self.enable_cwip_accounting: if self.enable_cwip_accounting:
missing_cwip_accounts_for_company = [] missing_cwip_accounts_for_company = []
for d in self.accounts: for d in self.accounts:
if not d.capital_work_in_progress_account and not frappe.db.get_value( if not d.capital_work_in_progress_account and not frappe.get_cached_value(
"Company", d.company_name, "capital_work_in_progress_account" "Company", d.company_name, "capital_work_in_progress_account"
): ):
missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name)) missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
@@ -115,6 +125,71 @@ class AssetCategory(Document):
) )
frappe.throw(msg, title=_("Missing Account")) frappe.throw(msg, title=_("Missing Account"))
def validate_depreciation_accounts(self):
depreciation_account_map = {
"accumulated_depreciation_account": "Accumulated Depreciation Account",
"depreciation_expense_account": "Depreciation Expense Account",
}
error_msg = []
companies_with_accounts = set()
def validate_company_accounts(company, acc_row=None):
default_accounts = frappe.get_cached_value(
"Company",
company,
["accumulated_depreciation_account", "depreciation_expense_account"],
as_dict=True,
)
for fieldname, label in depreciation_account_map.items():
row_value = acc_row.get(fieldname) if acc_row else None
if not row_value and not default_accounts.get(fieldname):
if acc_row:
error_msg.append(
_("Row #{0}: Missing <b>{1}</b> for company <b>{2}</b>.").format(
acc_row.idx,
label,
get_link_to_form("Company", company),
)
)
else:
msg = _("Missing account configuration for company <b>{0}</b>.").format(
get_link_to_form("Company", company),
)
if msg not in error_msg:
error_msg.append(msg)
companies_with_assets = frappe.db.get_all(
"Asset",
{
"calculate_depreciation": 1,
"asset_category": self.name,
"status": ["in", ("Submitted", "Partially Depreciated")],
},
pluck="company",
distinct=True,
)
for acc_row in self.accounts:
companies_with_accounts.add(acc_row.company_name)
if acc_row.company_name in companies_with_assets:
validate_company_accounts(acc_row.company_name, acc_row)
for company in companies_with_assets:
if company not in companies_with_accounts:
validate_company_accounts(company)
if error_msg:
msg = _(
"Since there are active depreciable assets under this category, the following accounts are required. <br><br>"
)
msg += _(
"You can either configure default depreciation accounts in the Company or set the required accounts in the following rows: <br><br>"
)
msg += "<br>".join(error_msg)
frappe.throw(msg, title=_("Missing Accounts"))
def get_asset_category_account( def get_asset_category_account(
fieldname, item=None, asset=None, account=None, asset_category=None, company=None fieldname, item=None, asset=None, account=None, asset_category=None, company=None

View File

@@ -4,6 +4,8 @@
import frappe import frappe
from frappe.tests import IntegrationTestCase from frappe.tests import IntegrationTestCase
from erpnext.assets.doctype.asset.test_asset import create_asset
class TestAssetCategory(IntegrationTestCase): class TestAssetCategory(IntegrationTestCase):
def test_mandatory_fields(self): def test_mandatory_fields(self):
@@ -50,3 +52,67 @@ class TestAssetCategory(IntegrationTestCase):
) )
self.assertRaises(frappe.ValidationError, asset_category.insert) self.assertRaises(frappe.ValidationError, asset_category.insert)
def test_duplicate_company_accounts(self):
asset_category = frappe.get_doc(
{
"doctype": "Asset Category",
"asset_category_name": "Computers",
"accounts": [
{
"company_name": "_Test Company",
"fixed_asset_account": "_Test Fixed Asset - _TC",
},
{
"company_name": "_Test Company",
"fixed_asset_account": "_Test Fixed Asset - _TC",
},
],
}
)
with self.assertRaises(frappe.ValidationError) as err:
asset_category.save()
self.assertTrue("Cannot set multiple account rows for the same company" in str(err.exception))
def test_depreciation_accounts_required_for_existing_depreciable_assets(self):
asset = create_asset(
asset_category="Computers",
calculate_depreciation=1,
company="_Test Company",
submit=1,
)
company_acccount_depreciation = frappe.db.get_value(
"Company",
asset.company,
[
"accumulated_depreciation_account",
"depreciation_expense_account",
],
as_dict=True,
)
frappe.db.set_value(
"Company",
asset.company,
{
"accumulated_depreciation_account": "",
"depreciation_expense_account": "",
},
)
try:
asset_category = frappe.get_doc("Asset Category", asset.asset_category)
asset_category.enable_cwip_accounting = 0
for row in asset_category.accounts:
if row.company_name == asset.company and (
row.accumulated_depreciation_account or row.depreciation_expense_account
):
row.accumulated_depreciation_account = None
row.depreciation_expense_account = None
with self.assertRaises(frappe.ValidationError) as err:
asset_category.save()
self.assertTrue(
"Since there are active depreciable assets under this category, the following accounts are required."
in str(err.exception)
)
finally:
frappe.db.set_value("Company", asset.company, company_acccount_depreciation)

View File

@@ -271,7 +271,7 @@ def get_asset_shift_factors_map():
@frappe.whitelist() @frappe.whitelist()
def get_depr_schedule(asset_name, status, finance_book=None): def get_depr_schedule(asset_name: str, status: str, finance_book: str | None = None):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book) asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book)
if not asset_depr_schedule_doc: if not asset_depr_schedule_doc:
@@ -281,13 +281,13 @@ def get_depr_schedule(asset_name, status, finance_book=None):
@frappe.whitelist() @frappe.whitelist()
def get_asset_depr_schedule_doc(asset_name, status=None, finance_book=None): def get_asset_depr_schedule_doc(asset_name: str, status: str | None = None, finance_book: str | None = None):
asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book) asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book)
if not asset_depr_schedule: if not asset_depr_schedule:
return return
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule[0].name) asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule)
return asset_depr_schedule_doc return asset_depr_schedule_doc
@@ -299,21 +299,23 @@ def get_asset_depr_schedule_name(asset_name, status=None, finance_book=None):
] ]
if status: if status:
if isinstance(status, str): status_list = [status] if isinstance(status, str) else status
status = [status] filters.append(["status", "in", status_list])
filters.append(["status", "in", status])
if finance_book: finance_book_filter = (
filters.append(["finance_book", "=", finance_book]) ["finance_book", "=", finance_book] if finance_book else ["finance_book", "is", "not set"]
else: )
filters.append(["finance_book", "is", "not set"]) filters.append(finance_book_filter)
return frappe.get_all( depreciation_schedules = frappe.get_all(
doctype="Asset Depreciation Schedule", doctype="Asset Depreciation Schedule",
filters=filters, filters=filters,
fields=["name"],
limit=1, limit=1,
) )
return depreciation_schedules[0].name if depreciation_schedules else None
def is_first_day_of_the_month(date): def is_first_day_of_the_month(date):
first_day_of_the_month = get_first_day(date) first_day_of_the_month = get_first_day(date)

View File

@@ -87,7 +87,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
calculate_depreciation=1, calculate_depreciation=1,
depreciation_method="Straight Line", depreciation_method="Straight Line",
available_for_use_date="2023-10-10", available_for_use_date="2023-10-10",
is_existing_asset=1, asset_type="Existing Asset",
opening_number_of_booked_depreciations=9, opening_number_of_booked_depreciations=9,
opening_accumulated_depreciation=265, opening_accumulated_depreciation=265,
depreciation_start_date="2024-07-31", depreciation_start_date="2024-07-31",
@@ -127,7 +127,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
calculate_depreciation=1, calculate_depreciation=1,
depreciation_method="Straight Line", depreciation_method="Straight Line",
available_for_use_date="2023-10-10", available_for_use_date="2023-10-10",
is_existing_asset=1, asset_type="Existing Asset",
opening_number_of_booked_depreciations=9, opening_number_of_booked_depreciations=9,
opening_accumulated_depreciation=265.30, opening_accumulated_depreciation=265.30,
depreciation_start_date="2024-07-31", depreciation_start_date="2024-07-31",
@@ -165,7 +165,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
calculate_depreciation=1, calculate_depreciation=1,
depreciation_method="Straight Line", depreciation_method="Straight Line",
available_for_use_date="2023-11-01", available_for_use_date="2023-11-01",
is_existing_asset=1, asset_type="Existing Asset",
opening_number_of_booked_depreciations=4, opening_number_of_booked_depreciations=4,
opening_accumulated_depreciation=223.15, opening_accumulated_depreciation=223.15,
depreciation_start_date="2024-12-31", depreciation_start_date="2024-12-31",
@@ -529,7 +529,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2023-03-31", depreciation_start_date="2023-03-31",
frequency_of_depreciation=1, frequency_of_depreciation=1,
total_number_of_depreciations=12, total_number_of_depreciations=12,
is_existing_asset=1, asset_type="Existing Asset",
opening_accumulated_depreciation=64.52, opening_accumulated_depreciation=64.52,
opening_number_of_booked_depreciations=2, opening_number_of_booked_depreciations=2,
submit=1, submit=1,
@@ -851,7 +851,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2023-03-31", depreciation_start_date="2023-03-31",
frequency_of_depreciation=1, frequency_of_depreciation=1,
total_number_of_depreciations=12, total_number_of_depreciations=12,
is_existing_asset=1, asset_type="Existing Asset",
opening_accumulated_depreciation=64.52, opening_accumulated_depreciation=64.52,
opening_number_of_booked_depreciations=2, opening_number_of_booked_depreciations=2,
submit=1, submit=1,
@@ -925,7 +925,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2021-12-31", depreciation_start_date="2021-12-31",
frequency_of_depreciation=12, frequency_of_depreciation=12,
total_number_of_depreciations=3, total_number_of_depreciations=3,
is_existing_asset=1, asset_type="Existing Asset",
submit=1, submit=1,
) )
post_depreciation_entries(date="2021-12-31") post_depreciation_entries(date="2021-12-31")
@@ -1014,7 +1014,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
depreciation_start_date="2021-12-31", depreciation_start_date="2021-12-31",
frequency_of_depreciation=12, frequency_of_depreciation=12,
total_number_of_depreciations=3, total_number_of_depreciations=3,
is_existing_asset=1, asset_type="Existing Asset",
submit=1, submit=1,
) )
post_depreciation_entries(date="2021-12-31") post_depreciation_entries(date="2021-12-31")
@@ -1093,7 +1093,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
rate_of_depreciation=50, rate_of_depreciation=50,
frequency_of_depreciation=12, frequency_of_depreciation=12,
total_number_of_depreciations=3, total_number_of_depreciations=3,
is_existing_asset=1, asset_type="Existing Asset",
submit=1, submit=1,
) )
post_depreciation_entries(date="2021-12-31") post_depreciation_entries(date="2021-12-31")

View File

@@ -2,11 +2,13 @@
# For license information, please see license.txt # For license information, please see license.txt
from typing import Any
import frappe import frappe
from frappe import _, throw from frappe import _, throw
from frappe.desk.form import assign_to from frappe.desk.form import assign_to
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_days, add_months, add_years, getdate, nowdate from frappe.utils import DateTimeLikeObject, add_days, add_months, add_years, getdate, nowdate
class AssetMaintenance(Document): class AssetMaintenance(Document):
@@ -90,7 +92,11 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
@frappe.whitelist() @frappe.whitelist()
def calculate_next_due_date( def calculate_next_due_date(
periodicity, start_date=None, end_date=None, last_completion_date=None, next_due_date=None periodicity: str,
start_date: DateTimeLikeObject | None = None,
end_date: DateTimeLikeObject | None = None,
last_completion_date: DateTimeLikeObject | None = None,
next_due_date: DateTimeLikeObject | None = None,
): ):
if not start_date and not last_completion_date: if not start_date and not last_completion_date:
start_date = frappe.utils.now() start_date = frappe.utils.now()
@@ -164,19 +170,30 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_team_members(doctype, txt, searchfield, start, page_len, filters): def get_team_members(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict[str, Any],
) -> list[tuple[str]]:
return frappe.db.get_values( return frappe.db.get_values(
"Maintenance Team Member", {"parent": filters.get("maintenance_team")}, "team_member" "Maintenance Team Member",
{"parent": filters.get("maintenance_team")},
"team_member",
) )
@frappe.whitelist() @frappe.whitelist()
def get_maintenance_log(asset_name): def get_maintenance_log(asset_name: str):
return frappe.db.sql( return frappe.db.sql(
""" """
select maintenance_status, count(asset_name) as count, asset_name select maintenance_status, count(asset_name) as count, asset_name
from `tabAsset Maintenance Log` from `tabAsset Maintenance Log`
where asset_name=%s group by maintenance_status""", where asset_name=%s
(asset_name), group by maintenance_status
""",
(asset_name,),
as_dict=1, as_dict=1,
) )

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr, get_link_to_form from frappe.utils import cstr, get_datetime, get_link_to_form
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
@@ -34,6 +34,7 @@ class AssetMovement(Document):
for d in self.assets: for d in self.assets:
self.validate_asset(d) self.validate_asset(d)
self.validate_movement(d) self.validate_movement(d)
self.validate_transaction_date(d)
def validate_asset(self, d): def validate_asset(self, d):
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"]) status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
@@ -51,6 +52,18 @@ class AssetMovement(Document):
else: else:
self.validate_employee(d) self.validate_employee(d)
def validate_transaction_date(self, d):
previous_movement_date = frappe.db.get_value(
"Asset Movement",
[["Asset Movement Item", "asset", "=", d.asset], ["docstatus", "=", 1]],
"transaction_date",
order_by="transaction_date desc",
)
if previous_movement_date and get_datetime(previous_movement_date) > get_datetime(
self.transaction_date
):
frappe.throw(_("Transaction date can't be earlier than previous movement date"))
def validate_location_and_employee(self, d): def validate_location_and_employee(self, d):
self.validate_location(d) self.validate_location(d)
self.validate_employee(d) self.validate_employee(d)

View File

@@ -3,9 +3,9 @@
import frappe import frappe
from frappe.tests import IntegrationTestCase from frappe.tests import IntegrationTestCase
from frappe.utils import now from frappe.utils import add_days, now
from erpnext.assets.doctype.asset.test_asset import create_asset_data from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.setup.doctype.employee.test_employee import make_employee from erpnext.setup.doctype.employee.test_employee import make_employee
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -146,6 +146,33 @@ class TestAssetMovement(IntegrationTestCase):
movement1.cancel() movement1.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
def test_movement_transaction_date(self):
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
asset.save().submit()
if not frappe.db.exists("Location", "Test Location 2"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
asset_creation_date = frappe.db.get_value(
"Asset Movement",
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
"transaction_date",
)
asset_movement = create_asset_movement(
purpose="Transfer",
company=asset.company,
assets=[
{
"asset": asset.name,
"source_location": "Test Location",
"target_location": "Test Location 2",
}
],
transaction_date=add_days(asset_creation_date, -1),
do_not_save=True,
)
self.assertRaises(frappe.ValidationError, asset_movement.save)
def create_asset_movement(**args): def create_asset_movement(**args):
args = frappe._dict(args) args = frappe._dict(args)
@@ -164,9 +191,10 @@ def create_asset_movement(**args):
"reference_name": args.reference_name, "reference_name": args.reference_name,
} }
) )
if not args.do_not_save:
movement.insert() movement.insert(ignore_if_duplicate=True)
movement.submit() if not args.do_not_submit:
movement.submit()
return movement return movement

View File

@@ -9,9 +9,9 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series", "naming_series",
"company",
"asset", "asset",
"asset_name", "asset_name",
"company",
"column_break_2", "column_break_2",
"repair_status", "repair_status",
"failure_date", "failure_date",
@@ -28,10 +28,6 @@
"column_break_ajbh", "column_break_ajbh",
"column_break_hkem", "column_break_hkem",
"repair_cost", "repair_cost",
"accounting_dimensions_section",
"cost_center",
"column_break_14",
"project",
"stock_consumption_details_section", "stock_consumption_details_section",
"stock_items", "stock_items",
"section_break_ltbb", "section_break_ltbb",
@@ -43,7 +39,12 @@
"capitalize_repair_cost", "capitalize_repair_cost",
"increase_in_asset_life", "increase_in_asset_life",
"column_break_xebe", "column_break_xebe",
"total_repair_cost" "total_repair_cost",
"accounting_dimensions_section",
"cost_center",
"column_break_14",
"project",
"connection_tab"
], ],
"fields": [ "fields": [
{ {
@@ -149,8 +150,7 @@
{ {
"fieldname": "accounting_details", "fieldname": "accounting_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1, "hide_border": 1
"label": "Repair Purchase Invoices"
}, },
{ {
"fieldname": "stock_items", "fieldname": "stock_items",
@@ -206,6 +206,7 @@
{ {
"fieldname": "invoices", "fieldname": "invoices",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Repair Purchase Invoices",
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;", "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;",
"no_copy": 1, "no_copy": 1,
"options": "Asset Repair Purchase Invoice" "options": "Asset Repair Purchase Invoice"
@@ -244,6 +245,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval: doc.consumed_items_cost",
"fieldname": "consumed_items_cost", "fieldname": "consumed_items_cost",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Consumed Items Cost" "label": "Consumed Items Cost"
@@ -256,7 +258,13 @@
"depends_on": "capitalize_repair_cost", "depends_on": "capitalize_repair_cost",
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Dimensions" "label": "Accounting Dimension"
},
{
"fieldname": "connection_tab",
"fieldtype": "Tab Break",
"label": "Connection",
"show_dashboard": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@@ -267,7 +275,7 @@
"link_fieldname": "asset_repair" "link_fieldname": "asset_repair"
} }
], ],
"modified": "2026-01-06 15:48:13.862505", "modified": "2026-02-06 14:57:54.257572",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Repair", "name": "Asset Repair",

View File

@@ -5,7 +5,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.query_builder import DocType from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours from frappe.utils import DateTimeLikeObject, cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -448,14 +448,21 @@ class AssetRepair(AccountsController):
@frappe.whitelist() @frappe.whitelist()
def get_downtime(failure_date, completion_date): def get_downtime(failure_date: DateTimeLikeObject, completion_date: DateTimeLikeObject):
downtime = time_diff_in_hours(completion_date, failure_date) downtime = time_diff_in_hours(completion_date, failure_date)
return round(downtime, 2) return round(downtime, 2)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters): def get_purchase_invoice(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict,
):
""" """
Get Purchase Invoices that have expense accounts for non-stock items. Get Purchase Invoices that have expense accounts for non-stock items.
Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account. Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account.
@@ -490,7 +497,14 @@ def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters): def get_expense_accounts(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict,
):
""" """
Get expense accounts for non-stock (service) items from the purchase invoice. Get expense accounts for non-stock (service) items from the purchase invoice.
Used as a query function for link fields. Used as a query function for link fields.
@@ -548,7 +562,7 @@ def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[st
@frappe.whitelist() @frappe.whitelist()
def get_unallocated_repair_cost( def get_unallocated_repair_cost(
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
) -> float: ):
""" """
Calculate the unused repair cost for a purchase invoice and expense account. Calculate the unused repair cost for a purchase invoice and expense account.
""" """

View File

@@ -210,26 +210,29 @@ class TestAssetRepair(IntegrationTestCase):
self.assertRaises(frappe.ValidationError, asset_repair2.save) self.assertRaises(frappe.ValidationError, asset_repair2.save)
def test_gl_entries_with_perpetual_inventory(self): def test_gl_entries_with_perpetual_inventory(self):
set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") company = "_Test Company with perpetual inventory"
set_depreciation_settings_in_company(company)
asset_category = frappe.get_doc("Asset Category", "Computers") asset_category = frappe.get_doc("Asset Category", "Computers")
asset_category.append(
"accounts", if not any(row.company_name == company for row in asset_category.accounts):
{ asset_category.append(
"company_name": "_Test Company with perpetual inventory", "accounts",
"fixed_asset_account": "_Test Fixed Asset - TCP1", {
"accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", "company_name": company,
"depreciation_expense_account": "_Test Depreciations - TCP1", "fixed_asset_account": "_Test Fixed Asset - TCP1",
"capital_work_in_progress_account": "CWIP Account - TCP1", "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1",
}, "depreciation_expense_account": "_Test Depreciations - TCP1",
) "capital_work_in_progress_account": "CWIP Account - TCP1",
asset_category.save() },
)
asset_category.save()
asset_repair = create_asset_repair( asset_repair = create_asset_repair(
capitalize_repair_cost=1, capitalize_repair_cost=1,
stock_consumption=1, stock_consumption=1,
warehouse="Stores - TCP1", warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory", company=company,
pi_expense_account1="Administrative Expenses - TCP1", pi_expense_account1="Administrative Expenses - TCP1",
pi_expense_account2="Legal Expenses - TCP1", pi_expense_account2="Legal Expenses - TCP1",
item="_Test Non Stock Item", item="_Test Non Stock Item",
@@ -359,7 +362,7 @@ class TestAssetRepair(IntegrationTestCase):
self.assertEqual(stock_entry.asset_repair, asset_repair.name) self.assertEqual(stock_entry.asset_repair, asset_repair.name)
def test_gl_entries_with_capitalized_asset_repair(self): def test_gl_entries_with_capitalized_asset_repair(self):
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1) asset = create_asset(asset_type="Existing Asset", calculate_depreciation=1, submit=1)
asset_repair = create_asset_repair( asset_repair = create_asset_repair(
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1 asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
) )
@@ -399,7 +402,7 @@ def create_asset_repair(**args):
if args.asset: if args.asset:
asset = args.asset asset = args.asset
else: else:
asset = create_asset(is_existing_asset=1, submit=1, company=args.company) asset = create_asset(asset_type=args.asset_type or "Existing Asset", submit=1, company=args.company)
asset_repair = frappe.new_doc("Asset Repair") asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update( asset_repair.update(
{ {

View File

@@ -227,6 +227,6 @@ class AssetValueAdjustment(Document):
@frappe.whitelist() @frappe.whitelist()
def get_value_of_accounting_dimensions(asset_name): def get_value_of_accounting_dimensions(asset_name: str):
dimension_fields = [*frappe.get_list("Accounting Dimension", pluck="fieldname"), "cost_center"] dimension_fields = [*frappe.get_list("Accounting Dimension", pluck="fieldname"), "cost_center"]
return frappe.db.get_value("Asset", asset_name, fieldname=dimension_fields, as_dict=True) return frappe.db.get_value("Asset", asset_name, fieldname=dimension_fields, as_dict=True)

View File

@@ -211,7 +211,7 @@ def _ring_area(coords):
@frappe.whitelist() @frappe.whitelist()
def get_children(doctype, parent=None, location=None, is_root=False): def get_children(doctype: str, parent: str | None = None, location: str | None = None, is_root: bool = False):
if parent is None or parent == "All Locations": if parent is None or parent == "All Locations":
parent = "" parent = ""

View File

@@ -143,7 +143,7 @@ def get_conditions(filters):
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]] conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("only_existing_assets"): if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets") conditions["asset_type"] = "Existing Asset"
if filters.get("asset_category"): if filters.get("asset_category"):
conditions["asset_category"] = filters.get("asset_category") conditions["asset_category"] = filters.get("asset_category")
if filters.get("cost_center"): if filters.get("cost_center"):
@@ -273,7 +273,7 @@ def get_asset_depreciation_amount_map(filters, finance_book):
) )
if filters.only_existing_assets: if filters.only_existing_assets:
query = query.where(asset.is_existing_asset == 1) query = query.where(asset.asset_type == "Existing Asset")
if filters.asset_category: if filters.asset_category:
query = query.where(asset.asset_category == filters.asset_category) query = query.where(asset.asset_category == filters.asset_category)
if filters.cost_center: if filters.cost_center:
@@ -324,7 +324,7 @@ def get_asset_value_adjustment_map(filters, finance_book):
) )
if filters.only_existing_assets: if filters.only_existing_assets:
query = query.where(asset.is_existing_asset == 1) query = query.where(asset.asset_type == "Existing Asset")
if filters.asset_category: if filters.asset_category:
query = query.where(asset.asset_category == filters.asset_category) query = query.where(asset.asset_category == filters.asset_category)
if filters.cost_center: if filters.cost_center:

View File

@@ -461,27 +461,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
} }
} }
get_items_from_open_material_requests() {
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order_based_on_supplier",
args: {
supplier: this.frm.doc.supplier,
},
source_doctype: "Material Request",
source_name: this.frm.doc.supplier,
target: this.frm,
setters: {
company: this.frm.doc.company,
},
get_query_filters: {
docstatus: ["!=", 2],
supplier: this.frm.doc.supplier,
},
get_query_method:
"erpnext.stock.doctype.material_request.material_request.get_material_requests_based_on_supplier",
});
}
validate() { validate() {
set_schedule_date(this.frm); set_schedule_date(this.frm);
} }
@@ -803,7 +782,7 @@ frappe.ui.form.on("Purchase Order", "is_subcontracted", function (frm) {
function prevent_past_schedule_dates(frm) { function prevent_past_schedule_dates(frm) {
if (frm.doc.transaction_date) { if (frm.doc.transaction_date) {
frm.fields_dict["schedule_date"].datepicker.update({ frm.fields_dict["schedule_date"].datepicker?.update({
minDate: new Date(frm.doc.transaction_date), minDate: new Date(frm.doc.transaction_date),
}); });
} }

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