mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 12:25:09 +00:00
Merge branch 'develop' into fixing-emp-contacts
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -60,7 +60,7 @@ body:
|
||||
description: Share exact version number of Frappe and ERPNext you are using.
|
||||
placeholder: |
|
||||
Frappe version -
|
||||
ERPNext Verion -
|
||||
ERPNext version -
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
2
.github/workflows/generate-pot-file.yml
vendored
2
.github/workflows/generate-pot-file.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch: ["develop"]
|
||||
branch: ["develop", "version-16-hotfix"]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- "**.js"
|
||||
- "**.css"
|
||||
- "**.svg"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- 'crowdin.yml'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
<div align="center">
|
||||
<a href="https://frappe.io/erpnext">
|
||||
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
||||
|
||||
50
erpnext/accounts/accounts_dashboard/payments/payments.json
Normal file
50
erpnext/accounts/accounts_dashboard/payments/payments.json
Normal 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"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,17 @@
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"Kas": {
|
||||
@@ -97,17 +108,6 @@
|
||||
},
|
||||
"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"
|
||||
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -97,7 +97,7 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
||||
if doc.doctype == "Bank Clearance":
|
||||
return
|
||||
elif doc.doctype == "Asset":
|
||||
if doc.is_existing_asset:
|
||||
if doc.asset_type == "Existing Asset":
|
||||
return
|
||||
else:
|
||||
date = doc.available_for_use_date
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
"confirm_before_resetting_posting_date",
|
||||
"analytics_section",
|
||||
"enable_accounting_dimensions",
|
||||
"column_break_vtnr",
|
||||
"enable_discounts_and_margin",
|
||||
"journals_section",
|
||||
"merge_similar_account_heads",
|
||||
"deferred_accounting_settings_section",
|
||||
@@ -51,12 +55,16 @@
|
||||
"allow_pegged_currencies_exchange_rates",
|
||||
"column_break_yuug",
|
||||
"stale_days",
|
||||
"payments_tab",
|
||||
"section_break_jpd0",
|
||||
"auto_reconcile_payments",
|
||||
"auto_reconciliation_job_trigger",
|
||||
"reconciliation_queue_size",
|
||||
"column_break_resa",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"payment_options_section",
|
||||
"enable_loyalty_point_program",
|
||||
"column_break_ctam",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -281,7 +289,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
@@ -637,16 +645,59 @@
|
||||
"fieldname": "budget_section",
|
||||
"fieldtype": "Section Break",
|
||||
"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,
|
||||
"hide_toolbar": 1,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-11 18:30:45.968531",
|
||||
"modified": "2026-02-04 17:15:38.609327",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -12,6 +12,28 @@ from frappe.utils import cint
|
||||
|
||||
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):
|
||||
# begin: auto-generated types
|
||||
@@ -43,9 +65,12 @@ class AccountsSettings(Document):
|
||||
default_ageing_range: DF.Data | None
|
||||
delete_linked_ledger_entries: DF.Check
|
||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||
enable_accounting_dimensions: DF.Check
|
||||
enable_common_party_accounting: DF.Check
|
||||
enable_discounts_and_margin: DF.Check
|
||||
enable_fuzzy_matching: DF.Check
|
||||
enable_immutable_ledger: DF.Check
|
||||
enable_loyalty_point_program: DF.Check
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
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:
|
||||
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:
|
||||
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.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,
|
||||
)
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
frappe.provide("erpnext.integrations");
|
||||
|
||||
frappe.ui.form.on("Bank", {
|
||||
onload: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
},
|
||||
refresh: function (frm) {
|
||||
add_fields_to_mapping_table(frm);
|
||||
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(
|
||||
"bank_transaction_field",
|
||||
"options",
|
||||
options
|
||||
);
|
||||
const grid = frm.fields_dict.bank_transaction_mapping?.grid;
|
||||
|
||||
if (grid) {
|
||||
grid.update_docfield_property("bank_transaction_field", "options", options);
|
||||
}
|
||||
};
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
plaid_success(token, response) {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company Account",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@@ -98,6 +99,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"mandatory_depends_on": "is_company_account",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
@@ -252,7 +254,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified": "2026-01-20 00:46:16.633364",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
|
||||
@@ -51,25 +51,29 @@ class BankAccount(Document):
|
||||
delete_contact_and_address("Bank Account", self.name)
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_account()
|
||||
self.validate_is_company_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
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 validate_is_company_account(self):
|
||||
if self.is_company_account:
|
||||
if not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_company(self):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
if not self.account:
|
||||
frappe.throw(_("Company Account is mandatory"))
|
||||
|
||||
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):
|
||||
if self.is_default and not self.disabled:
|
||||
|
||||
@@ -15,7 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cstr, date_diff, flt, getdate
|
||||
from pypika.terms import LiteralValue
|
||||
from pypika.terms import Bracket, LiteralValue
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -541,7 +541,7 @@ class FinancialQueryBuilder:
|
||||
.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")
|
||||
|
||||
for row in results:
|
||||
@@ -636,12 +636,15 @@ class FinancialQueryBuilder:
|
||||
return self._execute_with_permissions(query, "GL Entry")
|
||||
|
||||
def _calculate_running_balances(self, balances_data: dict, gl_data: list[dict]) -> dict:
|
||||
for row in gl_data:
|
||||
account = row["account"]
|
||||
gl_dict = {row["account"]: row for row in gl_data}
|
||||
accounts = set(balances_data.keys()) | set(gl_dict.keys())
|
||||
|
||||
for account in accounts:
|
||||
if account not in balances_data:
|
||||
balances_data[account] = AccountData(account=account, **self._get_account_meta(account))
|
||||
|
||||
account_data: AccountData = balances_data[account]
|
||||
gl_movement = gl_dict.get(account, {})
|
||||
|
||||
if account_data.has_periods():
|
||||
first_period = account_data.get_period(self.periods[0]["key"])
|
||||
@@ -651,20 +654,13 @@ class FinancialQueryBuilder:
|
||||
|
||||
for period in self.periods:
|
||||
period_key = period["key"]
|
||||
movement = row.get(period_key, 0.0)
|
||||
movement = gl_movement.get(period_key, 0.0)
|
||||
closing_balance = current_balance + movement
|
||||
|
||||
account_data.add_period(PeriodValue(period_key, current_balance, closing_balance, movement))
|
||||
|
||||
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):
|
||||
for account_data in balances_data.values():
|
||||
account_data: AccountData
|
||||
@@ -683,12 +679,12 @@ class FinancialQueryBuilder:
|
||||
else:
|
||||
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 hasattr(table, "is_period_closing_voucher_entry"):
|
||||
query = query.where(table.is_period_closing_voucher_entry == 0)
|
||||
else:
|
||||
if doctype == "GL Entry":
|
||||
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"):
|
||||
projects = self.filters.get("project")
|
||||
@@ -736,7 +732,7 @@ class FinancialQueryBuilder:
|
||||
user_conditions = build_match_conditions(doctype)
|
||||
|
||||
if user_conditions:
|
||||
query = query.where(LiteralValue(user_conditions))
|
||||
query = query.where(Bracket(LiteralValue(user_conditions)))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ from frappe.utils import flt
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
DependencyResolver,
|
||||
FilterExpressionParser,
|
||||
FinancialQueryBuilder,
|
||||
FormulaCalculator,
|
||||
)
|
||||
from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import (
|
||||
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
|
||||
# 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)
|
||||
condition = parser.build_condition(mock_row_invalid, account_table)
|
||||
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()
|
||||
|
||||
@@ -277,7 +277,21 @@ frappe.ui.form.on("Journal Entry", {
|
||||
var update_jv_details = function (doc, r) {
|
||||
$.each(r, function (i, d) {
|
||||
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");
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"entry_type_and_date",
|
||||
"company",
|
||||
"is_system_generated",
|
||||
"title",
|
||||
"voucher_type",
|
||||
@@ -17,7 +18,6 @@
|
||||
"reversal_of",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
"company",
|
||||
"posting_date",
|
||||
"finance_book",
|
||||
"apply_tds",
|
||||
@@ -638,7 +638,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2025-11-13 17:54:14.542903",
|
||||
"modified": "2026-02-03 14:40:39.944524",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -74,8 +74,8 @@ class JournalEntry(AccountsController):
|
||||
mode_of_payment: DF.Link | None
|
||||
multi_currency: DF.Check
|
||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||
party_not_required: DF.Check
|
||||
override_tax_withholding_entries: DF.Check
|
||||
party_not_required: DF.Check
|
||||
pay_to_recd_from: DF.Data | None
|
||||
payment_order: DF.Link | None
|
||||
periodic_entry_difference_account: DF.Link | None
|
||||
@@ -179,7 +179,7 @@ class JournalEntry(AccountsController):
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def submit(self):
|
||||
if len(self.accounts) > 100:
|
||||
if len(self.accounts) > 100 and not self.meta.queue_in_background:
|
||||
queue_submission(self, "_submit")
|
||||
else:
|
||||
return self._submit()
|
||||
@@ -1691,6 +1691,10 @@ def get_exchange_rate(
|
||||
credit=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", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
frappe.ui.form.on("Journal Entry Template", {
|
||||
onload: function (frm) {
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
if (frm.is_new()) {
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
@@ -37,6 +38,31 @@ frappe.ui.form.on("Journal Entry Template", {
|
||||
|
||||
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) {
|
||||
var add_accounts = function (doc, r) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -42,7 +43,29 @@ class JournalEntryTemplate(Document):
|
||||
]
|
||||
# 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()
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account"
|
||||
"account",
|
||||
"party_type",
|
||||
"party",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -15,18 +21,55 @@
|
||||
"label": "Account",
|
||||
"options": "Account",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:58.986448",
|
||||
"modified": "2026-01-09 13:16:27.615083",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Template Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,13 @@ class JournalEntryTemplateAccount(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
account: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
project: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
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) {
|
||||
@@ -1108,7 +1118,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||
await frm.call("allocate_amount_to_references", {
|
||||
paid_amount: paid_amount,
|
||||
paid_amount: flt(paid_amount),
|
||||
paid_amount_change: paid_amount_change,
|
||||
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||
});
|
||||
@@ -1444,16 +1454,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
// set taxes table
|
||||
if (r.message) {
|
||||
for (let tax of r.message) {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.add_child("taxes", tax);
|
||||
let taxes = r.message;
|
||||
taxes.forEach((tax) => {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -132,6 +132,12 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date",
|
||||
|
||||
@@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document):
|
||||
amount_in_account_currency: DF.Currency
|
||||
company: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
project: DF.Link | None
|
||||
delinked: DF.Check
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
|
||||
@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
for x in self.dimensions:
|
||||
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))
|
||||
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
|
||||
@@ -535,7 +535,7 @@ class PaymentRequest(Document):
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def make_payment_request(**args):
|
||||
"""Make payment request"""
|
||||
|
||||
@@ -546,6 +546,9 @@ def make_payment_request(**args):
|
||||
if args.dn and not isinstance(args.dn, 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)
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
@@ -819,7 +822,7 @@ def get_print_format_list(ref_doctype):
|
||||
return {"print_format": print_format_list}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def resend_payment_email(docname):
|
||||
return frappe.get_doc("Payment Request", docname).send_email()
|
||||
|
||||
|
||||
@@ -159,15 +159,16 @@
|
||||
"language",
|
||||
"column_break_84",
|
||||
"select_print_heading",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"column_break_bhao",
|
||||
"utm_campaign",
|
||||
"more_information",
|
||||
"inter_company_invoice_reference",
|
||||
"customer_group",
|
||||
"is_discounted",
|
||||
"col_break23",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"column_break_gpiw",
|
||||
"status",
|
||||
"more_info",
|
||||
"debit_to",
|
||||
@@ -1541,10 +1542,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gpiw",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"fieldtype": "Link",
|
||||
@@ -1610,13 +1607,24 @@
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Details",
|
||||
"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",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 22:22:31.471752",
|
||||
"modified": "2026-02-10 14:23:07.181782",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -897,6 +897,53 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||
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):
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
BatchNegativeStockError,
|
||||
|
||||
@@ -12,56 +12,78 @@
|
||||
"disabled",
|
||||
"column_break_9",
|
||||
"warehouse",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"company_address",
|
||||
"section_break_15",
|
||||
"applicable_for_users",
|
||||
"accounting_tab",
|
||||
"section_break_11",
|
||||
"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",
|
||||
"hide_images",
|
||||
"hide_unavailable_items",
|
||||
"auto_add_item_to_cart",
|
||||
"validate_stock_on_save",
|
||||
"print_receipt_on_order_complete",
|
||||
"action_on_new_invoice",
|
||||
"validate_stock_on_save",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"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",
|
||||
"column_break_hwfg",
|
||||
"allow_discount_change",
|
||||
"set_grand_total_to_default_mop",
|
||||
"allow_partial_payment",
|
||||
"column_break_egpi",
|
||||
"allow_warehouse_change",
|
||||
"section_break_15",
|
||||
"applicable_for_users",
|
||||
"section_break_23",
|
||||
"item_groups",
|
||||
"column_break_25",
|
||||
"customer_groups",
|
||||
"more_info_tab",
|
||||
"section_break_16",
|
||||
"print_format",
|
||||
"letter_head",
|
||||
"column_break0",
|
||||
"tc_name",
|
||||
"select_print_heading",
|
||||
"section_break_19",
|
||||
"selling_price_list",
|
||||
"currency",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"write_off_limit",
|
||||
"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"
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"column_break_tvls",
|
||||
"utm_campaign",
|
||||
"column_break_xygw",
|
||||
"utm_medium"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -130,8 +152,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Configuration"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Only show Items from these Item Groups",
|
||||
@@ -152,6 +173,7 @@
|
||||
"options": "POS Customer Group"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Print Settings"
|
||||
@@ -191,7 +213,7 @@
|
||||
{
|
||||
"fieldname": "section_break_19",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting"
|
||||
"label": "Miscellaneous"
|
||||
},
|
||||
{
|
||||
"fieldname": "selling_price_list",
|
||||
@@ -427,9 +449,111 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Applicable only on Transactions made using POS",
|
||||
"fieldname": "allow_partial_payment",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
@@ -458,7 +582,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-06-24 11:19:19.834905",
|
||||
"modified": "2026-02-10 14:24:48.597412",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -34,6 +34,7 @@ class POSProfile(Document):
|
||||
allow_discount_change: DF.Check
|
||||
allow_partial_payment: DF.Check
|
||||
allow_rate_change: DF.Check
|
||||
allow_warehouse_change: DF.Check
|
||||
applicable_for_users: DF.Table[POSProfileUser]
|
||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||
auto_add_item_to_cart: DF.Check
|
||||
|
||||
@@ -98,8 +98,7 @@ def get_customers_list(pos_profile=None):
|
||||
|
||||
return (
|
||||
frappe.db.sql(
|
||||
f""" select name, customer_name, customer_group,
|
||||
territory, customer_pos_id from tabCustomer where disabled = 0
|
||||
f""" select name, customer_name, customer_group, territory from tabCustomer where disabled = 0
|
||||
and {cond}""",
|
||||
tuple(customer_groups),
|
||||
as_dict=1,
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Apply On",
|
||||
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
|
||||
"options": "Item Code\nItem Group\nBrand\nTransaction",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -657,7 +657,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 11:40:07.096854",
|
||||
"modified": "2026-02-17 12:24:07.553505",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
@@ -714,9 +714,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class PricingRule(Document):
|
||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||
apply_discount_on_rate: 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_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
|
||||
brands: DF.Table[PricingRuleBrand]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
||||
"depends_on": "eval:parent.apply_on == 'Brand'",
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -28,14 +28,15 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:17.857046",
|
||||
"modified": "2026-02-17 12:17:13.073587",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule Brand",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
||||
"depends_on": "eval:parent.apply_on == 'Item Group'",
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -28,14 +28,15 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:18.221095",
|
||||
"modified": "2026-02-17 12:16:57.778471",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule Item Group",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,8 +415,9 @@ def reconcile(doc: None | str = None) -> None:
|
||||
for x in allocations:
|
||||
pr.append("allocation", x)
|
||||
|
||||
skip_ref_details_update_for_pe = check_multi_currency(pr)
|
||||
# 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
|
||||
# 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")
|
||||
|
||||
|
||||
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()
|
||||
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
|
||||
@@ -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 () {
|
||||
return {
|
||||
filters: { is_composite_asset: 1, docstatus: 0 },
|
||||
filters: { asset_type: "Composite Asset", docstatus: 0 },
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -85,20 +85,24 @@
|
||||
"taxes_and_charges_added",
|
||||
"taxes_and_charges_deducted",
|
||||
"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_rounding_adjustment",
|
||||
"base_rounded_total",
|
||||
"column_break_hcca",
|
||||
"base_in_words",
|
||||
"column_break8",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
"use_company_roundoff_cost_center",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"base_rounded_total",
|
||||
"section_break_ttrv",
|
||||
"total_advance",
|
||||
"column_break_peap",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"section_tax_withholding_entry",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
@@ -606,6 +610,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.items.every((item) => !item.pr_detail)",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Stock",
|
||||
@@ -882,15 +887,10 @@
|
||||
"options": "currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_49",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Grand Total (Company Currency)",
|
||||
"label": "Grand Total",
|
||||
"oldfieldname": "grand_total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
@@ -901,7 +901,7 @@
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment (Company Currency)",
|
||||
"label": "Rounding Adjustment",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
@@ -911,7 +911,7 @@
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounded Total (Company Currency)",
|
||||
"label": "Rounded Total",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
@@ -920,7 +920,7 @@
|
||||
{
|
||||
"fieldname": "base_in_words",
|
||||
"fieldtype": "Data",
|
||||
"label": "In Words (Company Currency)",
|
||||
"label": "In Words",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words",
|
||||
"oldfieldtype": "Data",
|
||||
@@ -1660,6 +1660,28 @@
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
@@ -1667,7 +1689,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-15 06:41:38.237728",
|
||||
"modified": "2026-02-05 20:45:16.964500",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -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_category.asset_category import get_asset_category_account
|
||||
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.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
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)
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
for row in target_parent.get("items"):
|
||||
if row.get("qty") == 0:
|
||||
target_parent.remove(row)
|
||||
remove_items_with_zero_qty(target_parent)
|
||||
set_missing_values(source_parent, target_parent)
|
||||
|
||||
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):
|
||||
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,
|
||||
"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,
|
||||
post_parent_process,
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"stock_uom_rate",
|
||||
"is_free_item",
|
||||
"apply_tds",
|
||||
"allow_zero_valuation_rate",
|
||||
"section_break_22",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -97,7 +98,6 @@
|
||||
"service_start_date",
|
||||
"service_end_date",
|
||||
"reference",
|
||||
"allow_zero_valuation_rate",
|
||||
"item_tax_rate",
|
||||
"bom",
|
||||
"include_exploded_items",
|
||||
@@ -420,6 +420,7 @@
|
||||
"options": "UOM"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
"fieldname": "warehouse_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Warehouse"
|
||||
@@ -447,7 +448,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
@@ -459,14 +459,12 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "rejected_serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Rejected Serial No",
|
||||
@@ -577,6 +575,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
@@ -800,7 +799,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.is_internal_supplier && parent.update_stock",
|
||||
"depends_on": "eval:parent.is_internal_supplier",
|
||||
"fieldname": "from_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
@@ -896,7 +895,7 @@
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -906,7 +905,7 @@
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"label": "Rejected Serial and Batch Bundle",
|
||||
@@ -922,7 +921,7 @@
|
||||
"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",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No"
|
||||
@@ -992,7 +991,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-13 14:10:02.379392",
|
||||
"modified": "2026-02-15 21:07:49.455930",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
||||
@@ -44,6 +44,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
"Unreconcile Payment Entries",
|
||||
"Serial and Batch Bundle",
|
||||
"Bank Transaction",
|
||||
"Packing Slip",
|
||||
];
|
||||
|
||||
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) {
|
||||
// show Make Delivery Note button only if Sales Invoice is not created from Delivery Note
|
||||
var from_delivery_note = false;
|
||||
from_delivery_note = this.frm.doc.items.some(function (item) {
|
||||
return item.delivery_note ? true : false;
|
||||
});
|
||||
|
||||
if (!from_delivery_note && !is_delivered_by_supplier) {
|
||||
this.frm.add_custom_button(
|
||||
__("Delivery"),
|
||||
this.frm.cscript["Make Delivery Note"],
|
||||
__("Create")
|
||||
if (!is_delivered_by_supplier) {
|
||||
const should_create_delivery_note = doc.items.some(
|
||||
(item) =>
|
||||
item.qty - item.delivered_qty > 0 &&
|
||||
!item.scio_detail &&
|
||||
!item.dn_detail &&
|
||||
!item.delivered_by_supplier
|
||||
);
|
||||
if (should_create_delivery_note) {
|
||||
this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
this.frm.cscript["Make Delivery Note"],
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_section",
|
||||
"company",
|
||||
"company_tax_id",
|
||||
"naming_series",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"tax_id",
|
||||
"company",
|
||||
"company_tax_id",
|
||||
"column_break1",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
@@ -77,34 +77,36 @@
|
||||
"base_total_taxes_and_charges",
|
||||
"column_break_47",
|
||||
"total_taxes_and_charges",
|
||||
"totals",
|
||||
"base_grand_total",
|
||||
"base_rounding_adjustment",
|
||||
"base_rounded_total",
|
||||
"base_in_words",
|
||||
"column_break5",
|
||||
"totals_section",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
"use_company_roundoff_cost_center",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"column_break5",
|
||||
"rounded_total",
|
||||
"disable_rounded_total",
|
||||
"total_advance",
|
||||
"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",
|
||||
"tax_withholding_group",
|
||||
"ignore_tax_withholding_threshold",
|
||||
"override_tax_withholding_entries",
|
||||
"tax_withholding_entries",
|
||||
"section_break_49",
|
||||
"additional_discount_section",
|
||||
"apply_discount_on",
|
||||
"base_discount_amount",
|
||||
"coupon_code",
|
||||
"is_cash_or_non_trade_discount",
|
||||
"additional_discount_account",
|
||||
"column_break_51",
|
||||
"additional_discount_percentage",
|
||||
"discount_amount",
|
||||
"is_cash_or_non_trade_discount",
|
||||
"additional_discount_account",
|
||||
"sec_tax_breakup",
|
||||
"other_charges_calculation",
|
||||
"item_wise_tax_details",
|
||||
@@ -194,13 +196,13 @@
|
||||
"column_break8",
|
||||
"unrealized_profit_loss_account",
|
||||
"against_income_account",
|
||||
"sales_team_section_break",
|
||||
"commission_section",
|
||||
"sales_partner",
|
||||
"amount_eligible_for_commission",
|
||||
"column_break10",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"section_break2",
|
||||
"sales_team_section",
|
||||
"sales_team",
|
||||
"edit_printing_settings",
|
||||
"letter_head",
|
||||
@@ -215,20 +217,21 @@
|
||||
"column_break_140",
|
||||
"to_date",
|
||||
"update_auto_repeat_reference",
|
||||
"utm_analytics_section",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"column_break_ixxw",
|
||||
"utm_campaign",
|
||||
"utm_content",
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"represents_company",
|
||||
"remarks",
|
||||
"customer_group",
|
||||
"column_break_imbx",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"col_break23",
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"inter_company_invoice_reference",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -703,6 +706,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
@@ -793,7 +797,8 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Time Sheets",
|
||||
"options": "Sales Invoice Timesheet",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -1072,14 +1077,6 @@
|
||||
"no_copy": 1,
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_49",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Additional Discount"
|
||||
},
|
||||
{
|
||||
"default": "Grand Total",
|
||||
"fieldname": "apply_discount_on",
|
||||
@@ -1124,22 +1121,12 @@
|
||||
"options": "currency",
|
||||
"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",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Grand Total (Company Currency)",
|
||||
"label": "Grand Total",
|
||||
"oldfieldname": "grand_total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
@@ -1153,9 +1140,8 @@
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Rounding Adjustment (Company Currency)",
|
||||
"label": "Rounding Adjustment",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1165,10 +1151,9 @@
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Rounded Total (Company Currency)",
|
||||
"label": "Rounded Total",
|
||||
"oldfieldname": "rounded_total",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -1178,7 +1163,7 @@
|
||||
"fieldtype": "Small Text",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "In Words (Company Currency)",
|
||||
"label": "In Words",
|
||||
"length": 240,
|
||||
"oldfieldname": "in_words",
|
||||
"oldfieldtype": "Data",
|
||||
@@ -1271,7 +1256,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "advances",
|
||||
"fieldname": "advances_section",
|
||||
"fieldtype": "Section Break",
|
||||
@@ -1647,13 +1631,6 @@
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break23",
|
||||
"fieldtype": "Column Break",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "Draft",
|
||||
"fieldname": "status",
|
||||
@@ -1705,10 +1682,10 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "No",
|
||||
"fieldname": "is_opening",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Is Opening Entry",
|
||||
@@ -1737,18 +1714,6 @@
|
||||
"oldfieldtype": "Text",
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
@@ -1792,16 +1757,6 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"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,
|
||||
"fieldname": "sales_team",
|
||||
@@ -2250,7 +2205,8 @@
|
||||
"hidden": 1,
|
||||
"label": "Item Wise Tax Details",
|
||||
"no_copy": 1,
|
||||
"options": "Item Wise Tax Detail"
|
||||
"options": "Item Wise Tax Detail",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -2291,6 +2247,66 @@
|
||||
"fieldname": "override_tax_withholding_entries",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
@@ -2304,7 +2320,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-24 18:29:50.242618",
|
||||
"modified": "2026-02-10 11:59:07.819903",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -2470,7 +2470,10 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
"cost_center": "cost_center",
|
||||
},
|
||||
"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 Team": {
|
||||
|
||||
@@ -4739,6 +4739,66 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
|
||||
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):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"is_free_item",
|
||||
"apply_tds",
|
||||
"grant_commission",
|
||||
"allow_zero_valuation_rate",
|
||||
"section_break_21",
|
||||
"net_rate",
|
||||
"net_amount",
|
||||
@@ -88,7 +89,6 @@
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
"col_break5",
|
||||
"allow_zero_valuation_rate",
|
||||
"incoming_rate",
|
||||
"item_tax_rate",
|
||||
"actual_batch_qty",
|
||||
@@ -580,6 +580,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.serial_no || doc.batch_no",
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
"fieldname": "warehouse_and_reference",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Stock Details"
|
||||
@@ -595,7 +596,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: parent.is_internal_customer && parent.update_stock",
|
||||
"depends_on": "eval: parent.is_internal_customer",
|
||||
"fieldname": "target_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
@@ -613,7 +614,6 @@
|
||||
"options": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
@@ -626,6 +626,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.update_stock",
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
@@ -633,7 +634,6 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
@@ -906,7 +906,7 @@
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial and Batch Bundle",
|
||||
@@ -916,7 +916,7 @@
|
||||
"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",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch No"
|
||||
@@ -1009,7 +1009,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-04 11:08:25.583561",
|
||||
"modified": "2026-02-15 21:08:57.341638",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
||||
"depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
@@ -85,14 +85,15 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:36.427565",
|
||||
"modified": "2026-02-16 20:46:34.592604",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,16 +43,18 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:55.008837",
|
||||
"modified": "2025-11-14 16:17:25.584675",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Transaction Deletion Record Details",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -151,6 +151,8 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
separate_check_filters: true,
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
@@ -108,6 +108,8 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
separate_check_filters: true,
|
||||
|
||||
onload: function (report) {
|
||||
report.page.add_inner_button(__("Accounts Payable"), function () {
|
||||
|
||||
@@ -178,6 +178,8 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
separate_check_filters: true,
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
@@ -131,6 +131,8 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
separate_check_filters: true,
|
||||
|
||||
onload: function (report) {
|
||||
report.page.add_inner_button(__("Accounts Receivable"), function () {
|
||||
|
||||
@@ -232,11 +232,11 @@ def get_report_summary(
|
||||
|
||||
|
||||
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 = [], [], []
|
||||
|
||||
for p in columns[2:]:
|
||||
for p in columns[4:]:
|
||||
if asset:
|
||||
asset_data.append(asset[-2].get(p.get("fieldname")))
|
||||
if liability:
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe.query_builder import Criterion, Tuple
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import getdate, nowdate
|
||||
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 (
|
||||
get_accounting_dimensions,
|
||||
@@ -84,10 +84,8 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
match_conditions = build_match_conditions(party_type)
|
||||
|
||||
if match_conditions:
|
||||
query = query.where(LiteralValue(match_conditions))
|
||||
if match_conditions := build_match_conditions(party_type):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
party_details = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import frappe
|
||||
from frappe import _
|
||||
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 pypika.terms import ExistsCriterion
|
||||
from pypika.terms import Bracket, ExistsCriterion, LiteralValue
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
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)
|
||||
query = query.where(ExistsCriterion(account_filter_query))
|
||||
|
||||
if group_by_account:
|
||||
query = query.groupby("account")
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += "and" + match_conditions
|
||||
|
||||
if group_by_account:
|
||||
query += " GROUP BY `account`"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):
|
||||
|
||||
@@ -224,7 +224,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
},
|
||||
],
|
||||
collapsible_filters: true,
|
||||
seperate_check_filters: true,
|
||||
separate_check_filters: true,
|
||||
};
|
||||
|
||||
erpnext.utils.add_dimensions("General Ledger", 15);
|
||||
|
||||
@@ -324,10 +324,8 @@ def get_conditions(filters):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
match_conditions = build_match_conditions("GL Entry")
|
||||
|
||||
if match_conditions:
|
||||
conditions.append(match_conditions)
|
||||
if match_conditions := build_match_conditions("GL Entry"):
|
||||
conditions.append(f"({match_conditions})")
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
|
||||
@@ -5,15 +5,16 @@ from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
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 pypika.terms import ExistsCriterion
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_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.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()
|
||||
|
||||
# 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
|
||||
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")
|
||||
@@ -203,7 +206,11 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
|
||||
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(
|
||||
frappe._dict(
|
||||
{
|
||||
@@ -215,7 +222,7 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
"buying_amount": total_buying_amount,
|
||||
"gross_profit": total_gross_profit,
|
||||
"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,
|
||||
)
|
||||
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)
|
||||
|
||||
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
|
||||
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 = {
|
||||
group_columns[0]: "Total",
|
||||
@@ -581,10 +592,15 @@ class GrossProfitGenerator:
|
||||
base_amount += row.base_amount
|
||||
|
||||
# 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:
|
||||
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,
|
||||
)
|
||||
else:
|
||||
@@ -673,9 +689,14 @@ class GrossProfitGenerator:
|
||||
return 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 = (
|
||||
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
|
||||
else 0
|
||||
)
|
||||
@@ -851,129 +872,173 @@ class GrossProfitGenerator:
|
||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||
|
||||
def load_invoice_items(self):
|
||||
conditions = ""
|
||||
if self.filters.company:
|
||||
conditions += " and `tabSales Invoice`.company = %(company)s"
|
||||
if self.filters.from_date:
|
||||
conditions += " and posting_date >= %(from_date)s"
|
||||
if self.filters.to_date:
|
||||
conditions += " and posting_date <= %(to_date)s"
|
||||
self.si_list = []
|
||||
|
||||
SalesInvoice = frappe.qb.DocType("Sales Invoice")
|
||||
base_query = self.prepare_invoice_query()
|
||||
|
||||
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:
|
||||
conditions += " and is_return = 0"
|
||||
invoice_query = base_query.where(SalesInvoice.is_return == 0)
|
||||
|
||||
if self.filters.item_group:
|
||||
conditions += f" and {get_item_group_condition(self.filters.item_group)}"
|
||||
self.si_list += invoice_query.run(as_dict=True)
|
||||
self.prepare_vouchers_to_ignore()
|
||||
|
||||
if self.filters.sales_person:
|
||||
conditions += """
|
||||
and exists(select 1
|
||||
from `tabSales Team` st
|
||||
where st.parent = `tabSales Invoice`.name
|
||||
and st.sales_person = %(sales_person)s)
|
||||
"""
|
||||
ret_invoice_query = base_query.where(
|
||||
(SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnotnull()
|
||||
)
|
||||
if self.vouchers_to_ignore:
|
||||
ret_invoice_query = ret_invoice_query.where(
|
||||
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":
|
||||
sales_person_cols = """, sales.sales_person,
|
||||
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount,
|
||||
sales.incentives
|
||||
"""
|
||||
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
|
||||
else:
|
||||
sales_person_cols = ""
|
||||
sales_team_table = ""
|
||||
query = query.select(
|
||||
SalesTeam.sales_person,
|
||||
(SalesTeam.allocated_percentage * SalesInvoiceItem.base_net_amount / 100).as_(
|
||||
"allocated_amount"
|
||||
),
|
||||
SalesTeam.incentives,
|
||||
)
|
||||
|
||||
query = query.left_join(SalesTeam).on(SalesTeam.parent == SalesInvoice.name)
|
||||
|
||||
if self.filters.group_by == "Payment Term":
|
||||
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
|
||||
'{}',
|
||||
coalesce(schedule.payment_term, '{}')) as payment_term,
|
||||
schedule.invoice_portion,
|
||||
schedule.payment_amount """.format(_("Sales Return"), _("No Terms"))
|
||||
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
|
||||
`tabSales Invoice`.is_return = 0 """
|
||||
else:
|
||||
payment_term_cols = ""
|
||||
payment_term_table = ""
|
||||
query = query.select(
|
||||
Case()
|
||||
.when(SalesInvoice.is_return == 1, _("Sales Return"))
|
||||
.else_(Coalesce(PaymentSchedule.payment_term, _("No Terms")))
|
||||
.as_("payment_term"),
|
||||
PaymentSchedule.invoice_portion,
|
||||
PaymentSchedule.payment_amount,
|
||||
)
|
||||
|
||||
if self.filters.get("sales_invoice"):
|
||||
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
||||
query = query.left_join(PaymentSchedule).on(
|
||||
(PaymentSchedule.parent == SalesInvoice.name) & (SalesInvoice.is_return == 0)
|
||||
)
|
||||
|
||||
if self.filters.get("item_code"):
|
||||
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s"
|
||||
query = query.orderby(SalesInvoice.posting_date, order=Order.desc).orderby(
|
||||
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 = 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"))
|
||||
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)
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
if self.filters.get(dimension.fieldname):
|
||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
||||
dimension.document_type, self.filters.get(dimension.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"
|
||||
)
|
||||
for dim in get_accounting_dimensions(as_list=False) or []:
|
||||
if self.filters.get(dim.fieldname):
|
||||
if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
|
||||
self.filters[dim.fieldname] = get_dimension_with_children(
|
||||
dim.document_type, self.filters.get(dim.fieldname)
|
||||
)
|
||||
query = query.where(SalesInvoiceItem[dim.fieldname].isin(self.filters[dim.fieldname]))
|
||||
|
||||
if self.filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
if self.filters.warehouse:
|
||||
lft, rgt = frappe.db.get_value("Warehouse", self.filters.warehouse, ["lft", "rgt"])
|
||||
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(
|
||||
"""
|
||||
select
|
||||
`tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent,
|
||||
`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,
|
||||
)
|
||||
return query
|
||||
|
||||
def prepare_vouchers_to_ignore(self):
|
||||
self.vouchers_to_ignore = tuple(row["parent"] for row in self.si_list)
|
||||
|
||||
def get_delivery_notes(self):
|
||||
self.delivery_notes = frappe._dict({})
|
||||
|
||||
@@ -470,7 +470,7 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.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]
|
||||
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):
|
||||
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
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = month_start_date
|
||||
sinv.posting_date = sales_inv_date
|
||||
sinv.save().submit()
|
||||
|
||||
# create credit note on next month start date
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
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()
|
||||
|
||||
# apply filters for invoiced period
|
||||
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)
|
||||
@@ -675,7 +678,7 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
# 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)
|
||||
total = data[-1]
|
||||
@@ -684,3 +687,63 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from pypika.terms import Bracket, LiteralValue
|
||||
|
||||
import erpnext
|
||||
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
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
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():
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.query_builder import functions as fn
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
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.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):
|
||||
invoice = f"`tab{doctype}`"
|
||||
invoice_item = f"`tab{doctype} Item`"
|
||||
invoice = frappe.qb.DocType(doctype)
|
||||
invoice_item = frappe.qb.DocType(f"{doctype} Item")
|
||||
|
||||
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":
|
||||
query += f" order by {invoice_item}.parent desc"
|
||||
query = query.orderby(invoice_item.parent, order=Order.desc)
|
||||
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":
|
||||
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"):
|
||||
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
|
||||
|
||||
@@ -481,15 +483,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions(doctype)
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
if match_conditions := build_match_conditions(doctype):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
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):
|
||||
|
||||
@@ -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):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
labels = [d.get("label") for d in columns[4:]]
|
||||
|
||||
income_data, expense_data, net_profit = [], [], []
|
||||
|
||||
for p in columns[2:]:
|
||||
for p in columns[4:]:
|
||||
if income:
|
||||
income_data.append(income[-2].get(p.get("fieldname")))
|
||||
if expense:
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
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):
|
||||
if based_on == "Cost Center":
|
||||
return frappe.db.sql(
|
||||
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
|
||||
from `tabCost Center` where company=%s order by name""",
|
||||
company,
|
||||
as_dict=True,
|
||||
cc = qb.DocType("Cost Center")
|
||||
return (
|
||||
qb.from_(cc)
|
||||
.select(
|
||||
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":
|
||||
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
|
||||
):
|
||||
"""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:
|
||||
additional_conditions.append("and voucher_type !='Period Closing Voucher'")
|
||||
conditions.append(gl.voucher_type.ne("Period Closing Voucher"))
|
||||
|
||||
if from_date:
|
||||
additional_conditions.append("and posting_date >= %(from_date)s")
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select posting_date, {based_on} as based_on, debit, credit,
|
||||
is_opening, (select root_type from `tabAccount` where name = account) as type
|
||||
from `tabGL Entry` where company=%(company)s
|
||||
{additional_conditions}
|
||||
and posting_date <= %(to_date)s
|
||||
and {based_on} is not null
|
||||
and is_cancelled = 0
|
||||
order by {based_on}, posting_date""".format(
|
||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
||||
),
|
||||
{"company": company, "from_date": from_date, "to_date": to_date},
|
||||
as_dict=True,
|
||||
root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account))
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
.select(
|
||||
gl.posting_date,
|
||||
gl[based_on].as_("based_on"),
|
||||
gl.debit,
|
||||
gl.credit,
|
||||
gl.is_opening,
|
||||
root_subquery.as_("type"),
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(gl[based_on], gl.posting_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
|
||||
@@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika.terms import Bracket, LiteralValue, Order
|
||||
|
||||
from erpnext.accounts.party import get_party_account
|
||||
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
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Purchase Invoice")
|
||||
if match_conditions := build_match_conditions("Purchase Invoice"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
query = query.orderby("posting_date", order=Order.desc)
|
||||
query = query.orderby("name", order=Order.desc)
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _, msgprint
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika.terms import Bracket, LiteralValue, Order
|
||||
|
||||
from erpnext.accounts.party import get_party_account
|
||||
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
|
||||
|
||||
query, params = query.walk()
|
||||
match_conditions = build_match_conditions("Sales Invoice")
|
||||
if match_conditions := build_match_conditions("Sales Invoice"):
|
||||
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||
|
||||
if match_conditions:
|
||||
query += " and " + match_conditions
|
||||
query = query.orderby("posting_date", order=Order.desc)
|
||||
query = query.orderby("name", order=Order.desc)
|
||||
|
||||
query += " order by posting_date desc, name desc"
|
||||
|
||||
return frappe.db.sql(query, params, as_dict=True)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(filters, query, doctype):
|
||||
|
||||
@@ -154,17 +154,11 @@ def get_columns(filters):
|
||||
"width": 60,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"label": _("Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Base Total"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Tax Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
@@ -172,10 +166,16 @@ def get_columns(filters):
|
||||
"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",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
"width": 170,
|
||||
},
|
||||
{
|
||||
"label": _("Reference Date"),
|
||||
|
||||
@@ -106,7 +106,7 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"label": _("Total Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
|
||||
@@ -11,6 +11,7 @@ import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
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.functions import Count, IfNull, Max, Round, Sum
|
||||
from frappe.query_builder.utils import DocType
|
||||
@@ -25,6 +26,7 @@ from frappe.utils import (
|
||||
get_number_format_info,
|
||||
getdate,
|
||||
now,
|
||||
now_datetime,
|
||||
nowdate,
|
||||
)
|
||||
from frappe.utils.caching import site_cache
|
||||
@@ -66,6 +68,7 @@ def get_fiscal_year(
|
||||
as_dict=False,
|
||||
boolean=None,
|
||||
raise_on_missing=True,
|
||||
truncate=False,
|
||||
):
|
||||
if isinstance(raise_on_missing, str):
|
||||
raise_on_missing = loads(raise_on_missing)
|
||||
@@ -79,7 +82,14 @@ def get_fiscal_year(
|
||||
fiscal_years = get_fiscal_years(
|
||||
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(
|
||||
@@ -547,6 +557,7 @@ def reconcile_against_document(
|
||||
doc.make_advance_gl_entries(entry=row)
|
||||
else:
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
_delete_adv_pl_entries(voucher_type, voucher_no)
|
||||
gl_map = doc.build_gl_map()
|
||||
# Make sure there is no overallocation
|
||||
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["unadjusted_amount"] = d["unadjusted_amount"] * -1
|
||||
|
||||
insert_position = -1
|
||||
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
||||
# adjust the unreconciled balance
|
||||
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:
|
||||
journal_entry.remove(jv_detail)
|
||||
insert_position += jv_detail.idx
|
||||
|
||||
# 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
|
||||
[
|
||||
@@ -1500,14 +1513,14 @@ def get_autoname_with_number(number_value, doc_title, company):
|
||||
|
||||
|
||||
def parse_naming_series_variable(doc, variable):
|
||||
if variable == "FY":
|
||||
if variable in ["FY", "TFY"]:
|
||||
if doc:
|
||||
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
|
||||
company = doc.get("company")
|
||||
else:
|
||||
date = getdate()
|
||||
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":
|
||||
if doc:
|
||||
@@ -1517,6 +1530,18 @@ def parse_naming_series_variable(doc, variable):
|
||||
|
||||
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()
|
||||
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,
|
||||
party_type=gle.party_type,
|
||||
party=gle.party,
|
||||
project=gle.project,
|
||||
cost_center=gle.cost_center,
|
||||
finance_book=gle.finance_book,
|
||||
due_date=gle.due_date,
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
"for_user": "",
|
||||
"hide_custom": 0,
|
||||
"icon": "accounting",
|
||||
"idx": 3,
|
||||
"idx": 4,
|
||||
"indicator_color": "",
|
||||
"is_hidden": 0,
|
||||
"label": "Accounting",
|
||||
"label": "Invoicing",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
@@ -587,10 +587,10 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-24 13:20:34.857205",
|
||||
"modified": "2026-01-23 11:05:47.246213",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
"name": "Invoicing",
|
||||
"number_cards": [
|
||||
{
|
||||
"label": "Outgoing Bills",
|
||||
@@ -617,6 +617,6 @@
|
||||
"roles": [],
|
||||
"sequence_id": 2.0,
|
||||
"shortcuts": [],
|
||||
"title": "Accounting",
|
||||
"title": "Invoicing",
|
||||
"type": "Workspace"
|
||||
}
|
||||
@@ -6,11 +6,11 @@
|
||||
"docstatus": 0,
|
||||
"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()\"}",
|
||||
"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,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-10-28 23:16:16.939070",
|
||||
"modified": "2026-02-03 15:48:13.407835",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Category-wise Asset Value",
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
"docstatus": 0,
|
||||
"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()\"}",
|
||||
"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,
|
||||
"is_public": 1,
|
||||
"is_standard": 1,
|
||||
"modified": "2020-10-28 23:16:07.883312",
|
||||
"modified": "2026-02-03 15:48:13.407835",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Location-wise Asset Value",
|
||||
|
||||
@@ -100,7 +100,7 @@ def get_charts(fiscal_year, year_start_date, year_end_date):
|
||||
"company": company,
|
||||
"status": "In Location",
|
||||
"group_by": "Asset Category",
|
||||
"is_existing_asset": 0,
|
||||
"asset_type": ["!=", "Existing Asset"],
|
||||
}
|
||||
),
|
||||
"type": "Donut",
|
||||
@@ -126,7 +126,12 @@ def get_charts(fiscal_year, year_start_date, year_end_date):
|
||||
"x_field": "location",
|
||||
"timeseries": 0,
|
||||
"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",
|
||||
"doctype": "Dashboard Chart",
|
||||
|
||||
@@ -81,23 +81,79 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
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."));
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frappe.ui.form.trigger("Asset", "is_existing_asset");
|
||||
refresh: async function (frm) {
|
||||
frappe.ui.form.trigger("Asset", "asset_type");
|
||||
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
|
||||
|
||||
let has_create_buttons = false;
|
||||
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 (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(
|
||||
__("Transfer Asset"),
|
||||
function () {
|
||||
erpnext.asset.transfer_asset(frm);
|
||||
},
|
||||
__("Manage")
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
@@ -105,7 +161,7 @@ frappe.ui.form.on("Asset", {
|
||||
function () {
|
||||
erpnext.asset.scrap_asset(frm);
|
||||
},
|
||||
__("Manage")
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
@@ -113,23 +169,7 @@ frappe.ui.form.on("Asset", {
|
||||
function () {
|
||||
frm.trigger("sell_asset");
|
||||
},
|
||||
__("Manage")
|
||||
);
|
||||
|
||||
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")
|
||||
__("Actions")
|
||||
);
|
||||
} else if (frm.doc.status == "Scrapped") {
|
||||
frm.add_custom_button(__("Restore Asset"), function () {
|
||||
@@ -137,39 +177,9 @@ frappe.ui.form.on("Asset", {
|
||||
}).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(
|
||||
__("Maintain Asset"),
|
||||
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"),
|
||||
__("Accounting Ledger"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
@@ -179,7 +189,7 @@ frappe.ui.form.on("Asset", {
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
__("Manage")
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +205,7 @@ frappe.ui.form.on("Asset", {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
|
||||
if (frm.doc.is_composite_asset) {
|
||||
if (frm.doc.asset_type == "Composite Asset") {
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
|
||||
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) {
|
||||
const alert = `
|
||||
<div class="row">
|
||||
@@ -232,7 +264,8 @@ frappe.ui.form.on("Asset", {
|
||||
|
||||
toggle_reference_doc: function (frm) {
|
||||
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) => {
|
||||
if (frm.doc[field]) {
|
||||
@@ -508,15 +541,13 @@ frappe.ui.form.on("Asset", {
|
||||
});
|
||||
},
|
||||
|
||||
is_existing_asset: function (frm) {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
},
|
||||
|
||||
is_composite_asset: function (frm) {
|
||||
if (frm.doc.is_composite_asset) {
|
||||
frm.set_value("net_purchase_amount", 0);
|
||||
} else {
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
||||
asset_type: function (frm) {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
if (frm.doc.asset_type == "Composite Asset") {
|
||||
frm.set_value("net_purchase_amount", 0);
|
||||
} else {
|
||||
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
||||
}
|
||||
}
|
||||
frm.trigger("toggle_reference_doc");
|
||||
},
|
||||
|
||||
@@ -9,20 +9,17 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"company",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"asset_name",
|
||||
"asset_category",
|
||||
"location",
|
||||
"image",
|
||||
"column_break_3",
|
||||
"status",
|
||||
"company",
|
||||
"asset_owner",
|
||||
"asset_owner_company",
|
||||
"is_existing_asset",
|
||||
"is_composite_asset",
|
||||
"is_composite_component",
|
||||
"location",
|
||||
"asset_category",
|
||||
"asset_type",
|
||||
"maintenance_required",
|
||||
"calculate_depreciation",
|
||||
"purchase_details_section",
|
||||
"purchase_receipt",
|
||||
"purchase_receipt_item",
|
||||
@@ -30,31 +27,44 @@
|
||||
"purchase_invoice_item",
|
||||
"purchase_date",
|
||||
"available_for_use_date",
|
||||
"disposal_date",
|
||||
"column_break_23",
|
||||
"net_purchase_amount",
|
||||
"purchase_amount",
|
||||
"asset_quantity",
|
||||
"additional_asset_cost",
|
||||
"section_break_uiyd",
|
||||
"column_break_bbwr",
|
||||
"column_break_bfkm",
|
||||
"total_asset_cost",
|
||||
"disposal_date",
|
||||
"depreciation_tab",
|
||||
"calculate_depreciation",
|
||||
"column_break_33",
|
||||
"column_break_wqzi",
|
||||
"opening_accumulated_depreciation",
|
||||
"opening_number_of_booked_depreciations",
|
||||
"is_fully_depreciated",
|
||||
"column_break_33",
|
||||
"opening_number_of_booked_depreciations",
|
||||
"section_break_36",
|
||||
"finance_books",
|
||||
"section_break_33",
|
||||
"depreciation_method",
|
||||
"value_after_depreciation",
|
||||
"total_number_of_depreciations",
|
||||
"column_break_24",
|
||||
"frequency_of_depreciation",
|
||||
"column_break_24",
|
||||
"next_depreciation_date",
|
||||
"total_number_of_depreciations",
|
||||
"depreciation_schedule_sb",
|
||||
"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",
|
||||
"insurer",
|
||||
"insured_value",
|
||||
@@ -62,22 +72,17 @@
|
||||
"insurance_start_date",
|
||||
"insurance_end_date",
|
||||
"comprehensive_insurance",
|
||||
"other_info_tab",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"section_break_jtou",
|
||||
"status",
|
||||
"custodian",
|
||||
"department",
|
||||
"default_finance_book",
|
||||
"depr_entry_posting_status",
|
||||
"booked_fixed_asset",
|
||||
"customer",
|
||||
"supplier",
|
||||
"column_break_51",
|
||||
"department",
|
||||
"split_from",
|
||||
"journal_entry_for_scrap",
|
||||
"split_from",
|
||||
"amended_from",
|
||||
"maintenance_required",
|
||||
"booked_fixed_asset",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -106,13 +111,6 @@
|
||||
"options": "Item",
|
||||
"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",
|
||||
"fetch_from": "item_code.asset_category",
|
||||
@@ -207,7 +205,7 @@
|
||||
"fieldname": "purchase_date",
|
||||
"fieldtype": "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
|
||||
},
|
||||
{
|
||||
@@ -229,25 +227,18 @@
|
||||
{
|
||||
"fieldname": "available_for_use_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Available-for-use Date",
|
||||
"mandatory_depends_on": "eval:(!(doc.is_composite_component || doc.is_composite_asset) || doc.docstatus==1)"
|
||||
"label": "Available for Use Date",
|
||||
"mandatory_depends_on": "eval:(!(doc.asset_type == \"Composite Component\" || doc.asset_type == \"Composite Asset\") || doc.docstatus==1)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "calculate_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"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.is_composite_asset && !doc.is_composite_component)",
|
||||
"fieldname": "is_existing_asset",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Existing Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"depends_on": "eval:(doc.asset_type == \"Existing Asset\")",
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Opening Accumulated Depreciation",
|
||||
@@ -257,18 +248,20 @@
|
||||
"columns": 10,
|
||||
"fieldname": "finance_books",
|
||||
"fieldtype": "Table",
|
||||
"label": "Finance Books",
|
||||
"options": "Asset Finance Book"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_33",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1
|
||||
"hidden": 1,
|
||||
"label": "Depreciation Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "depreciation_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Depreciation Method",
|
||||
"options": "\nStraight Line\nDouble Declining Balance\nManual"
|
||||
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual"
|
||||
},
|
||||
{
|
||||
"fieldname": "value_after_depreciation",
|
||||
@@ -295,6 +288,7 @@
|
||||
{
|
||||
"fieldname": "next_depreciation_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"label": "Next Depreciation Date",
|
||||
"no_copy": 1
|
||||
},
|
||||
@@ -364,7 +358,7 @@
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Receipt",
|
||||
@@ -373,7 +367,7 @@
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Invoice",
|
||||
@@ -399,7 +393,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible_depends_on": "is_existing_asset",
|
||||
"collapsible_depends_on": "eval:doc.asset_type == \"Existing Asset\"",
|
||||
"fieldname": "purchase_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Purchase Details"
|
||||
@@ -413,10 +407,9 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "calculate_depreciation",
|
||||
"depends_on": "eval: doc.calculate_depreciation",
|
||||
"fieldname": "section_break_36",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Finance Books"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "split_from",
|
||||
@@ -455,18 +448,11 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "is_fully_depreciated",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"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",
|
||||
"fieldname": "total_asset_cost",
|
||||
@@ -496,7 +482,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"depends_on": "eval:(doc.asset_type == \"Existing Asset\")",
|
||||
"fieldname": "opening_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Opening Number of Booked Depreciations"
|
||||
@@ -513,15 +499,10 @@
|
||||
"hidden": 1,
|
||||
"label": "Purchase Invoice Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "insurance_details_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Insurance"
|
||||
},
|
||||
{
|
||||
"fieldname": "other_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Other Info"
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "connections_tab",
|
||||
@@ -530,6 +511,7 @@
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.calculate_depreciation || doc.asset_type == \"Existing Asset\"",
|
||||
"fieldname": "depreciation_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Depreciation"
|
||||
@@ -544,20 +526,61 @@
|
||||
"fieldtype": "Section Break",
|
||||
"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",
|
||||
"fieldtype": "Currency",
|
||||
"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",
|
||||
"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,
|
||||
@@ -601,7 +624,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-18 16:36:40.904246",
|
||||
"modified": "2026-02-05 12:42:45.350216",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -56,6 +56,7 @@ class Asset(AccountsController):
|
||||
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
|
||||
asset_owner_company: DF.Link | None
|
||||
asset_quantity: DF.Int
|
||||
asset_type: DF.Literal["", "Existing Asset", "Composite Asset", "Composite Component"]
|
||||
available_for_use_date: DF.Date | None
|
||||
booked_fixed_asset: DF.Check
|
||||
calculate_depreciation: DF.Check
|
||||
@@ -67,7 +68,9 @@ class Asset(AccountsController):
|
||||
default_finance_book: DF.Link | None
|
||||
department: DF.Link | None
|
||||
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
|
||||
finance_books: DF.Table[AssetFinanceBook]
|
||||
frequency_of_depreciation: DF.Int
|
||||
@@ -76,9 +79,6 @@ class Asset(AccountsController):
|
||||
insurance_start_date: DF.Date | None
|
||||
insured_value: 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
|
||||
item_code: DF.Link
|
||||
item_name: DF.ReadOnly | None
|
||||
@@ -243,14 +243,20 @@ class Asset(AccountsController):
|
||||
self.set_total_booked_depreciations()
|
||||
|
||||
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."))
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_in_use_date()
|
||||
self.make_asset_movement()
|
||||
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()
|
||||
if self.calculate_depreciation and not self.split_from:
|
||||
convert_draft_asset_depr_schedules_into_active(self)
|
||||
@@ -265,7 +271,7 @@ class Asset(AccountsController):
|
||||
cancel_asset_depr_schedules(self)
|
||||
self.set_status()
|
||||
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)
|
||||
self.db_set("booked_fixed_asset", 0)
|
||||
add_asset_activity(self.name, _("Asset cancelled"))
|
||||
@@ -283,7 +289,7 @@ class Asset(AccountsController):
|
||||
add_asset_activity(self.name, _("Asset deleted"))
|
||||
|
||||
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
|
||||
|
||||
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))
|
||||
|
||||
def validate_item(self):
|
||||
@@ -372,7 +378,7 @@ class Asset(AccountsController):
|
||||
)
|
||||
|
||||
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"))
|
||||
|
||||
for d in self.finance_books:
|
||||
@@ -420,12 +426,15 @@ class Asset(AccountsController):
|
||||
non_depreciable_category = frappe.db.get_value(
|
||||
"Asset Category", self.asset_category, "non_depreciable_category"
|
||||
)
|
||||
if self.calculate_depreciation and non_depreciable_category:
|
||||
frappe.throw(
|
||||
_(
|
||||
"This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category."
|
||||
if self.calculate_depreciation:
|
||||
if non_depreciable_category:
|
||||
frappe.throw(
|
||||
_(
|
||||
"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):
|
||||
if self.net_purchase_amount:
|
||||
@@ -440,13 +449,13 @@ class Asset(AccountsController):
|
||||
if not self.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)
|
||||
|
||||
if is_cwip_accounting_enabled(self.asset_category):
|
||||
if (
|
||||
not self.is_existing_asset
|
||||
and not self.is_composite_asset
|
||||
not self.asset_type == "Existing Asset"
|
||||
and not self.asset_type == "Composite Asset"
|
||||
and not self.purchase_receipt
|
||||
and not self.purchase_invoice
|
||||
):
|
||||
@@ -475,7 +484,7 @@ class Asset(AccountsController):
|
||||
if self.is_fully_depreciated:
|
||||
frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
|
||||
|
||||
if self.is_existing_asset:
|
||||
if self.asset_type == "Existing Asset":
|
||||
return
|
||||
|
||||
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):
|
||||
if self.is_existing_asset:
|
||||
if self.asset_type == "Existing Asset":
|
||||
return
|
||||
|
||||
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_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_number_of_booked_depreciations = 0
|
||||
else:
|
||||
@@ -766,7 +775,7 @@ class Asset(AccountsController):
|
||||
def get_status(self):
|
||||
"""Returns status based on whether it is draft, submitted, scrapped or depreciated"""
|
||||
if self.docstatus == 0:
|
||||
if self.is_composite_asset:
|
||||
if self.asset_type == "Composite Asset":
|
||||
status = "Work In Progress"
|
||||
else:
|
||||
status = "Draft"
|
||||
@@ -786,13 +795,12 @@ class Asset(AccountsController):
|
||||
].expected_value_after_useful_life
|
||||
value_after_depreciation = self.finance_books[idx].value_after_depreciation
|
||||
|
||||
if (
|
||||
flt(value_after_depreciation) <= expected_value_after_useful_life
|
||||
or self.is_fully_depreciated
|
||||
):
|
||||
if flt(value_after_depreciation) <= expected_value_after_useful_life:
|
||||
status = "Fully Depreciated"
|
||||
elif flt(value_after_depreciation) < flt(self.net_purchase_amount):
|
||||
status = "Partially Depreciated"
|
||||
elif self.is_fully_depreciated:
|
||||
status = "Fully Depreciated"
|
||||
elif self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
return status
|
||||
@@ -839,7 +847,7 @@ class Asset(AccountsController):
|
||||
return records
|
||||
|
||||
def validate_make_gl_entry(self):
|
||||
if self.is_composite_asset:
|
||||
if self.asset_type == "Composite Asset":
|
||||
return True
|
||||
|
||||
purchase_document = self.get_purchase_document()
|
||||
@@ -920,7 +928,7 @@ class Asset(AccountsController):
|
||||
purchase_document = self.get_purchase_document()
|
||||
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
|
||||
) <= getdate():
|
||||
gl_entries.append(
|
||||
@@ -960,7 +968,7 @@ class Asset(AccountsController):
|
||||
self.db_set("booked_fixed_asset", 1)
|
||||
|
||||
def check_asset_capitalization_gl_entries(self):
|
||||
if self.is_composite_asset:
|
||||
if self.asset_type == "Composite Asset":
|
||||
result = frappe.db.get_value(
|
||||
"Asset Capitalization",
|
||||
{"target_asset": self.name, "docstatus": 1},
|
||||
@@ -1084,7 +1092,7 @@ def get_asset_naming_series():
|
||||
|
||||
|
||||
@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)
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
si.company = company
|
||||
@@ -1117,7 +1125,13 @@ def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
|
||||
|
||||
|
||||
@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.update(
|
||||
{
|
||||
@@ -1132,14 +1146,23 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
|
||||
|
||||
|
||||
@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.update({"company": company, "asset": asset, "asset_name": asset_name})
|
||||
return asset_repair
|
||||
|
||||
|
||||
@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.update(
|
||||
{
|
||||
@@ -1153,35 +1176,22 @@ def create_asset_capitalization(company, asset, asset_name, item_code):
|
||||
|
||||
|
||||
@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.update({"asset": asset, "company": company, "asset_category": asset_category})
|
||||
return asset_value_adjustment
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def transfer_asset(args):
|
||||
args = json.loads(args)
|
||||
|
||||
if args.get("serial_no"):
|
||||
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):
|
||||
def get_item_details(
|
||||
item_code: str,
|
||||
asset_category: str,
|
||||
net_purchase_amount: float,
|
||||
):
|
||||
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
|
||||
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()
|
||||
def make_journal_entry(asset_name):
|
||||
def make_journal_entry(asset_name: str):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
fixed_asset_account,
|
||||
@@ -1273,7 +1283,10 @@ def make_journal_entry(asset_name):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_asset_movement(assets, purpose=None):
|
||||
def make_asset_movement(
|
||||
assets: list[dict] | str,
|
||||
purpose: str = "Transfer",
|
||||
):
|
||||
import json
|
||||
|
||||
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."))
|
||||
|
||||
asset_movement = frappe.new_doc("Asset Movement")
|
||||
asset_movement.quantity = len(assets)
|
||||
asset_movement.purpose = purpose
|
||||
for asset in assets:
|
||||
asset = frappe.get_doc("Asset", asset.get("name"))
|
||||
asset_movement.company = asset.get("company")
|
||||
@@ -1305,7 +1318,10 @@ def is_cwip_accounting_enabled(asset_category):
|
||||
|
||||
|
||||
@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)
|
||||
if not asset.calculate_depreciation:
|
||||
return flt(asset.value_after_depreciation)
|
||||
@@ -1314,7 +1330,7 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_active_capitalization(asset):
|
||||
def has_active_capitalization(asset: str):
|
||||
active_capitalizations = frappe.db.count(
|
||||
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
|
||||
)
|
||||
@@ -1322,7 +1338,11 @@ def has_active_capitalization(asset):
|
||||
|
||||
|
||||
@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)
|
||||
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()
|
||||
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."""
|
||||
existing_asset = frappe.get_doc("Asset", asset_name)
|
||||
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):
|
||||
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.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
|
||||
asset_doc.opening_accumulated_depreciation = (
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Max, Min
|
||||
from frappe.utils import (
|
||||
DateTimeLikeObject,
|
||||
add_months,
|
||||
cint,
|
||||
flt,
|
||||
@@ -161,11 +162,11 @@ def get_depr_cost_center_and_series():
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_depreciation_entry(
|
||||
depr_schedule_name,
|
||||
date=None,
|
||||
sch_start_idx=None,
|
||||
sch_end_idx=None,
|
||||
accounting_dimensions=None,
|
||||
depr_schedule_name: str,
|
||||
date: DateTimeLikeObject | None = None,
|
||||
sch_start_idx: int | None = None,
|
||||
sch_end_idx: int | None = None,
|
||||
accounting_dimensions: list[dict] | None = None,
|
||||
):
|
||||
frappe.has_permission("Journal Entry", throw=True)
|
||||
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):
|
||||
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.company = asset.company
|
||||
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()
|
||||
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)
|
||||
scrap_date = getdate(scrap_date) or getdate(today())
|
||||
asset.db_set("disposal_date", scrap_date)
|
||||
@@ -443,7 +446,7 @@ def create_journal_entry_for_scrap(asset, scrap_date):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def restore_asset(asset_name):
|
||||
def restore_asset(asset_name: str):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
reverse_depreciation_entry_made_on_disposal(asset)
|
||||
reset_depreciation_schedule(asset, get_note_for_restore(asset))
|
||||
@@ -770,7 +773,7 @@ def get_profit_gl_entries(
|
||||
|
||||
|
||||
@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(
|
||||
"Company", company, ["disposal_account", "depreciation_cost_center"]
|
||||
)
|
||||
@@ -784,10 +787,14 @@ def get_disposal_account_and_cost_center(company):
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
if asset_doc.is_composite_component:
|
||||
if asset_doc.asset_type == "Composite Component":
|
||||
validate_disposal_date(asset_doc.purchase_date, getdate(disposal_date), "purchase")
|
||||
return flt(asset_doc.value_after_depreciation)
|
||||
|
||||
|
||||
@@ -70,16 +70,16 @@ class TestAsset(AssetSetup):
|
||||
self.assertRaises(frappe.MandatoryError, asset.save)
|
||||
|
||||
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.is_existing_asset = 0
|
||||
asset.asset_type = ""
|
||||
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
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.is_existing_asset = 0
|
||||
asset.asset_type = ""
|
||||
asset.purchase_date = getdate("2021-10-10")
|
||||
asset.available_for_use_date = getdate("2021-10-1")
|
||||
|
||||
@@ -182,7 +182,7 @@ class TestAsset(AssetSetup):
|
||||
asset.submit()
|
||||
|
||||
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.company = "_Test Company"
|
||||
doc.supplier = "_Test Supplier"
|
||||
@@ -709,7 +709,7 @@ class TestAsset(AssetSetup):
|
||||
# create an asset
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
calculate_depreciation=1,
|
||||
available_for_use_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)
|
||||
|
||||
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):
|
||||
@classmethod
|
||||
@@ -901,7 +987,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2030-06-06",
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
opening_number_of_booked_depreciations=2,
|
||||
opening_accumulated_depreciation=47178.08,
|
||||
expected_value_after_useful_life=10000,
|
||||
@@ -950,7 +1036,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2030-01-01",
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
depreciation_method="Double Declining Balance",
|
||||
opening_number_of_booked_depreciations=1,
|
||||
opening_accumulated_depreciation=50000,
|
||||
@@ -1691,7 +1777,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
self.assertEqual(asset.finance_books[0].value_after_depreciation, 100000.0)
|
||||
|
||||
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"
|
||||
|
||||
self.assertRaises(frappe.ValidationError, asset.submit)
|
||||
@@ -1728,7 +1814,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
def test_manual_depreciation_for_existing_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-01-30",
|
||||
submit=1,
|
||||
@@ -1828,6 +1914,71 @@ class TestDepreciationBasics(AssetSetup):
|
||||
pr.submit()
|
||||
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):
|
||||
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",
|
||||
"location": args.location or "Test Location",
|
||||
"asset_owner": args.asset_owner or "Company",
|
||||
"is_existing_asset": args.is_existing_asset or 1,
|
||||
"is_composite_asset": args.is_composite_asset or 0,
|
||||
"is_composite_component": args.is_composite_component or 0,
|
||||
"asset_type": args.asset_type or "Existing Asset",
|
||||
"asset_quantity": args.get("asset_quantity") or 1,
|
||||
"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.purchase_amount = 0
|
||||
|
||||
|
||||
@@ -17,10 +17,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
refresh() {
|
||||
this.show_general_ledger();
|
||||
|
||||
if (
|
||||
(this.frm.doc.stock_items && this.frm.doc.stock_items.length) ||
|
||||
!this.frm.doc.target_is_fixed_asset
|
||||
) {
|
||||
if (this.frm.doc.stock_items && this.frm.doc.stock_items.length) {
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
|
||||
@@ -41,7 +38,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
|
||||
me.frm.set_query("target_asset", function () {
|
||||
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();
|
||||
}
|
||||
|
||||
target_qty() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
rate() {
|
||||
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",
|
||||
child: item,
|
||||
args: {
|
||||
args: {
|
||||
ctx: {
|
||||
item_code: item.item_code,
|
||||
warehouse: cstr(item.warehouse),
|
||||
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.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.target_qty
|
||||
? me.frm.doc.total_value / flt(me.frm.doc.target_qty)
|
||||
: me.frm.doc.total_value;
|
||||
me.frm.doc.target_incoming_rate = me.frm.doc.total_value;
|
||||
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
|
||||
@@ -9,30 +9,33 @@
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"company",
|
||||
"target_asset",
|
||||
"target_asset_name",
|
||||
"target_item_code",
|
||||
"finance_book",
|
||||
"target_qty",
|
||||
"column_break_9",
|
||||
"company",
|
||||
"finance_book",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"set_posting_time",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"target_item_code",
|
||||
"amended_from",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"section_break_16",
|
||||
"stock_items",
|
||||
"section_break_urtz",
|
||||
"column_break_gqep",
|
||||
"column_break_yvlx",
|
||||
"stock_items_total",
|
||||
"section_break_26",
|
||||
"asset_items",
|
||||
"section_break_arbh",
|
||||
"column_break_boeu",
|
||||
"column_break_qecy",
|
||||
"asset_items_total",
|
||||
"service_expenses_section",
|
||||
"service_items",
|
||||
"section_break_ptna",
|
||||
"column_break_szvh",
|
||||
"column_break_katv",
|
||||
"service_items_total",
|
||||
"totals_section",
|
||||
"total_value",
|
||||
@@ -55,20 +58,12 @@
|
||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal)",
|
||||
"fieldname": "target_item_code",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Item Code",
|
||||
"options": "Item",
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
@@ -143,6 +138,7 @@
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Consumed Stock Items"
|
||||
},
|
||||
{
|
||||
@@ -151,49 +147,11 @@
|
||||
"label": "Stock Items",
|
||||
"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)",
|
||||
"fieldname": "section_break_26",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Consumed Assets"
|
||||
},
|
||||
{
|
||||
@@ -203,6 +161,7 @@
|
||||
"options": "Asset Capitalization Asset Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.stock_items_total",
|
||||
"fieldname": "stock_items_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Consumed Stock Total Value",
|
||||
@@ -210,6 +169,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.asset_items_total",
|
||||
"fieldname": "asset_items_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Consumed Asset Total Value",
|
||||
@@ -226,6 +186,7 @@
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
|
||||
"fieldname": "service_expenses_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Service Expenses"
|
||||
},
|
||||
{
|
||||
@@ -235,6 +196,7 @@
|
||||
"options": "Asset Capitalization Service Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.service_items_total",
|
||||
"fieldname": "service_items_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Service Expense Total Amount",
|
||||
@@ -277,10 +239,10 @@
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
@@ -292,12 +254,48 @@
|
||||
"label": "Target Fixed Asset Account",
|
||||
"options": "Account",
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-20 15:15:12.110035",
|
||||
"modified": "2026-02-06 01:52:41.890713",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
|
||||
@@ -38,9 +39,6 @@ force_fields = [
|
||||
"target_asset_name",
|
||||
"item_name",
|
||||
"asset_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_serial_no",
|
||||
"target_has_batch_no",
|
||||
"stock_uom",
|
||||
"fixed_asset_account",
|
||||
"valuation_rate",
|
||||
@@ -75,6 +73,7 @@ class AssetCapitalization(StockController):
|
||||
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
|
||||
posting_date: DF.Date
|
||||
posting_time: DF.Time
|
||||
project: DF.Link | None
|
||||
service_items: DF.Table[AssetCapitalizationServiceItem]
|
||||
service_items_total: DF.Currency
|
||||
set_posting_time: DF.Check
|
||||
@@ -82,15 +81,9 @@ class AssetCapitalization(StockController):
|
||||
stock_items_total: DF.Currency
|
||||
target_asset: DF.Link | None
|
||||
target_asset_name: DF.Data | None
|
||||
target_batch_no: 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_is_fixed_asset: DF.Check
|
||||
target_item_code: DF.Link | None
|
||||
target_qty: DF.Float
|
||||
target_serial_no: DF.SmallText | None
|
||||
title: DF.Data | None
|
||||
total_value: DF.Currency
|
||||
# end: auto-generated types
|
||||
@@ -189,22 +182,13 @@ class AssetCapitalization(StockController):
|
||||
if not target_item.is_fixed_asset:
|
||||
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)
|
||||
|
||||
def validate_target_asset(self):
|
||||
if 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))
|
||||
|
||||
if target_asset.item_code != self.target_item_code:
|
||||
@@ -313,7 +297,7 @@ class AssetCapitalization(StockController):
|
||||
return frappe.db.get_value(
|
||||
"Asset",
|
||||
asset,
|
||||
["name", "item_code", "company", "status", "docstatus", "is_composite_asset"],
|
||||
["name", "item_code", "company", "status", "docstatus", "asset_type"],
|
||||
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 = 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_qty
|
||||
self.target_incoming_rate = self.total_value
|
||||
|
||||
def update_stock_ledger(self):
|
||||
sl_entries = []
|
||||
@@ -488,7 +471,7 @@ class AssetCapitalization(StockController):
|
||||
for item in self.asset_items:
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
|
||||
if not asset.is_composite_component:
|
||||
if asset.asset_type != "Composite Component":
|
||||
if asset.calculate_depreciation:
|
||||
notes = _(
|
||||
"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):
|
||||
composite_component_value = 0
|
||||
for item in self.asset_items:
|
||||
asset = frappe.db.get_value("Asset", item.asset, ["is_composite_component"], as_dict=True)
|
||||
if asset and asset.is_composite_component:
|
||||
asset = frappe.db.get_value("Asset", item.asset, ["asset_type"], as_dict=True)
|
||||
if asset and asset.asset_type == "Composite Component":
|
||||
composite_component_value += flt(item.asset_value, item.precision("asset_value"))
|
||||
return composite_component_value
|
||||
|
||||
def get_gl_entries_for_target_item(
|
||||
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)
|
||||
if total_value:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": total_value,
|
||||
"cost_center": self.get("cost_center"),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
total_value = flt(self.total_value - composite_component_value, precision)
|
||||
if total_value:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": target_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": total_value,
|
||||
"cost_center": self.get("cost_center"),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def update_target_asset(self):
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
@@ -573,13 +555,19 @@ class AssetCapitalization(StockController):
|
||||
if self.docstatus == 2:
|
||||
net_purchase_amount = asset_doc.net_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:
|
||||
net_purchase_amount = asset_doc.net_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("purchase_amount", purchase_amount)
|
||||
asset_doc.db_set(
|
||||
{
|
||||
"net_purchase_amount": net_purchase_amount,
|
||||
"purchase_amount": purchase_amount,
|
||||
"total_asset_cost": total_asset_cost,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("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):
|
||||
if self.docstatus == 1:
|
||||
if self.target_is_fixed_asset:
|
||||
asset.set_status("Capitalized")
|
||||
add_asset_activity(
|
||||
asset.name,
|
||||
_("Asset capitalized after Asset Capitalization {0} was submitted").format(
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
asset.set_status("Capitalized")
|
||||
add_asset_activity(
|
||||
asset.name,
|
||||
_("Asset capitalized after Asset Capitalization {0} was submitted").format(
|
||||
get_link_to_form("Asset Capitalization", self.name)
|
||||
),
|
||||
)
|
||||
else:
|
||||
asset.set_status()
|
||||
add_asset_activity(
|
||||
@@ -623,7 +610,7 @@ class AssetCapitalization(StockController):
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
# Get Item Details
|
||||
@@ -633,17 +620,6 @@ def get_target_item_details(item_code=None, company=None):
|
||||
|
||||
# Set Item Details
|
||||
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
|
||||
item_defaults = get_item_defaults(item.name, company)
|
||||
@@ -660,7 +636,7 @@ def get_target_item_details(item_code=None, company=None):
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
# Get Asset Details
|
||||
@@ -735,24 +711,22 @@ def get_consumed_stock_item_details(ctx: ItemDetailsCtx):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_warehouse_details(args):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
out = {}
|
||||
if args.warehouse and args.item_code:
|
||||
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),
|
||||
}
|
||||
@erpnext.normalize_ctx_input(ItemDetailsCtx)
|
||||
def get_warehouse_details(ctx: ItemDetailsCtx) -> frappe._dict:
|
||||
out = frappe._dict()
|
||||
if ctx.warehouse and ctx.item_code:
|
||||
out = frappe._dict(
|
||||
{
|
||||
"actual_qty": get_previous_sle(ctx).get("qty_after_transaction") or 0,
|
||||
"valuation_rate": get_incoming_rate(ctx, raise_error_if_no_rate=False),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@erpnext.normalize_ctx_input(ItemDetailsCtx)
|
||||
def get_consumed_asset_details(ctx):
|
||||
def get_consumed_asset_details(ctx: ItemDetailsCtx) -> frappe._dict:
|
||||
out = frappe._dict()
|
||||
|
||||
asset_details = frappe._dict()
|
||||
@@ -798,7 +772,7 @@ def get_consumed_asset_details(ctx):
|
||||
|
||||
@frappe.whitelist()
|
||||
@erpnext.normalize_ctx_input(ItemDetailsCtx)
|
||||
def get_service_item_details(ctx):
|
||||
def get_service_item_details(ctx: ItemDetailsCtx) -> frappe._dict:
|
||||
out = frappe._dict()
|
||||
|
||||
item = frappe._dict()
|
||||
@@ -820,7 +794,7 @@ def get_service_item_details(ctx):
|
||||
|
||||
|
||||
@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):
|
||||
params = json.loads(params)
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||
from erpnext.assets.doctype.asset.test_asset import (
|
||||
create_asset,
|
||||
create_asset_data,
|
||||
create_fixed_asset_item,
|
||||
set_depreciation_settings_in_company,
|
||||
)
|
||||
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 (
|
||||
make_serial_batch_bundle,
|
||||
)
|
||||
@@ -57,7 +59,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
asset_type="Composite Asset",
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
@@ -77,7 +79,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
)
|
||||
|
||||
# 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].amount, stock_amount)
|
||||
@@ -152,7 +153,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
asset_type="Composite Asset",
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
@@ -172,8 +173,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
)
|
||||
|
||||
# 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].amount, stock_amount)
|
||||
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
||||
@@ -241,7 +240,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
asset_type="Composite Asset",
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
@@ -258,8 +257,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
)
|
||||
|
||||
# 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].amount, stock_amount)
|
||||
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
||||
@@ -309,7 +306,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
asset_type="Composite Asset",
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
@@ -357,33 +354,45 @@ class TestAssetCapitalization(IntegrationTestCase):
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
asset_type="Composite Asset",
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
|
||||
consumed_asset_value = 100000
|
||||
|
||||
consumed_asset = create_asset(
|
||||
asset_name="Asset Capitalization Consumable Asset",
|
||||
asset_value=consumed_asset_value,
|
||||
submit=1,
|
||||
warehouse="Stores - _TC",
|
||||
is_composite_component=1,
|
||||
item = create_fixed_asset_item("Asset Capitalization Consumable Asset")
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item.item_code,
|
||||
qty=1,
|
||||
rate=consumed_asset_value,
|
||||
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
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
consumed_asset=consumed_asset.name,
|
||||
consumed_asset=consumed_asset_doc.name,
|
||||
company=company,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
|
||||
|
||||
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_asset": target_asset.name,
|
||||
"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,
|
||||
}
|
||||
)
|
||||
@@ -512,7 +518,7 @@ def create_depreciation_asset(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
asset = frappe.new_doc("Asset")
|
||||
asset.is_existing_asset = 1
|
||||
asset.asset_type = args.asset_type or "Existing Asset"
|
||||
asset.calculate_depreciation = 1
|
||||
asset.asset_owner = "Company"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class AssetCategory(Document):
|
||||
self.validate_finance_books()
|
||||
self.validate_account_types()
|
||||
self.validate_account_currency()
|
||||
self.valide_cwip_account()
|
||||
self.validate_accounts()
|
||||
|
||||
def validate_finance_books(self):
|
||||
for d in self.finance_books:
|
||||
@@ -97,11 +97,21 @@ class AssetCategory(Document):
|
||||
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:
|
||||
missing_cwip_accounts_for_company = []
|
||||
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"
|
||||
):
|
||||
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"))
|
||||
|
||||
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(
|
||||
fieldname, item=None, asset=None, account=None, asset_category=None, company=None
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset
|
||||
|
||||
|
||||
class TestAssetCategory(IntegrationTestCase):
|
||||
def test_mandatory_fields(self):
|
||||
@@ -50,3 +52,67 @@ class TestAssetCategory(IntegrationTestCase):
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
@@ -271,7 +271,7 @@ def get_asset_shift_factors_map():
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
if not asset_depr_schedule_doc:
|
||||
@@ -281,13 +281,13 @@ def get_depr_schedule(asset_name, status, finance_book=None):
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
if not asset_depr_schedule:
|
||||
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
|
||||
|
||||
@@ -299,21 +299,23 @@ def get_asset_depr_schedule_name(asset_name, status=None, finance_book=None):
|
||||
]
|
||||
|
||||
if status:
|
||||
if isinstance(status, str):
|
||||
status = [status]
|
||||
filters.append(["status", "in", status])
|
||||
status_list = [status] if isinstance(status, str) else status
|
||||
filters.append(["status", "in", status_list])
|
||||
|
||||
if finance_book:
|
||||
filters.append(["finance_book", "=", finance_book])
|
||||
else:
|
||||
filters.append(["finance_book", "is", "not set"])
|
||||
finance_book_filter = (
|
||||
["finance_book", "=", finance_book] if finance_book else ["finance_book", "is", "not set"]
|
||||
)
|
||||
filters.append(finance_book_filter)
|
||||
|
||||
return frappe.get_all(
|
||||
depreciation_schedules = frappe.get_all(
|
||||
doctype="Asset Depreciation Schedule",
|
||||
filters=filters,
|
||||
fields=["name"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
return depreciation_schedules[0].name if depreciation_schedules else None
|
||||
|
||||
|
||||
def is_first_day_of_the_month(date):
|
||||
first_day_of_the_month = get_first_day(date)
|
||||
|
||||
@@ -87,7 +87,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-10-10",
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
opening_number_of_booked_depreciations=9,
|
||||
opening_accumulated_depreciation=265,
|
||||
depreciation_start_date="2024-07-31",
|
||||
@@ -127,7 +127,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-10-10",
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
opening_number_of_booked_depreciations=9,
|
||||
opening_accumulated_depreciation=265.30,
|
||||
depreciation_start_date="2024-07-31",
|
||||
@@ -165,7 +165,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
calculate_depreciation=1,
|
||||
depreciation_method="Straight Line",
|
||||
available_for_use_date="2023-11-01",
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
opening_number_of_booked_depreciations=4,
|
||||
opening_accumulated_depreciation=223.15,
|
||||
depreciation_start_date="2024-12-31",
|
||||
@@ -529,7 +529,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2023-03-31",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=12,
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
opening_accumulated_depreciation=64.52,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
submit=1,
|
||||
@@ -851,7 +851,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2023-03-31",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=12,
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
opening_accumulated_depreciation=64.52,
|
||||
opening_number_of_booked_depreciations=2,
|
||||
submit=1,
|
||||
@@ -925,7 +925,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2021-12-31",
|
||||
frequency_of_depreciation=12,
|
||||
total_number_of_depreciations=3,
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
submit=1,
|
||||
)
|
||||
post_depreciation_entries(date="2021-12-31")
|
||||
@@ -1014,7 +1014,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
depreciation_start_date="2021-12-31",
|
||||
frequency_of_depreciation=12,
|
||||
total_number_of_depreciations=3,
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
submit=1,
|
||||
)
|
||||
post_depreciation_entries(date="2021-12-31")
|
||||
@@ -1093,7 +1093,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
||||
rate_of_depreciation=50,
|
||||
frequency_of_depreciation=12,
|
||||
total_number_of_depreciations=3,
|
||||
is_existing_asset=1,
|
||||
asset_type="Existing Asset",
|
||||
submit=1,
|
||||
)
|
||||
post_depreciation_entries(date="2021-12-31")
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.desk.form import assign_to
|
||||
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):
|
||||
@@ -90,7 +92,11 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
|
||||
|
||||
@frappe.whitelist()
|
||||
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:
|
||||
start_date = frappe.utils.now()
|
||||
@@ -164,19 +170,30 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task):
|
||||
|
||||
@frappe.whitelist()
|
||||
@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(
|
||||
"Maintenance Team Member", {"parent": filters.get("maintenance_team")}, "team_member"
|
||||
"Maintenance Team Member",
|
||||
{"parent": filters.get("maintenance_team")},
|
||||
"team_member",
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_maintenance_log(asset_name):
|
||||
def get_maintenance_log(asset_name: str):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select maintenance_status, count(asset_name) as count, asset_name
|
||||
from `tabAsset Maintenance Log`
|
||||
where asset_name=%s group by maintenance_status""",
|
||||
(asset_name),
|
||||
where asset_name=%s
|
||||
group by maintenance_status
|
||||
""",
|
||||
(asset_name,),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
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
|
||||
|
||||
@@ -34,6 +34,7 @@ class AssetMovement(Document):
|
||||
for d in self.assets:
|
||||
self.validate_asset(d)
|
||||
self.validate_movement(d)
|
||||
self.validate_transaction_date(d)
|
||||
|
||||
def validate_asset(self, d):
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
@@ -51,6 +52,18 @@ class AssetMovement(Document):
|
||||
else:
|
||||
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):
|
||||
self.validate_location(d)
|
||||
self.validate_employee(d)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
import frappe
|
||||
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.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@@ -146,6 +146,33 @@ class TestAssetMovement(IntegrationTestCase):
|
||||
movement1.cancel()
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
@@ -164,9 +191,10 @@ def create_asset_movement(**args):
|
||||
"reference_name": args.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
movement.insert()
|
||||
movement.submit()
|
||||
if not args.do_not_save:
|
||||
movement.insert(ignore_if_duplicate=True)
|
||||
if not args.do_not_submit:
|
||||
movement.submit()
|
||||
|
||||
return movement
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"company",
|
||||
"asset",
|
||||
"asset_name",
|
||||
"company",
|
||||
"column_break_2",
|
||||
"repair_status",
|
||||
"failure_date",
|
||||
@@ -28,10 +28,6 @@
|
||||
"column_break_ajbh",
|
||||
"column_break_hkem",
|
||||
"repair_cost",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_14",
|
||||
"project",
|
||||
"stock_consumption_details_section",
|
||||
"stock_items",
|
||||
"section_break_ltbb",
|
||||
@@ -43,7 +39,12 @@
|
||||
"capitalize_repair_cost",
|
||||
"increase_in_asset_life",
|
||||
"column_break_xebe",
|
||||
"total_repair_cost"
|
||||
"total_repair_cost",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_14",
|
||||
"project",
|
||||
"connection_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -149,8 +150,7 @@
|
||||
{
|
||||
"fieldname": "accounting_details",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Repair Purchase Invoices"
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_items",
|
||||
@@ -206,6 +206,7 @@
|
||||
{
|
||||
"fieldname": "invoices",
|
||||
"fieldtype": "Table",
|
||||
"label": "Repair Purchase Invoices",
|
||||
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;",
|
||||
"no_copy": 1,
|
||||
"options": "Asset Repair Purchase Invoice"
|
||||
@@ -244,6 +245,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.consumed_items_cost",
|
||||
"fieldname": "consumed_items_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Consumed Items Cost"
|
||||
@@ -256,7 +258,13 @@
|
||||
"depends_on": "capitalize_repair_cost",
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"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,
|
||||
@@ -267,7 +275,7 @@
|
||||
"link_fieldname": "asset_repair"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-06 15:48:13.862505",
|
||||
"modified": "2026-02-06 14:57:54.257572",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
|
||||
@@ -5,7 +5,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType
|
||||
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
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -448,14 +448,21 @@ class AssetRepair(AccountsController):
|
||||
|
||||
|
||||
@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)
|
||||
return round(downtime, 2)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@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.
|
||||
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.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.
|
||||
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()
|
||||
def get_unallocated_repair_cost(
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -210,26 +210,29 @@ class TestAssetRepair(IntegrationTestCase):
|
||||
self.assertRaises(frappe.ValidationError, asset_repair2.save)
|
||||
|
||||
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.append(
|
||||
"accounts",
|
||||
{
|
||||
"company_name": "_Test Company with perpetual inventory",
|
||||
"fixed_asset_account": "_Test Fixed Asset - 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()
|
||||
|
||||
if not any(row.company_name == company for row in asset_category.accounts):
|
||||
asset_category.append(
|
||||
"accounts",
|
||||
{
|
||||
"company_name": company,
|
||||
"fixed_asset_account": "_Test Fixed Asset - 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_repair = create_asset_repair(
|
||||
capitalize_repair_cost=1,
|
||||
stock_consumption=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
company=company,
|
||||
pi_expense_account1="Administrative Expenses - TCP1",
|
||||
pi_expense_account2="Legal Expenses - TCP1",
|
||||
item="_Test Non Stock Item",
|
||||
@@ -359,7 +362,7 @@ class TestAssetRepair(IntegrationTestCase):
|
||||
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
|
||||
|
||||
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=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
|
||||
)
|
||||
@@ -399,7 +402,7 @@ def create_asset_repair(**args):
|
||||
if args.asset:
|
||||
asset = args.asset
|
||||
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.update(
|
||||
{
|
||||
|
||||
@@ -227,6 +227,6 @@ class AssetValueAdjustment(Document):
|
||||
|
||||
|
||||
@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"]
|
||||
return frappe.db.get_value("Asset", asset_name, fieldname=dimension_fields, as_dict=True)
|
||||
|
||||
@@ -211,7 +211,7 @@ def _ring_area(coords):
|
||||
|
||||
|
||||
@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":
|
||||
parent = ""
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ def get_conditions(filters):
|
||||
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
|
||||
|
||||
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"):
|
||||
conditions["asset_category"] = filters.get("asset_category")
|
||||
if filters.get("cost_center"):
|
||||
@@ -273,7 +273,7 @@ def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
)
|
||||
|
||||
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:
|
||||
query = query.where(asset.asset_category == filters.asset_category)
|
||||
if filters.cost_center:
|
||||
@@ -324,7 +324,7 @@ def get_asset_value_adjustment_map(filters, finance_book):
|
||||
)
|
||||
|
||||
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:
|
||||
query = query.where(asset.asset_category == filters.asset_category)
|
||||
if filters.cost_center:
|
||||
|
||||
@@ -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() {
|
||||
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) {
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user