mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-28 09:24:45 +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.
|
description: Share exact version number of Frappe and ERPNext you are using.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Frappe version -
|
Frappe version -
|
||||||
ERPNext Verion -
|
ERPNext version -
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/generate-pot-file.yml
vendored
2
.github/workflows/generate-pot-file.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
branch: ["develop"]
|
branch: ["develop", "version-16-hotfix"]
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "**.js"
|
- "**.js"
|
||||||
- "**.css"
|
- "**.css"
|
||||||
|
- "**.svg"
|
||||||
- "**.md"
|
- "**.md"
|
||||||
- "**.html"
|
- "**.html"
|
||||||
- 'crowdin.yml'
|
- 'crowdin.yml'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://frappe.io/erpnext">
|
<a href="https://frappe.io/erpnext">
|
||||||
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80xp"/>
|
||||||
|
|||||||
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"
|
"account_number": "1151.000"
|
||||||
},
|
},
|
||||||
|
"Pajak Dibayar di Muka": {
|
||||||
|
"PPN Masukan": {
|
||||||
|
"account_number": "1152.001",
|
||||||
|
"account_type": "Tax"
|
||||||
|
},
|
||||||
|
"PPh 23 Dibayar di Muka": {
|
||||||
|
"account_number": "1152.002",
|
||||||
|
"account_type": "Tax"
|
||||||
|
},
|
||||||
|
"account_number": "1152.000"
|
||||||
|
},
|
||||||
"account_number": "1150.000"
|
"account_number": "1150.000"
|
||||||
},
|
},
|
||||||
"Kas": {
|
"Kas": {
|
||||||
@@ -97,17 +108,6 @@
|
|||||||
},
|
},
|
||||||
"account_number": "1130.000"
|
"account_number": "1130.000"
|
||||||
},
|
},
|
||||||
"Pajak Dibayar di Muka": {
|
|
||||||
"PPN Masukan": {
|
|
||||||
"account_number": "1151.001",
|
|
||||||
"account_type": "Tax"
|
|
||||||
},
|
|
||||||
"PPh 23 Dibayar di Muka": {
|
|
||||||
"account_number": "1152.001",
|
|
||||||
"account_type": "Tax"
|
|
||||||
},
|
|
||||||
"account_number": "1150.000"
|
|
||||||
},
|
|
||||||
"account_number": "1100.000"
|
"account_number": "1100.000"
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
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":
|
if doc.doctype == "Bank Clearance":
|
||||||
return
|
return
|
||||||
elif doc.doctype == "Asset":
|
elif doc.doctype == "Asset":
|
||||||
if doc.is_existing_asset:
|
if doc.asset_type == "Existing Asset":
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
date = doc.available_for_use_date
|
date = doc.available_for_use_date
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
"enable_common_party_accounting",
|
"enable_common_party_accounting",
|
||||||
"allow_multi_currency_invoices_against_single_party_account",
|
"allow_multi_currency_invoices_against_single_party_account",
|
||||||
"confirm_before_resetting_posting_date",
|
"confirm_before_resetting_posting_date",
|
||||||
|
"analytics_section",
|
||||||
|
"enable_accounting_dimensions",
|
||||||
|
"column_break_vtnr",
|
||||||
|
"enable_discounts_and_margin",
|
||||||
"journals_section",
|
"journals_section",
|
||||||
"merge_similar_account_heads",
|
"merge_similar_account_heads",
|
||||||
"deferred_accounting_settings_section",
|
"deferred_accounting_settings_section",
|
||||||
@@ -51,12 +55,16 @@
|
|||||||
"allow_pegged_currencies_exchange_rates",
|
"allow_pegged_currencies_exchange_rates",
|
||||||
"column_break_yuug",
|
"column_break_yuug",
|
||||||
"stale_days",
|
"stale_days",
|
||||||
|
"payments_tab",
|
||||||
"section_break_jpd0",
|
"section_break_jpd0",
|
||||||
"auto_reconcile_payments",
|
"auto_reconcile_payments",
|
||||||
"auto_reconciliation_job_trigger",
|
"auto_reconciliation_job_trigger",
|
||||||
"reconciliation_queue_size",
|
"reconciliation_queue_size",
|
||||||
"column_break_resa",
|
"column_break_resa",
|
||||||
"exchange_gain_loss_posting_date",
|
"exchange_gain_loss_posting_date",
|
||||||
|
"payment_options_section",
|
||||||
|
"enable_loyalty_point_program",
|
||||||
|
"column_break_ctam",
|
||||||
"invoicing_settings_tab",
|
"invoicing_settings_tab",
|
||||||
"accounts_transactions_settings_section",
|
"accounts_transactions_settings_section",
|
||||||
"over_billing_allowance",
|
"over_billing_allowance",
|
||||||
@@ -281,7 +289,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>",
|
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\" rel=\"noopener noreferrer\">Common Party</a>",
|
||||||
"fieldname": "enable_common_party_accounting",
|
"fieldname": "enable_common_party_accounting",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enable Common Party Accounting"
|
"label": "Enable Common Party Accounting"
|
||||||
@@ -637,16 +645,59 @@
|
|||||||
"fieldname": "budget_section",
|
"fieldname": "budget_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Budget"
|
"label": "Budget"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "analytics_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Analytical Accounting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_vtnr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Apply discounts and margins on products",
|
||||||
|
"fieldname": "enable_discounts_and_margin",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Discounts and Margin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payments_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Payments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_options_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Payment Options"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_loyalty_point_program",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Loyalty Point Program"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ctam",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Enable cost center, projects and other custom accounting dimensions",
|
||||||
|
"fieldname": "enable_accounting_dimensions",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Accounting Dimensions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 0,
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-11 18:30:45.968531",
|
"modified": "2026-02-04 17:15:38.609327",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -12,6 +12,28 @@ from frappe.utils import cint
|
|||||||
|
|
||||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||||
|
|
||||||
|
SELLING_DOCTYPES = [
|
||||||
|
"Sales Invoice",
|
||||||
|
"Sales Order",
|
||||||
|
"Delivery Note",
|
||||||
|
"Quotation",
|
||||||
|
"Sales Invoice Item",
|
||||||
|
"Sales Order Item",
|
||||||
|
"Delivery Note Item",
|
||||||
|
"Quotation Item",
|
||||||
|
"POS Invoice",
|
||||||
|
"POS Invoice Item",
|
||||||
|
]
|
||||||
|
|
||||||
|
BUYING_DOCTYPES = [
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Purchase Order",
|
||||||
|
"Purchase Receipt",
|
||||||
|
"Purchase Invoice Item",
|
||||||
|
"Purchase Order Item",
|
||||||
|
"Purchase Receipt Item",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class AccountsSettings(Document):
|
class AccountsSettings(Document):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
@@ -43,9 +65,12 @@ class AccountsSettings(Document):
|
|||||||
default_ageing_range: DF.Data | None
|
default_ageing_range: DF.Data | None
|
||||||
delete_linked_ledger_entries: DF.Check
|
delete_linked_ledger_entries: DF.Check
|
||||||
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
|
||||||
|
enable_accounting_dimensions: DF.Check
|
||||||
enable_common_party_accounting: DF.Check
|
enable_common_party_accounting: DF.Check
|
||||||
|
enable_discounts_and_margin: DF.Check
|
||||||
enable_fuzzy_matching: DF.Check
|
enable_fuzzy_matching: DF.Check
|
||||||
enable_immutable_ledger: DF.Check
|
enable_immutable_ledger: DF.Check
|
||||||
|
enable_loyalty_point_program: DF.Check
|
||||||
enable_party_matching: DF.Check
|
enable_party_matching: DF.Check
|
||||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||||
@@ -98,6 +123,18 @@ class AccountsSettings(Document):
|
|||||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||||
self.enable_payment_schedule_in_print()
|
self.enable_payment_schedule_in_print()
|
||||||
|
|
||||||
|
if old_doc.enable_accounting_dimensions != self.enable_accounting_dimensions:
|
||||||
|
toggle_accounting_dimension_sections(not self.enable_accounting_dimensions)
|
||||||
|
clear_cache = True
|
||||||
|
|
||||||
|
if old_doc.enable_discounts_and_margin != self.enable_discounts_and_margin:
|
||||||
|
toggle_sales_discount_section(not self.enable_discounts_and_margin)
|
||||||
|
clear_cache = True
|
||||||
|
|
||||||
|
if old_doc.enable_loyalty_point_program != self.enable_loyalty_point_program:
|
||||||
|
toggle_loyalty_point_program_section(not self.enable_loyalty_point_program)
|
||||||
|
clear_cache = True
|
||||||
|
|
||||||
if clear_cache:
|
if clear_cache:
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
|
|
||||||
@@ -154,3 +191,36 @@ class AccountsSettings(Document):
|
|||||||
|
|
||||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
|
||||||
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_accounting_dimension_sections(hide):
|
||||||
|
accounting_dimension_doctypes = frappe.get_hooks("accounting_dimension_doctypes")
|
||||||
|
for doctype in accounting_dimension_doctypes:
|
||||||
|
create_property_setter_for_hiding_field(doctype, "accounting_dimensions_section", hide)
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_sales_discount_section(hide):
|
||||||
|
for doctype in SELLING_DOCTYPES + BUYING_DOCTYPES:
|
||||||
|
meta = frappe.get_meta(doctype)
|
||||||
|
if meta.has_field("additional_discount_section"):
|
||||||
|
create_property_setter_for_hiding_field(doctype, "additional_discount_section", hide)
|
||||||
|
if meta.has_field("discount_and_margin"):
|
||||||
|
create_property_setter_for_hiding_field(doctype, "discount_and_margin", hide)
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_loyalty_point_program_section(hide):
|
||||||
|
for doctype in SELLING_DOCTYPES:
|
||||||
|
meta = frappe.get_meta(doctype)
|
||||||
|
if meta.has_field("loyalty_points_redemption"):
|
||||||
|
create_property_setter_for_hiding_field(doctype, "loyalty_points_redemption", hide)
|
||||||
|
|
||||||
|
|
||||||
|
def create_property_setter_for_hiding_field(doctype, field_name, hide):
|
||||||
|
make_property_setter(
|
||||||
|
doctype,
|
||||||
|
field_name,
|
||||||
|
"hidden",
|
||||||
|
hide,
|
||||||
|
"Check",
|
||||||
|
validate_fields_for_doctype=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
|
"options": "currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
frappe.provide("erpnext.integrations");
|
frappe.provide("erpnext.integrations");
|
||||||
|
|
||||||
frappe.ui.form.on("Bank", {
|
frappe.ui.form.on("Bank", {
|
||||||
onload: function (frm) {
|
|
||||||
add_fields_to_mapping_table(frm);
|
|
||||||
},
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
add_fields_to_mapping_table(frm);
|
add_fields_to_mapping_table(frm);
|
||||||
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
|
frm.toggle_display(["address_html", "contact_html"], !frm.doc.__islocal);
|
||||||
@@ -37,11 +34,11 @@ let add_fields_to_mapping_table = function (frm) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property(
|
const grid = frm.fields_dict.bank_transaction_mapping?.grid;
|
||||||
"bank_transaction_field",
|
|
||||||
"options",
|
if (grid) {
|
||||||
options
|
grid.update_docfield_property("bank_transaction_field", "options", options);
|
||||||
);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||||
@@ -116,7 +113,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
|||||||
"There was an issue connecting to Plaid's authentication server. Check browser console for more information"
|
"There was an issue connecting to Plaid's authentication server. Check browser console for more information"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
console.log(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
plaid_success(token, response) {
|
plaid_success(token, response) {
|
||||||
|
|||||||
@@ -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",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Company Account",
|
"label": "Company Account",
|
||||||
|
"mandatory_depends_on": "is_company_account",
|
||||||
"options": "Account"
|
"options": "Account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -98,6 +99,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
|
"mandatory_depends_on": "is_company_account",
|
||||||
"options": "Company"
|
"options": "Company"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -252,7 +254,7 @@
|
|||||||
"link_fieldname": "default_bank_account"
|
"link_fieldname": "default_bank_account"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-08-29 12:32:01.081687",
|
"modified": "2026-01-20 00:46:16.633364",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Account",
|
"name": "Bank Account",
|
||||||
|
|||||||
@@ -51,25 +51,29 @@ class BankAccount(Document):
|
|||||||
delete_contact_and_address("Bank Account", self.name)
|
delete_contact_and_address("Bank Account", self.name)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_company()
|
self.validate_is_company_account()
|
||||||
self.validate_account()
|
|
||||||
self.update_default_bank_account()
|
self.update_default_bank_account()
|
||||||
|
|
||||||
def validate_account(self):
|
def validate_is_company_account(self):
|
||||||
if self.account:
|
if self.is_company_account:
|
||||||
if accounts := frappe.db.get_all(
|
if not self.company:
|
||||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
frappe.throw(_("Company is mandatory for company account"))
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
|
||||||
frappe.bold(self.account),
|
|
||||||
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_company(self):
|
if not self.account:
|
||||||
if self.is_company_account and not self.company:
|
frappe.throw(_("Company Account is mandatory"))
|
||||||
frappe.throw(_("Company is mandatory for company account"))
|
|
||||||
|
self.validate_account()
|
||||||
|
|
||||||
|
def validate_account(self):
|
||||||
|
if accounts := frappe.db.get_all(
|
||||||
|
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||||
|
frappe.bold(self.account),
|
||||||
|
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def update_default_bank_account(self):
|
def update_default_bank_account(self):
|
||||||
if self.is_default and not self.disabled:
|
if self.is_default and not self.disabled:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
|
|||||||
from frappe.query_builder import Case
|
from frappe.query_builder import Case
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cstr, date_diff, flt, getdate
|
from frappe.utils import cstr, date_diff, flt, getdate
|
||||||
from pypika.terms import LiteralValue
|
from pypika.terms import Bracket, LiteralValue
|
||||||
|
|
||||||
from erpnext import get_company_currency
|
from erpnext import get_company_currency
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -541,7 +541,7 @@ class FinancialQueryBuilder:
|
|||||||
.where(acb_table.period_closing_voucher == closing_voucher)
|
.where(acb_table.period_closing_voucher == closing_voucher)
|
||||||
)
|
)
|
||||||
|
|
||||||
query = self._apply_standard_filters(query, acb_table)
|
query = self._apply_standard_filters(query, acb_table, "Account Closing Balance")
|
||||||
results = self._execute_with_permissions(query, "Account Closing Balance")
|
results = self._execute_with_permissions(query, "Account Closing Balance")
|
||||||
|
|
||||||
for row in results:
|
for row in results:
|
||||||
@@ -636,12 +636,15 @@ class FinancialQueryBuilder:
|
|||||||
return self._execute_with_permissions(query, "GL Entry")
|
return self._execute_with_permissions(query, "GL Entry")
|
||||||
|
|
||||||
def _calculate_running_balances(self, balances_data: dict, gl_data: list[dict]) -> dict:
|
def _calculate_running_balances(self, balances_data: dict, gl_data: list[dict]) -> dict:
|
||||||
for row in gl_data:
|
gl_dict = {row["account"]: row for row in gl_data}
|
||||||
account = row["account"]
|
accounts = set(balances_data.keys()) | set(gl_dict.keys())
|
||||||
|
|
||||||
|
for account in accounts:
|
||||||
if account not in balances_data:
|
if account not in balances_data:
|
||||||
balances_data[account] = AccountData(account=account, **self._get_account_meta(account))
|
balances_data[account] = AccountData(account=account, **self._get_account_meta(account))
|
||||||
|
|
||||||
account_data: AccountData = balances_data[account]
|
account_data: AccountData = balances_data[account]
|
||||||
|
gl_movement = gl_dict.get(account, {})
|
||||||
|
|
||||||
if account_data.has_periods():
|
if account_data.has_periods():
|
||||||
first_period = account_data.get_period(self.periods[0]["key"])
|
first_period = account_data.get_period(self.periods[0]["key"])
|
||||||
@@ -651,20 +654,13 @@ class FinancialQueryBuilder:
|
|||||||
|
|
||||||
for period in self.periods:
|
for period in self.periods:
|
||||||
period_key = period["key"]
|
period_key = period["key"]
|
||||||
movement = row.get(period_key, 0.0)
|
movement = gl_movement.get(period_key, 0.0)
|
||||||
closing_balance = current_balance + movement
|
closing_balance = current_balance + movement
|
||||||
|
|
||||||
account_data.add_period(PeriodValue(period_key, current_balance, closing_balance, movement))
|
account_data.add_period(PeriodValue(period_key, current_balance, closing_balance, movement))
|
||||||
|
|
||||||
current_balance = closing_balance
|
current_balance = closing_balance
|
||||||
|
|
||||||
# Accounts with no movements
|
|
||||||
for account_data in balances_data.values():
|
|
||||||
for period in self.periods:
|
|
||||||
period_key = period["key"]
|
|
||||||
if period_key not in account_data.period_values:
|
|
||||||
account_data.add_period(PeriodValue(period_key, 0.0, 0.0, 0.0))
|
|
||||||
|
|
||||||
def _handle_balance_accumulation(self, balances_data):
|
def _handle_balance_accumulation(self, balances_data):
|
||||||
for account_data in balances_data.values():
|
for account_data in balances_data.values():
|
||||||
account_data: AccountData
|
account_data: AccountData
|
||||||
@@ -683,12 +679,12 @@ class FinancialQueryBuilder:
|
|||||||
else:
|
else:
|
||||||
account_data.unaccumulate_values()
|
account_data.unaccumulate_values()
|
||||||
|
|
||||||
def _apply_standard_filters(self, query, table):
|
def _apply_standard_filters(self, query, table, doctype: str = "GL Entry"):
|
||||||
if self.filters.get("ignore_closing_entries"):
|
if self.filters.get("ignore_closing_entries"):
|
||||||
if hasattr(table, "is_period_closing_voucher_entry"):
|
if doctype == "GL Entry":
|
||||||
query = query.where(table.is_period_closing_voucher_entry == 0)
|
|
||||||
else:
|
|
||||||
query = query.where(table.voucher_type != "Period Closing Voucher")
|
query = query.where(table.voucher_type != "Period Closing Voucher")
|
||||||
|
else:
|
||||||
|
query = query.where(table.is_period_closing_voucher_entry == 0)
|
||||||
|
|
||||||
if self.filters.get("project"):
|
if self.filters.get("project"):
|
||||||
projects = self.filters.get("project")
|
projects = self.filters.get("project")
|
||||||
@@ -736,7 +732,7 @@ class FinancialQueryBuilder:
|
|||||||
user_conditions = build_match_conditions(doctype)
|
user_conditions = build_match_conditions(doctype)
|
||||||
|
|
||||||
if user_conditions:
|
if user_conditions:
|
||||||
query = query.where(LiteralValue(user_conditions))
|
query = query.where(Bracket(LiteralValue(user_conditions)))
|
||||||
|
|
||||||
return query.run(as_dict=True)
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ from frappe.utils import flt
|
|||||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||||
DependencyResolver,
|
DependencyResolver,
|
||||||
FilterExpressionParser,
|
FilterExpressionParser,
|
||||||
|
FinancialQueryBuilder,
|
||||||
FormulaCalculator,
|
FormulaCalculator,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import (
|
from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import (
|
||||||
FinancialReportTemplateTestCase,
|
FinancialReportTemplateTestCase,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import get_currency_precision
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
|
from erpnext.accounts.utils import get_currency_precision, get_fiscal_year
|
||||||
|
|
||||||
# On IntegrationTestCase, the doctype test records and all
|
# On IntegrationTestCase, the doctype test records and all
|
||||||
# link-field test record dependencies are recursively loaded
|
# link-field test record dependencies are recursively loaded
|
||||||
@@ -1668,3 +1670,360 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
|
|||||||
mock_row_invalid = self._create_mock_report_row(invalid_formula)
|
mock_row_invalid = self._create_mock_report_row(invalid_formula)
|
||||||
condition = parser.build_condition(mock_row_invalid, account_table)
|
condition = parser.build_condition(mock_row_invalid, account_table)
|
||||||
self.assertIsNone(condition)
|
self.assertIsNone(condition)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
|
||||||
|
def test_fetch_balances_with_journal_entries(self):
|
||||||
|
company = "_Test Company"
|
||||||
|
cash_account = "_Test Cash - _TC"
|
||||||
|
bank_account = "_Test Bank - _TC"
|
||||||
|
|
||||||
|
# Create journal entries in different periods
|
||||||
|
# October: Transfer 1000 from Bank to Cash
|
||||||
|
jv_oct = make_journal_entry(
|
||||||
|
account1=cash_account,
|
||||||
|
account2=bank_account,
|
||||||
|
amount=1000,
|
||||||
|
posting_date="2024-10-15",
|
||||||
|
company=company,
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# November: Transfer 500 from Bank to Cash
|
||||||
|
jv_nov = make_journal_entry(
|
||||||
|
account1=cash_account,
|
||||||
|
account2=bank_account,
|
||||||
|
amount=500,
|
||||||
|
posting_date="2024-11-20",
|
||||||
|
company=company,
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# December: No transactions (test zero movement period)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set up filters and periods for Q4 2024
|
||||||
|
filters = {
|
||||||
|
"company": company,
|
||||||
|
"from_fiscal_year": "2024",
|
||||||
|
"to_fiscal_year": "2024",
|
||||||
|
"period_start_date": "2024-10-01",
|
||||||
|
"period_end_date": "2024-12-31",
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
}
|
||||||
|
|
||||||
|
periods = [
|
||||||
|
{"key": "2024_oct", "from_date": "2024-10-01", "to_date": "2024-10-31"},
|
||||||
|
{"key": "2024_nov", "from_date": "2024-11-01", "to_date": "2024-11-30"},
|
||||||
|
{"key": "2024_dec", "from_date": "2024-12-01", "to_date": "2024-12-31"},
|
||||||
|
]
|
||||||
|
|
||||||
|
query_builder = FinancialQueryBuilder(filters, periods)
|
||||||
|
|
||||||
|
# Create account objects as expected by fetch_account_balances
|
||||||
|
accounts = [
|
||||||
|
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||||
|
frappe._dict({"name": bank_account, "account_name": "Bank", "account_number": "1002"}),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Fetch balances using the full workflow
|
||||||
|
balances_data = query_builder.fetch_account_balances(accounts)
|
||||||
|
|
||||||
|
# Verify Cash account balances
|
||||||
|
cash_data = balances_data.get(cash_account)
|
||||||
|
self.assertIsNotNone(cash_data, "Cash account should exist in results")
|
||||||
|
|
||||||
|
# October: movement = +1000 (debit)
|
||||||
|
oct_cash = cash_data.get_period("2024_oct")
|
||||||
|
self.assertIsNotNone(oct_cash, "October period should exist for cash")
|
||||||
|
self.assertEqual(oct_cash.movement, 1000.0, "October cash movement should be 1000")
|
||||||
|
|
||||||
|
# November: movement = +500
|
||||||
|
nov_cash = cash_data.get_period("2024_nov")
|
||||||
|
self.assertIsNotNone(nov_cash, "November period should exist for cash")
|
||||||
|
self.assertEqual(nov_cash.movement, 500.0, "November cash movement should be 500")
|
||||||
|
self.assertEqual(
|
||||||
|
nov_cash.opening, oct_cash.closing, "November opening should equal October closing"
|
||||||
|
)
|
||||||
|
|
||||||
|
# December: movement = 0 (no transactions)
|
||||||
|
dec_cash = cash_data.get_period("2024_dec")
|
||||||
|
self.assertIsNotNone(dec_cash, "December period should exist for cash")
|
||||||
|
self.assertEqual(dec_cash.movement, 0.0, "December cash movement should be 0")
|
||||||
|
self.assertEqual(
|
||||||
|
dec_cash.closing,
|
||||||
|
nov_cash.closing,
|
||||||
|
"December closing should equal November closing when no movement",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify Bank account balances (opposite direction)
|
||||||
|
bank_data = balances_data.get(bank_account)
|
||||||
|
self.assertIsNotNone(bank_data, "Bank account should exist in results")
|
||||||
|
|
||||||
|
oct_bank = bank_data.get_period("2024_oct")
|
||||||
|
self.assertEqual(oct_bank.movement, -1000.0, "October bank movement should be -1000")
|
||||||
|
|
||||||
|
nov_bank = bank_data.get_period("2024_nov")
|
||||||
|
self.assertEqual(nov_bank.movement, -500.0, "November bank movement should be -500")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up: cancel journal entries
|
||||||
|
jv_nov.cancel()
|
||||||
|
jv_oct.cancel()
|
||||||
|
|
||||||
|
def test_opening_balance_from_previous_period_closing(self):
|
||||||
|
company = "_Test Company"
|
||||||
|
cash_account = "_Test Cash - _TC"
|
||||||
|
sales_account = "Sales - _TC"
|
||||||
|
posting_date_2023 = "2023-06-15"
|
||||||
|
|
||||||
|
# Create journal entry in prior period (2023)
|
||||||
|
# Cash Dr 5000, Sales Cr 5000
|
||||||
|
jv_2023 = make_journal_entry(
|
||||||
|
account1=cash_account,
|
||||||
|
account2=sales_account,
|
||||||
|
amount=5000,
|
||||||
|
posting_date=posting_date_2023,
|
||||||
|
company=company,
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
pcv = None
|
||||||
|
jv_2024 = None
|
||||||
|
original_pcv_setting = frappe.db.get_single_value(
|
||||||
|
"Accounts Settings", "use_legacy_controller_for_pcv"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create Period Closing Voucher for 2023
|
||||||
|
# This will create Account Closing Balance entries
|
||||||
|
closing_account = frappe.db.get_value(
|
||||||
|
"Account",
|
||||||
|
{
|
||||||
|
"company": company,
|
||||||
|
"root_type": "Liability",
|
||||||
|
"is_group": 0,
|
||||||
|
"account_type": ["not in", ["Payable", "Receivable"]],
|
||||||
|
},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
fy_2023 = get_fiscal_year(posting_date_2023, company=company)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
|
||||||
|
|
||||||
|
pcv = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Period Closing Voucher",
|
||||||
|
"transaction_date": "2023-12-31",
|
||||||
|
"period_start_date": fy_2023[1],
|
||||||
|
"period_end_date": fy_2023[2],
|
||||||
|
"company": company,
|
||||||
|
"fiscal_year": fy_2023[0],
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"closing_account_head": closing_account,
|
||||||
|
"remarks": "Test Period Closing",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pcv.insert()
|
||||||
|
pcv.submit()
|
||||||
|
pcv.reload()
|
||||||
|
|
||||||
|
# Now create a small transaction in 2024 to ensure the account appears
|
||||||
|
jv_2024 = make_journal_entry(
|
||||||
|
account1=cash_account,
|
||||||
|
account2=sales_account,
|
||||||
|
amount=100,
|
||||||
|
posting_date="2024-01-15",
|
||||||
|
company=company,
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up filters for Q1 2024 (after the period closing)
|
||||||
|
filters = {
|
||||||
|
"company": company,
|
||||||
|
"from_fiscal_year": "2024",
|
||||||
|
"to_fiscal_year": "2024",
|
||||||
|
"period_start_date": "2024-01-01",
|
||||||
|
"period_end_date": "2024-03-31",
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"ignore_closing_entries": True, # Don't include PCV entries in movements
|
||||||
|
}
|
||||||
|
|
||||||
|
periods = [
|
||||||
|
{"key": "2024_jan", "from_date": "2024-01-01", "to_date": "2024-01-31"},
|
||||||
|
{"key": "2024_feb", "from_date": "2024-02-01", "to_date": "2024-02-29"},
|
||||||
|
{"key": "2024_mar", "from_date": "2024-03-01", "to_date": "2024-03-31"},
|
||||||
|
]
|
||||||
|
|
||||||
|
query_builder = FinancialQueryBuilder(filters, periods)
|
||||||
|
|
||||||
|
accounts = [
|
||||||
|
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||||
|
]
|
||||||
|
|
||||||
|
balances_data = query_builder.fetch_account_balances(accounts)
|
||||||
|
|
||||||
|
# Verify Cash account has opening balance from 2023 transactions
|
||||||
|
cash_data = balances_data.get(cash_account)
|
||||||
|
self.assertIsNotNone(cash_data, "Cash account should exist in results")
|
||||||
|
|
||||||
|
jan_cash = cash_data.get_period("2024_jan")
|
||||||
|
self.assertIsNotNone(jan_cash, "January period should exist")
|
||||||
|
|
||||||
|
# Opening balance should be from prior period
|
||||||
|
# Cash had 5000 debit in 2023, so opening in 2024 should be >= 5000
|
||||||
|
# (may be higher if there were other test transactions)
|
||||||
|
self.assertEqual(
|
||||||
|
jan_cash.opening,
|
||||||
|
5000.0,
|
||||||
|
"January opening should equal to balance from 2023 (5000)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify running balance logic
|
||||||
|
# Movement in January is 100 (from jv_2024)
|
||||||
|
self.assertEqual(jan_cash.movement, 100.0, "January movement should be 100")
|
||||||
|
self.assertEqual(
|
||||||
|
jan_cash.closing, jan_cash.opening + jan_cash.movement, "Closing = Opening + Movement"
|
||||||
|
)
|
||||||
|
|
||||||
|
# February and March should have no movement but carry the balance
|
||||||
|
feb_cash = cash_data.get_period("2024_feb")
|
||||||
|
self.assertEqual(feb_cash.opening, jan_cash.closing, "Feb opening = Jan closing")
|
||||||
|
self.assertEqual(feb_cash.movement, 0.0, "February should have no movement")
|
||||||
|
self.assertEqual(feb_cash.closing, feb_cash.opening, "Feb closing = opening when no movement")
|
||||||
|
|
||||||
|
mar_cash = cash_data.get_period("2024_mar")
|
||||||
|
self.assertEqual(mar_cash.opening, feb_cash.closing, "Mar opening = Feb closing")
|
||||||
|
self.assertEqual(mar_cash.movement, 0.0, "March should have no movement")
|
||||||
|
self.assertEqual(mar_cash.closing, mar_cash.opening, "Mar closing = opening when no movement")
|
||||||
|
|
||||||
|
# Set up filters for Q2 2024
|
||||||
|
filters_q2 = {
|
||||||
|
"company": company,
|
||||||
|
"from_fiscal_year": "2024",
|
||||||
|
"to_fiscal_year": "2024",
|
||||||
|
"period_start_date": "2024-04-01",
|
||||||
|
"period_end_date": "2024-06-30",
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"ignore_closing_entries": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
periods_q2 = [
|
||||||
|
{"key": "2024_apr", "from_date": "2024-04-01", "to_date": "2024-04-30"},
|
||||||
|
{"key": "2024_may", "from_date": "2024-05-01", "to_date": "2024-05-31"},
|
||||||
|
{"key": "2024_jun", "from_date": "2024-06-01", "to_date": "2024-06-30"},
|
||||||
|
]
|
||||||
|
|
||||||
|
query_builder_q2 = FinancialQueryBuilder(filters_q2, periods_q2)
|
||||||
|
|
||||||
|
balances_data_q2 = query_builder_q2.fetch_account_balances(accounts)
|
||||||
|
|
||||||
|
# Verify Cash account in Q2
|
||||||
|
cash_data_q2 = balances_data_q2.get(cash_account)
|
||||||
|
self.assertIsNotNone(cash_data_q2, "Cash account should exist in Q2 results")
|
||||||
|
|
||||||
|
apr_cash = cash_data_q2.get_period("2024_apr")
|
||||||
|
self.assertIsNotNone(apr_cash, "April period should exist")
|
||||||
|
|
||||||
|
# Opening balance in April should equal closing in March
|
||||||
|
self.assertEqual(
|
||||||
|
apr_cash.opening,
|
||||||
|
mar_cash.closing,
|
||||||
|
"April opening should equal March closing balance",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(apr_cash.closing, apr_cash.opening, "April closing = opening when no movement")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Clean up
|
||||||
|
frappe.db.set_single_value(
|
||||||
|
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if jv_2024:
|
||||||
|
jv_2024.cancel()
|
||||||
|
|
||||||
|
if pcv:
|
||||||
|
pcv.reload()
|
||||||
|
if pcv.docstatus == 1:
|
||||||
|
pcv.cancel()
|
||||||
|
|
||||||
|
jv_2023.cancel()
|
||||||
|
|
||||||
|
def test_account_with_gl_entries_but_no_prior_closing_balance(self):
|
||||||
|
company = "_Test Company"
|
||||||
|
cash_account = "_Test Cash - _TC"
|
||||||
|
bank_account = "_Test Bank - _TC"
|
||||||
|
|
||||||
|
# Create journal entries WITHOUT any prior Period Closing Voucher
|
||||||
|
# This ensures the account exists in gl_dict but NOT in balances_data
|
||||||
|
jv = make_journal_entry(
|
||||||
|
account1=cash_account,
|
||||||
|
account2=bank_account,
|
||||||
|
amount=2500,
|
||||||
|
posting_date="2024-07-15",
|
||||||
|
company=company,
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Set up filters - use a period with no prior PCV
|
||||||
|
filters = {
|
||||||
|
"company": company,
|
||||||
|
"from_fiscal_year": "2024",
|
||||||
|
"to_fiscal_year": "2024",
|
||||||
|
"period_start_date": "2024-07-01",
|
||||||
|
"period_end_date": "2024-09-30",
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
}
|
||||||
|
|
||||||
|
periods = [
|
||||||
|
{"key": "2024_jul", "from_date": "2024-07-01", "to_date": "2024-07-31"},
|
||||||
|
{"key": "2024_aug", "from_date": "2024-08-01", "to_date": "2024-08-31"},
|
||||||
|
{"key": "2024_sep", "from_date": "2024-09-01", "to_date": "2024-09-30"},
|
||||||
|
]
|
||||||
|
|
||||||
|
query_builder = FinancialQueryBuilder(filters, periods)
|
||||||
|
|
||||||
|
# Use accounts that have GL entries but may not have Account Closing Balance
|
||||||
|
accounts = [
|
||||||
|
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
|
||||||
|
frappe._dict({"name": bank_account, "account_name": "Bank", "account_number": "1002"}),
|
||||||
|
]
|
||||||
|
|
||||||
|
balances_data = query_builder.fetch_account_balances(accounts)
|
||||||
|
|
||||||
|
# Verify accounts are present in results even without prior closing balance
|
||||||
|
cash_data = balances_data.get(cash_account)
|
||||||
|
self.assertIsNotNone(cash_data, "Cash account should exist in results")
|
||||||
|
|
||||||
|
bank_data = balances_data.get(bank_account)
|
||||||
|
self.assertIsNotNone(bank_data, "Bank account should exist in results")
|
||||||
|
|
||||||
|
# Verify July has the movement from journal entry
|
||||||
|
jul_cash = cash_data.get_period("2024_jul")
|
||||||
|
self.assertIsNotNone(jul_cash, "July period should exist for cash")
|
||||||
|
self.assertEqual(jul_cash.movement, 2500.0, "July cash movement should be 2500")
|
||||||
|
|
||||||
|
jul_bank = bank_data.get_period("2024_jul")
|
||||||
|
self.assertIsNotNone(jul_bank, "July period should exist for bank")
|
||||||
|
self.assertEqual(jul_bank.movement, -2500.0, "July bank movement should be -2500")
|
||||||
|
|
||||||
|
# Verify subsequent periods exist with zero movement
|
||||||
|
aug_cash = cash_data.get_period("2024_aug")
|
||||||
|
self.assertIsNotNone(aug_cash, "August period should exist for cash")
|
||||||
|
self.assertEqual(aug_cash.movement, 0.0, "August cash movement should be 0")
|
||||||
|
self.assertEqual(aug_cash.opening, jul_cash.closing, "August opening = July closing")
|
||||||
|
|
||||||
|
sep_cash = cash_data.get_period("2024_sep")
|
||||||
|
self.assertIsNotNone(sep_cash, "September period should exist for cash")
|
||||||
|
self.assertEqual(sep_cash.movement, 0.0, "September cash movement should be 0")
|
||||||
|
self.assertEqual(sep_cash.opening, aug_cash.closing, "September opening = August closing")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
jv.cancel()
|
||||||
|
|||||||
@@ -277,7 +277,21 @@ frappe.ui.form.on("Journal Entry", {
|
|||||||
var update_jv_details = function (doc, r) {
|
var update_jv_details = function (doc, r) {
|
||||||
$.each(r, function (i, d) {
|
$.each(r, function (i, d) {
|
||||||
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
|
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
|
||||||
frappe.model.set_value(row.doctype, row.name, "account", d.account);
|
const {
|
||||||
|
idx,
|
||||||
|
name,
|
||||||
|
owner,
|
||||||
|
parent,
|
||||||
|
parenttype,
|
||||||
|
parentfield,
|
||||||
|
creation,
|
||||||
|
modified,
|
||||||
|
modified_by,
|
||||||
|
doctype,
|
||||||
|
docstatus,
|
||||||
|
...fields
|
||||||
|
} = d;
|
||||||
|
frappe.model.set_value(row.doctype, row.name, fields);
|
||||||
});
|
});
|
||||||
refresh_field("accounts");
|
refresh_field("accounts");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"entry_type_and_date",
|
"entry_type_and_date",
|
||||||
|
"company",
|
||||||
"is_system_generated",
|
"is_system_generated",
|
||||||
"title",
|
"title",
|
||||||
"voucher_type",
|
"voucher_type",
|
||||||
@@ -17,7 +18,6 @@
|
|||||||
"reversal_of",
|
"reversal_of",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"from_template",
|
"from_template",
|
||||||
"company",
|
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"finance_book",
|
"finance_book",
|
||||||
"apply_tds",
|
"apply_tds",
|
||||||
@@ -638,7 +638,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-11-13 17:54:14.542903",
|
"modified": "2026-02-03 14:40:39.944524",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry",
|
"name": "Journal Entry",
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ class JournalEntry(AccountsController):
|
|||||||
mode_of_payment: DF.Link | None
|
mode_of_payment: DF.Link | None
|
||||||
multi_currency: DF.Check
|
multi_currency: DF.Check
|
||||||
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
naming_series: DF.Literal["ACC-JV-.YYYY.-"]
|
||||||
party_not_required: DF.Check
|
|
||||||
override_tax_withholding_entries: DF.Check
|
override_tax_withholding_entries: DF.Check
|
||||||
|
party_not_required: DF.Check
|
||||||
pay_to_recd_from: DF.Data | None
|
pay_to_recd_from: DF.Data | None
|
||||||
payment_order: DF.Link | None
|
payment_order: DF.Link | None
|
||||||
periodic_entry_difference_account: DF.Link | None
|
periodic_entry_difference_account: DF.Link | None
|
||||||
@@ -179,7 +179,7 @@ class JournalEntry(AccountsController):
|
|||||||
validate_docs_for_deferred_accounting([self.name], [])
|
validate_docs_for_deferred_accounting([self.name], [])
|
||||||
|
|
||||||
def submit(self):
|
def submit(self):
|
||||||
if len(self.accounts) > 100:
|
if len(self.accounts) > 100 and not self.meta.queue_in_background:
|
||||||
queue_submission(self, "_submit")
|
queue_submission(self, "_submit")
|
||||||
else:
|
else:
|
||||||
return self._submit()
|
return self._submit()
|
||||||
@@ -1691,6 +1691,10 @@ def get_exchange_rate(
|
|||||||
credit=None,
|
credit=None,
|
||||||
exchange_rate=None,
|
exchange_rate=None,
|
||||||
):
|
):
|
||||||
|
# Ensure exchange_rate is always numeric to avoid calculation errors
|
||||||
|
if isinstance(exchange_rate, str):
|
||||||
|
exchange_rate = flt(exchange_rate) or 1
|
||||||
|
|
||||||
account_details = frappe.get_cached_value(
|
account_details = frappe.get_cached_value(
|
||||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
frappe.ui.form.on("Journal Entry Template", {
|
frappe.ui.form.on("Journal Entry Template", {
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
|
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||||
if (frm.is_new()) {
|
if (frm.is_new()) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
@@ -37,6 +38,31 @@ frappe.ui.form.on("Journal Entry Template", {
|
|||||||
|
|
||||||
return { filters: filters };
|
return { filters: filters };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("project", "accounts", function (doc, cdt, cdn) {
|
||||||
|
let row = frappe.get_doc(cdt, cdn);
|
||||||
|
let filters = {
|
||||||
|
company: doc.company,
|
||||||
|
};
|
||||||
|
if (row.party_type == "Customer") {
|
||||||
|
filters.customer = row.party;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.get_project_name",
|
||||||
|
filters,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("party_type", "accounts", function (doc, cdt, cdn) {
|
||||||
|
const row = locals[cdt][cdn];
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: "erpnext.setup.doctype.party_type.party_type.get_party_type",
|
||||||
|
filters: {
|
||||||
|
account: row.account,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
voucher_type: function (frm) {
|
voucher_type: function (frm) {
|
||||||
var add_accounts = function (doc, r) {
|
var add_accounts = function (doc, r) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +43,29 @@ class JournalEntryTemplate(Document):
|
|||||||
]
|
]
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
def validate(self):
|
||||||
|
self.validate_party()
|
||||||
|
|
||||||
|
def validate_party(self):
|
||||||
|
"""
|
||||||
|
Loop over all accounts and see if party and party type is set correctly
|
||||||
|
"""
|
||||||
|
for account in self.accounts:
|
||||||
|
if account.party_type:
|
||||||
|
account_type = frappe.get_cached_value("Account", account.account, "account_type")
|
||||||
|
if account_type not in ["Receivable", "Payable"]:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Check row {0} for account {1}: Party Type is only allowed for Receivable or Payable accounts"
|
||||||
|
).format(account.idx, account.account)
|
||||||
|
)
|
||||||
|
|
||||||
|
if account.party and not account.party_type:
|
||||||
|
frappe.throw(
|
||||||
|
_("Check row {0} for account {1}: Party is only allowed if Party Type is set").format(
|
||||||
|
account.idx, account.account
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"account"
|
"account",
|
||||||
|
"party_type",
|
||||||
|
"party",
|
||||||
|
"accounting_dimensions_section",
|
||||||
|
"cost_center",
|
||||||
|
"dimension_col_break",
|
||||||
|
"project"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -15,18 +21,55 @@
|
|||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Party Type",
|
||||||
|
"options": "DocType"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Party",
|
||||||
|
"options": "party_type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "accounting_dimensions_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounting Dimensions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "cost_center",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Cost Center",
|
||||||
|
"options": "Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dimension_col_break",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:09:58.986448",
|
"modified": "2026-01-09 13:16:27.615083",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry Template Account",
|
"name": "Journal Entry Template Account",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ class JournalEntryTemplateAccount(Document):
|
|||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
account: DF.Link
|
account: DF.Link
|
||||||
|
cost_center: DF.Link | None
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
party: DF.DynamicLink | None
|
||||||
|
party_type: DF.Link | None
|
||||||
|
project: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -400,6 +400,16 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
);
|
);
|
||||||
|
|
||||||
frm.refresh_fields();
|
frm.refresh_fields();
|
||||||
|
|
||||||
|
const party_currency =
|
||||||
|
frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
|
||||||
|
|
||||||
|
var reference_grid = frm.fields_dict["references"].grid;
|
||||||
|
["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
|
||||||
|
reference_grid.update_docfield_property(fieldname, "options", party_currency);
|
||||||
|
});
|
||||||
|
|
||||||
|
reference_grid.refresh();
|
||||||
},
|
},
|
||||||
|
|
||||||
show_general_ledger: function (frm) {
|
show_general_ledger: function (frm) {
|
||||||
@@ -1108,7 +1118,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
|
|
||||||
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||||
await frm.call("allocate_amount_to_references", {
|
await frm.call("allocate_amount_to_references", {
|
||||||
paid_amount: paid_amount,
|
paid_amount: flt(paid_amount),
|
||||||
paid_amount_change: paid_amount_change,
|
paid_amount_change: paid_amount_change,
|
||||||
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||||
});
|
});
|
||||||
@@ -1444,16 +1454,15 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
// set taxes table
|
// set taxes table
|
||||||
if (r.message) {
|
let taxes = r.message;
|
||||||
for (let tax of r.message) {
|
taxes.forEach((tax) => {
|
||||||
if (tax.charge_type === "On Net Total") {
|
if (tax.charge_type === "On Net Total") {
|
||||||
tax.charge_type = "On Paid Amount";
|
tax.charge_type = "On Paid Amount";
|
||||||
}
|
|
||||||
frm.add_child("taxes", tax);
|
|
||||||
}
|
}
|
||||||
frm.events.apply_taxes(frm);
|
});
|
||||||
frm.events.set_unallocated_amount(frm);
|
frm.set_value("taxes", taxes);
|
||||||
}
|
frm.events.apply_taxes(frm);
|
||||||
|
frm.events.set_unallocated_amount(frm);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -132,6 +132,12 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "due_date",
|
"fieldname": "due_date",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class PaymentLedgerEntry(Document):
|
|||||||
amount_in_account_currency: DF.Currency
|
amount_in_account_currency: DF.Currency
|
||||||
company: DF.Link | None
|
company: DF.Link | None
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
|
project: DF.Link | None
|
||||||
delinked: DF.Check
|
delinked: DF.Check
|
||||||
due_date: DF.Date | None
|
due_date: DF.Date | None
|
||||||
finance_book: DF.Link | None
|
finance_book: DF.Link | None
|
||||||
|
|||||||
@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
|
|||||||
ple = qb.DocType("Payment Ledger Entry")
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
for x in self.dimensions:
|
for x in self.dimensions:
|
||||||
dimension = x.fieldname
|
dimension = x.fieldname
|
||||||
if self.get(dimension):
|
if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
|
||||||
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
|
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
|
||||||
|
|
||||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ class PaymentRequest(Document):
|
|||||||
row_number += TO_SKIP_NEW_ROW
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist()
|
||||||
def make_payment_request(**args):
|
def make_payment_request(**args):
|
||||||
"""Make payment request"""
|
"""Make payment request"""
|
||||||
|
|
||||||
@@ -546,6 +546,9 @@ def make_payment_request(**args):
|
|||||||
if args.dn and not isinstance(args.dn, str):
|
if args.dn and not isinstance(args.dn, str):
|
||||||
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
|
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
|
||||||
|
|
||||||
|
frappe.has_permission("Payment Request", "create", throw=True)
|
||||||
|
frappe.has_permission(args.dt, "read", args.dn, throw=True)
|
||||||
|
|
||||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||||
if not args.get("company"):
|
if not args.get("company"):
|
||||||
args.company = ref_doc.company
|
args.company = ref_doc.company
|
||||||
@@ -819,7 +822,7 @@ def get_print_format_list(ref_doctype):
|
|||||||
return {"print_format": print_format_list}
|
return {"print_format": print_format_list}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist()
|
||||||
def resend_payment_email(docname):
|
def resend_payment_email(docname):
|
||||||
return frappe.get_doc("Payment Request", docname).send_email()
|
return frappe.get_doc("Payment Request", docname).send_email()
|
||||||
|
|
||||||
|
|||||||
@@ -159,15 +159,16 @@
|
|||||||
"language",
|
"language",
|
||||||
"column_break_84",
|
"column_break_84",
|
||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
|
"utm_analytics_section",
|
||||||
|
"utm_source",
|
||||||
|
"utm_medium",
|
||||||
|
"column_break_bhao",
|
||||||
|
"utm_campaign",
|
||||||
"more_information",
|
"more_information",
|
||||||
"inter_company_invoice_reference",
|
"inter_company_invoice_reference",
|
||||||
"customer_group",
|
"customer_group",
|
||||||
"is_discounted",
|
"is_discounted",
|
||||||
"col_break23",
|
"col_break23",
|
||||||
"utm_source",
|
|
||||||
"utm_campaign",
|
|
||||||
"utm_medium",
|
|
||||||
"column_break_gpiw",
|
|
||||||
"status",
|
"status",
|
||||||
"more_info",
|
"more_info",
|
||||||
"debit_to",
|
"debit_to",
|
||||||
@@ -1541,10 +1542,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Update Billed Amount in Delivery Note"
|
"label": "Update Billed Amount in Delivery Note"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_gpiw",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "utm_medium",
|
"fieldname": "utm_medium",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -1610,13 +1607,24 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Item Wise Tax Details",
|
"label": "Item Wise Tax Details",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Item Wise Tax Detail"
|
"options": "Item Wise Tax Detail",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "utm_analytics_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "UTM Analytics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bhao",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-04 22:22:31.471752",
|
"modified": "2026-02-10 14:23:07.181782",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice",
|
"name": "POS Invoice",
|
||||||
|
|||||||
@@ -897,6 +897,53 @@ class TestPOSInvoice(IntegrationTestCase):
|
|||||||
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||||
self.assertEqual(batch.qty, 5)
|
self.assertEqual(batch.qty, 5)
|
||||||
|
|
||||||
|
def test_pos_batch_reservation_with_return_qty(self):
|
||||||
|
"""
|
||||||
|
Test POS Invoice reserved qty for batch without bundle with return invoices.
|
||||||
|
"""
|
||||||
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
|
get_auto_batch_nos,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
|
create_batch_item_with_batch,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_batch_item_with_batch("_Batch Item Reserve Return", "TestBatch-RR 01")
|
||||||
|
se = make_stock_entry(
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
item_code="_Batch Item Reserve Return",
|
||||||
|
qty=30,
|
||||||
|
basic_rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
se.reload()
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
# POS Invoice for the batch without bundle
|
||||||
|
pos_inv = create_pos_invoice(item="_Batch Item Reserve Return", rate=300, qty=15, do_not_save=1)
|
||||||
|
pos_inv.append(
|
||||||
|
"payments",
|
||||||
|
{"mode_of_payment": "Cash", "amount": 4500},
|
||||||
|
)
|
||||||
|
pos_inv.items[0].batch_no = batch_no
|
||||||
|
pos_inv.save()
|
||||||
|
pos_inv.submit()
|
||||||
|
|
||||||
|
# POS Invoice return
|
||||||
|
pos_return = make_sales_return(pos_inv.name)
|
||||||
|
|
||||||
|
pos_return.insert()
|
||||||
|
pos_return.submit()
|
||||||
|
|
||||||
|
batches = get_auto_batch_nos(
|
||||||
|
frappe._dict({"item_code": "_Batch Item Reserve Return", "warehouse": "_Test Warehouse - _TC"})
|
||||||
|
)
|
||||||
|
|
||||||
|
for batch in batches:
|
||||||
|
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||||
|
self.assertEqual(batch.qty, 30)
|
||||||
|
|
||||||
def test_pos_batch_item_qty_validation(self):
|
def test_pos_batch_item_qty_validation(self):
|
||||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||||
BatchNegativeStockError,
|
BatchNegativeStockError,
|
||||||
|
|||||||
@@ -12,56 +12,78 @@
|
|||||||
"disabled",
|
"disabled",
|
||||||
"column_break_9",
|
"column_break_9",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"utm_source",
|
|
||||||
"utm_campaign",
|
|
||||||
"utm_medium",
|
|
||||||
"company_address",
|
"company_address",
|
||||||
"section_break_15",
|
"accounting_tab",
|
||||||
"applicable_for_users",
|
|
||||||
"section_break_11",
|
"section_break_11",
|
||||||
"payments",
|
"payments",
|
||||||
|
"set_grand_total_to_default_mop",
|
||||||
|
"price_list_and_currency_section",
|
||||||
|
"currency",
|
||||||
|
"column_break_bptt",
|
||||||
|
"selling_price_list",
|
||||||
|
"write_off_section",
|
||||||
|
"write_off_account",
|
||||||
|
"column_break_ukpz",
|
||||||
|
"write_off_cost_center",
|
||||||
|
"column_break_pkca",
|
||||||
|
"write_off_limit",
|
||||||
|
"income_and_expense_account",
|
||||||
|
"income_account",
|
||||||
|
"column_break_byzk",
|
||||||
|
"expense_account",
|
||||||
|
"taxes_section",
|
||||||
|
"taxes_and_charges",
|
||||||
|
"column_break_cjpp",
|
||||||
|
"tax_category",
|
||||||
|
"section_break_19",
|
||||||
|
"account_for_change_amount",
|
||||||
|
"disable_rounded_total",
|
||||||
|
"column_break_23",
|
||||||
|
"apply_discount_on",
|
||||||
|
"allow_partial_payment",
|
||||||
|
"accounting_dimensions_section",
|
||||||
|
"cost_center",
|
||||||
|
"dimension_col_break",
|
||||||
|
"project",
|
||||||
|
"pos_configurations_tab",
|
||||||
"section_break_14",
|
"section_break_14",
|
||||||
"hide_images",
|
|
||||||
"hide_unavailable_items",
|
|
||||||
"auto_add_item_to_cart",
|
|
||||||
"validate_stock_on_save",
|
|
||||||
"print_receipt_on_order_complete",
|
|
||||||
"action_on_new_invoice",
|
"action_on_new_invoice",
|
||||||
|
"validate_stock_on_save",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"update_stock",
|
"update_stock",
|
||||||
"ignore_pricing_rule",
|
"ignore_pricing_rule",
|
||||||
|
"print_receipt_on_order_complete",
|
||||||
|
"pos_item_selector_section",
|
||||||
|
"hide_images",
|
||||||
|
"column_break_rpny",
|
||||||
|
"hide_unavailable_items",
|
||||||
|
"column_break_stcl",
|
||||||
|
"auto_add_item_to_cart",
|
||||||
|
"pos_item_details_section",
|
||||||
"allow_rate_change",
|
"allow_rate_change",
|
||||||
|
"column_break_hwfg",
|
||||||
"allow_discount_change",
|
"allow_discount_change",
|
||||||
"set_grand_total_to_default_mop",
|
"column_break_egpi",
|
||||||
"allow_partial_payment",
|
"allow_warehouse_change",
|
||||||
|
"section_break_15",
|
||||||
|
"applicable_for_users",
|
||||||
"section_break_23",
|
"section_break_23",
|
||||||
"item_groups",
|
"item_groups",
|
||||||
"column_break_25",
|
"column_break_25",
|
||||||
"customer_groups",
|
"customer_groups",
|
||||||
|
"more_info_tab",
|
||||||
"section_break_16",
|
"section_break_16",
|
||||||
"print_format",
|
"print_format",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"column_break0",
|
"column_break0",
|
||||||
"tc_name",
|
"tc_name",
|
||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
"section_break_19",
|
"utm_analytics_section",
|
||||||
"selling_price_list",
|
"utm_source",
|
||||||
"currency",
|
"column_break_tvls",
|
||||||
"write_off_account",
|
"utm_campaign",
|
||||||
"write_off_cost_center",
|
"column_break_xygw",
|
||||||
"write_off_limit",
|
"utm_medium"
|
||||||
"account_for_change_amount",
|
|
||||||
"disable_rounded_total",
|
|
||||||
"column_break_23",
|
|
||||||
"income_account",
|
|
||||||
"expense_account",
|
|
||||||
"taxes_and_charges",
|
|
||||||
"tax_category",
|
|
||||||
"apply_discount_on",
|
|
||||||
"accounting_dimensions_section",
|
|
||||||
"cost_center",
|
|
||||||
"dimension_col_break",
|
|
||||||
"project"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -130,8 +152,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_14",
|
"fieldname": "section_break_14",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"label": "Configuration"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Only show Items from these Item Groups",
|
"description": "Only show Items from these Item Groups",
|
||||||
@@ -152,6 +173,7 @@
|
|||||||
"options": "POS Customer Group"
|
"options": "POS Customer Group"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"collapsible": 1,
|
||||||
"fieldname": "section_break_16",
|
"fieldname": "section_break_16",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Print Settings"
|
"label": "Print Settings"
|
||||||
@@ -191,7 +213,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_19",
|
"fieldname": "section_break_19",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Accounting"
|
"label": "Miscellaneous"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "selling_price_list",
|
"fieldname": "selling_price_list",
|
||||||
@@ -427,9 +449,111 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"description": "Applicable only on Transactions made using POS",
|
||||||
"fieldname": "allow_partial_payment",
|
"fieldname": "allow_partial_payment",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Partial Payment"
|
"label": "Allow Partial Payment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_tvls",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xygw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "accounting_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Accounting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "more_info_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "More Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pos_configurations_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "POS Configurations"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "price_list_and_currency_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Price List & Currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bptt",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "write_off_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Write Off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ukpz",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_pkca",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "income_and_expense_account",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Income and Expense"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_byzk",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "taxes_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Taxes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_cjpp",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pos_item_selector_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "POS Item Selector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_rpny",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_stcl",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "pos_item_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "POS Item Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_hwfg",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_egpi",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "allow_warehouse_change",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow User to Edit Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "utm_analytics_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Campaign"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -458,7 +582,7 @@
|
|||||||
"link_fieldname": "pos_profile"
|
"link_fieldname": "pos_profile"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-06-24 11:19:19.834905",
|
"modified": "2026-02-10 14:24:48.597412",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Profile",
|
"name": "POS Profile",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class POSProfile(Document):
|
|||||||
allow_discount_change: DF.Check
|
allow_discount_change: DF.Check
|
||||||
allow_partial_payment: DF.Check
|
allow_partial_payment: DF.Check
|
||||||
allow_rate_change: DF.Check
|
allow_rate_change: DF.Check
|
||||||
|
allow_warehouse_change: DF.Check
|
||||||
applicable_for_users: DF.Table[POSProfileUser]
|
applicable_for_users: DF.Table[POSProfileUser]
|
||||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||||
auto_add_item_to_cart: DF.Check
|
auto_add_item_to_cart: DF.Check
|
||||||
|
|||||||
@@ -98,8 +98,7 @@ def get_customers_list(pos_profile=None):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
frappe.db.sql(
|
frappe.db.sql(
|
||||||
f""" select name, customer_name, customer_group,
|
f""" select name, customer_name, customer_group, territory from tabCustomer where disabled = 0
|
||||||
territory, customer_pos_id from tabCustomer where disabled = 0
|
|
||||||
and {cond}""",
|
and {cond}""",
|
||||||
tuple(customer_groups),
|
tuple(customer_groups),
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Apply On",
|
"label": "Apply On",
|
||||||
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
|
"options": "Item Code\nItem Group\nBrand\nTransaction",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -657,7 +657,7 @@
|
|||||||
"icon": "fa fa-gift",
|
"icon": "fa fa-gift",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-20 11:40:07.096854",
|
"modified": "2026-02-17 12:24:07.553505",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule",
|
"name": "Pricing Rule",
|
||||||
@@ -714,9 +714,10 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "title"
|
"title_field": "title"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class PricingRule(Document):
|
|||||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||||
apply_discount_on_rate: DF.Check
|
apply_discount_on_rate: DF.Check
|
||||||
apply_multiple_pricing_rules: DF.Check
|
apply_multiple_pricing_rules: DF.Check
|
||||||
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"]
|
apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
|
||||||
apply_recursion_over: DF.Float
|
apply_recursion_over: DF.Float
|
||||||
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
|
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
|
||||||
brands: DF.Table[PricingRuleBrand]
|
brands: DF.Table[PricingRuleBrand]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
"depends_on": "eval:parent.apply_on == 'Brand'",
|
||||||
"fieldname": "brand",
|
"fieldname": "brand",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -28,14 +28,15 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:17.857046",
|
"modified": "2026-02-17 12:17:13.073587",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule Brand",
|
"name": "Pricing Rule Brand",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
"depends_on": "eval:parent.apply_on == 'Item Group'",
|
||||||
"fieldname": "item_group",
|
"fieldname": "item_group",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -28,14 +28,15 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:18.221095",
|
"modified": "2026-02-17 12:16:57.778471",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule Item Group",
|
"name": "Pricing Rule Item Group",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,8 +415,9 @@ def reconcile(doc: None | str = None) -> None:
|
|||||||
for x in allocations:
|
for x in allocations:
|
||||||
pr.append("allocation", x)
|
pr.append("allocation", x)
|
||||||
|
|
||||||
|
skip_ref_details_update_for_pe = check_multi_currency(pr)
|
||||||
# reconcile
|
# reconcile
|
||||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
|
pr.reconcile_allocations(skip_ref_details_update_for_pe=skip_ref_details_update_for_pe)
|
||||||
|
|
||||||
# If Payment Entry, update details only for newly linked references
|
# If Payment Entry, update details only for newly linked references
|
||||||
# This is for performance
|
# This is for performance
|
||||||
@@ -504,6 +505,37 @@ def reconcile(doc: None | str = None) -> None:
|
|||||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||||
|
|
||||||
|
|
||||||
|
def check_multi_currency(pr_doc):
|
||||||
|
GL = frappe.qb.DocType("GL Entry")
|
||||||
|
Account = frappe.qb.DocType("Account")
|
||||||
|
|
||||||
|
def get_account_currency(voucher_type, voucher_no):
|
||||||
|
currency = (
|
||||||
|
frappe.qb.from_(GL)
|
||||||
|
.join(Account)
|
||||||
|
.on(GL.account == Account.name)
|
||||||
|
.select(Account.account_currency)
|
||||||
|
.where(
|
||||||
|
(GL.voucher_type == voucher_type)
|
||||||
|
& (GL.voucher_no == voucher_no)
|
||||||
|
& (Account.account_type.isin(["Payable", "Receivable"]))
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
return currency[0].account_currency if currency else None
|
||||||
|
|
||||||
|
for allocation in pr_doc.allocation:
|
||||||
|
reference_currency = get_account_currency(allocation.reference_type, allocation.reference_name)
|
||||||
|
|
||||||
|
invoice_currency = get_account_currency(allocation.invoice_type, allocation.invoice_number)
|
||||||
|
|
||||||
|
if reference_currency != invoice_currency:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||||
running_doc = None
|
running_doc = None
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ cur_frm.fields_dict["select_print_heading"].get_query = function (doc, cdt, cdn)
|
|||||||
|
|
||||||
cur_frm.set_query("wip_composite_asset", "items", function () {
|
cur_frm.set_query("wip_composite_asset", "items", function () {
|
||||||
return {
|
return {
|
||||||
filters: { is_composite_asset: 1, docstatus: 0 },
|
filters: { asset_type: "Composite Asset", docstatus: 0 },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -85,20 +85,24 @@
|
|||||||
"taxes_and_charges_added",
|
"taxes_and_charges_added",
|
||||||
"taxes_and_charges_deducted",
|
"taxes_and_charges_deducted",
|
||||||
"total_taxes_and_charges",
|
"total_taxes_and_charges",
|
||||||
"section_break_49",
|
"totals_section",
|
||||||
|
"grand_total",
|
||||||
|
"disable_rounded_total",
|
||||||
|
"rounding_adjustment",
|
||||||
|
"column_break8",
|
||||||
|
"use_company_roundoff_cost_center",
|
||||||
|
"in_words",
|
||||||
|
"rounded_total",
|
||||||
|
"base_totals_section",
|
||||||
"base_grand_total",
|
"base_grand_total",
|
||||||
"base_rounding_adjustment",
|
"base_rounding_adjustment",
|
||||||
"base_rounded_total",
|
"column_break_hcca",
|
||||||
"base_in_words",
|
"base_in_words",
|
||||||
"column_break8",
|
"base_rounded_total",
|
||||||
"grand_total",
|
"section_break_ttrv",
|
||||||
"rounding_adjustment",
|
|
||||||
"use_company_roundoff_cost_center",
|
|
||||||
"rounded_total",
|
|
||||||
"in_words",
|
|
||||||
"total_advance",
|
"total_advance",
|
||||||
|
"column_break_peap",
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
"disable_rounded_total",
|
|
||||||
"section_tax_withholding_entry",
|
"section_tax_withholding_entry",
|
||||||
"tax_withholding_group",
|
"tax_withholding_group",
|
||||||
"ignore_tax_withholding_threshold",
|
"ignore_tax_withholding_threshold",
|
||||||
@@ -606,6 +610,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.items.every((item) => !item.pr_detail)",
|
||||||
"fieldname": "update_stock",
|
"fieldname": "update_stock",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Update Stock",
|
"label": "Update Stock",
|
||||||
@@ -882,15 +887,10 @@
|
|||||||
"options": "currency",
|
"options": "currency",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_49",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Totals"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "base_grand_total",
|
"fieldname": "base_grand_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Grand Total (Company Currency)",
|
"label": "Grand Total",
|
||||||
"oldfieldname": "grand_total",
|
"oldfieldname": "grand_total",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
@@ -901,7 +901,7 @@
|
|||||||
"depends_on": "eval:!doc.disable_rounded_total",
|
"depends_on": "eval:!doc.disable_rounded_total",
|
||||||
"fieldname": "base_rounding_adjustment",
|
"fieldname": "base_rounding_adjustment",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Rounding Adjustment (Company Currency)",
|
"label": "Rounding Adjustment",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@@ -911,7 +911,7 @@
|
|||||||
"depends_on": "eval:!doc.disable_rounded_total",
|
"depends_on": "eval:!doc.disable_rounded_total",
|
||||||
"fieldname": "base_rounded_total",
|
"fieldname": "base_rounded_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Rounded Total (Company Currency)",
|
"label": "Rounded Total",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@@ -920,7 +920,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "base_in_words",
|
"fieldname": "base_in_words",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "In Words (Company Currency)",
|
"label": "In Words",
|
||||||
"length": 240,
|
"length": 240,
|
||||||
"oldfieldname": "in_words",
|
"oldfieldname": "in_words",
|
||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data",
|
||||||
@@ -1660,6 +1660,28 @@
|
|||||||
"fieldname": "override_tax_withholding_entries",
|
"fieldname": "override_tax_withholding_entries",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Edit Tax Withholding Entries"
|
"label": "Edit Tax Withholding Entries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_hcca",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ttrv",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_peap",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_totals_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Totals (Company Currency)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "totals_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Totals"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -1667,7 +1689,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-12-15 06:41:38.237728",
|
"modified": "2026-02-05 20:45:16.964500",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update
|
|||||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||||
from erpnext.controllers.accounts_controller import validate_account_head
|
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
|
||||||
from erpnext.controllers.buying_controller import BuyingController
|
from erpnext.controllers.buying_controller import BuyingController
|
||||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||||
update_billed_amount_based_on_po,
|
update_billed_amount_based_on_po,
|
||||||
@@ -2005,9 +2005,17 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
|||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
def post_parent_process(source_parent, target_parent):
|
def post_parent_process(source_parent, target_parent):
|
||||||
for row in target_parent.get("items"):
|
remove_items_with_zero_qty(target_parent)
|
||||||
if row.get("qty") == 0:
|
set_missing_values(source_parent, target_parent)
|
||||||
target_parent.remove(row)
|
|
||||||
|
def remove_items_with_zero_qty(target_parent):
|
||||||
|
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
|
||||||
|
|
||||||
|
def set_missing_values(source_parent, target_parent):
|
||||||
|
target_parent.run_method("set_missing_values")
|
||||||
|
if args and args.get("merge_taxes"):
|
||||||
|
merge_taxes(source_parent, target_parent)
|
||||||
|
target_parent.run_method("calculate_taxes_and_totals")
|
||||||
|
|
||||||
def update_item(obj, target, source_parent):
|
def update_item(obj, target, source_parent):
|
||||||
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
|
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
|
||||||
@@ -2059,7 +2067,11 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
|||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||||
},
|
},
|
||||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
"Purchase Taxes and Charges": {
|
||||||
|
"doctype": "Purchase Taxes and Charges",
|
||||||
|
"reset_value": not (args and args.get("merge_taxes")),
|
||||||
|
"ignore": args.get("merge_taxes") if args else 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
post_parent_process,
|
post_parent_process,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"stock_uom_rate",
|
"stock_uom_rate",
|
||||||
"is_free_item",
|
"is_free_item",
|
||||||
"apply_tds",
|
"apply_tds",
|
||||||
|
"allow_zero_valuation_rate",
|
||||||
"section_break_22",
|
"section_break_22",
|
||||||
"net_rate",
|
"net_rate",
|
||||||
"net_amount",
|
"net_amount",
|
||||||
@@ -97,7 +98,6 @@
|
|||||||
"service_start_date",
|
"service_start_date",
|
||||||
"service_end_date",
|
"service_end_date",
|
||||||
"reference",
|
"reference",
|
||||||
"allow_zero_valuation_rate",
|
|
||||||
"item_tax_rate",
|
"item_tax_rate",
|
||||||
"bom",
|
"bom",
|
||||||
"include_exploded_items",
|
"include_exploded_items",
|
||||||
@@ -420,6 +420,7 @@
|
|||||||
"options": "UOM"
|
"options": "UOM"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:parent.update_stock",
|
||||||
"fieldname": "warehouse_section",
|
"fieldname": "warehouse_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Warehouse"
|
"label": "Warehouse"
|
||||||
@@ -447,7 +448,6 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
@@ -459,14 +459,12 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Serial No",
|
"label": "Serial No",
|
||||||
"no_copy": 1
|
"no_copy": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.is_fixed_asset && doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
|
||||||
"fieldname": "rejected_serial_no",
|
"fieldname": "rejected_serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Rejected Serial No",
|
"label": "Rejected Serial No",
|
||||||
@@ -577,6 +575,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"depends_on": "eval:parent.update_stock",
|
||||||
"fieldname": "allow_zero_valuation_rate",
|
"fieldname": "allow_zero_valuation_rate",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Zero Valuation Rate",
|
"label": "Allow Zero Valuation Rate",
|
||||||
@@ -800,7 +799,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.is_internal_supplier && parent.update_stock",
|
"depends_on": "eval:parent.is_internal_supplier",
|
||||||
"fieldname": "from_warehouse",
|
"fieldname": "from_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
@@ -896,7 +895,7 @@
|
|||||||
"label": "Consider for Tax Withholding"
|
"label": "Consider for Tax Withholding"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
@@ -906,7 +905,7 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "rejected_serial_and_batch_bundle",
|
"fieldname": "rejected_serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Rejected Serial and Batch Bundle",
|
"label": "Rejected Serial and Batch Bundle",
|
||||||
@@ -922,7 +921,7 @@
|
|||||||
"options": "Asset"
|
"options": "Asset"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock === 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
|
||||||
"fieldname": "add_serial_batch_bundle",
|
"fieldname": "add_serial_batch_bundle",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Add Serial / Batch No"
|
"label": "Add Serial / Batch No"
|
||||||
@@ -992,7 +991,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-12-13 14:10:02.379392",
|
"modified": "2026-02-15 21:07:49.455930",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
"Serial and Batch Bundle",
|
"Serial and Batch Bundle",
|
||||||
"Bank Transaction",
|
"Bank Transaction",
|
||||||
|
"Packing Slip",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
if (!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||||
@@ -115,18 +116,21 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cint(doc.update_stock) != 1) {
|
if (cint(doc.update_stock) != 1) {
|
||||||
// show Make Delivery Note button only if Sales Invoice is not created from Delivery Note
|
if (!is_delivered_by_supplier) {
|
||||||
var from_delivery_note = false;
|
const should_create_delivery_note = doc.items.some(
|
||||||
from_delivery_note = this.frm.doc.items.some(function (item) {
|
(item) =>
|
||||||
return item.delivery_note ? true : false;
|
item.qty - item.delivered_qty > 0 &&
|
||||||
});
|
!item.scio_detail &&
|
||||||
|
!item.dn_detail &&
|
||||||
if (!from_delivery_note && !is_delivered_by_supplier) {
|
!item.delivered_by_supplier
|
||||||
this.frm.add_custom_button(
|
|
||||||
__("Delivery"),
|
|
||||||
this.frm.cscript["Make Delivery Note"],
|
|
||||||
__("Create")
|
|
||||||
);
|
);
|
||||||
|
if (should_create_delivery_note) {
|
||||||
|
this.frm.add_custom_button(
|
||||||
|
__("Delivery Note"),
|
||||||
|
this.frm.cscript["Make Delivery Note"],
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"customer_section",
|
"customer_section",
|
||||||
|
"company",
|
||||||
|
"company_tax_id",
|
||||||
"naming_series",
|
"naming_series",
|
||||||
"customer",
|
"customer",
|
||||||
"customer_name",
|
"customer_name",
|
||||||
"tax_id",
|
"tax_id",
|
||||||
"company",
|
|
||||||
"company_tax_id",
|
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"posting_time",
|
"posting_time",
|
||||||
@@ -77,34 +77,36 @@
|
|||||||
"base_total_taxes_and_charges",
|
"base_total_taxes_and_charges",
|
||||||
"column_break_47",
|
"column_break_47",
|
||||||
"total_taxes_and_charges",
|
"total_taxes_and_charges",
|
||||||
"totals",
|
"totals_section",
|
||||||
"base_grand_total",
|
|
||||||
"base_rounding_adjustment",
|
|
||||||
"base_rounded_total",
|
|
||||||
"base_in_words",
|
|
||||||
"column_break5",
|
|
||||||
"grand_total",
|
"grand_total",
|
||||||
"rounding_adjustment",
|
"rounding_adjustment",
|
||||||
"use_company_roundoff_cost_center",
|
|
||||||
"rounded_total",
|
|
||||||
"in_words",
|
"in_words",
|
||||||
|
"column_break5",
|
||||||
|
"rounded_total",
|
||||||
|
"disable_rounded_total",
|
||||||
"total_advance",
|
"total_advance",
|
||||||
"outstanding_amount",
|
"outstanding_amount",
|
||||||
"disable_rounded_total",
|
"use_company_roundoff_cost_center",
|
||||||
|
"base_totals_section",
|
||||||
|
"base_grand_total",
|
||||||
|
"base_rounding_adjustment",
|
||||||
|
"base_in_words",
|
||||||
|
"column_break_xjag",
|
||||||
|
"base_rounded_total",
|
||||||
"section_tax_withholding_entry",
|
"section_tax_withholding_entry",
|
||||||
"tax_withholding_group",
|
"tax_withholding_group",
|
||||||
"ignore_tax_withholding_threshold",
|
"ignore_tax_withholding_threshold",
|
||||||
"override_tax_withholding_entries",
|
"override_tax_withholding_entries",
|
||||||
"tax_withholding_entries",
|
"tax_withholding_entries",
|
||||||
"section_break_49",
|
"additional_discount_section",
|
||||||
"apply_discount_on",
|
"apply_discount_on",
|
||||||
"base_discount_amount",
|
"base_discount_amount",
|
||||||
"coupon_code",
|
"coupon_code",
|
||||||
"is_cash_or_non_trade_discount",
|
|
||||||
"additional_discount_account",
|
|
||||||
"column_break_51",
|
"column_break_51",
|
||||||
"additional_discount_percentage",
|
"additional_discount_percentage",
|
||||||
"discount_amount",
|
"discount_amount",
|
||||||
|
"is_cash_or_non_trade_discount",
|
||||||
|
"additional_discount_account",
|
||||||
"sec_tax_breakup",
|
"sec_tax_breakup",
|
||||||
"other_charges_calculation",
|
"other_charges_calculation",
|
||||||
"item_wise_tax_details",
|
"item_wise_tax_details",
|
||||||
@@ -194,13 +196,13 @@
|
|||||||
"column_break8",
|
"column_break8",
|
||||||
"unrealized_profit_loss_account",
|
"unrealized_profit_loss_account",
|
||||||
"against_income_account",
|
"against_income_account",
|
||||||
"sales_team_section_break",
|
"commission_section",
|
||||||
"sales_partner",
|
"sales_partner",
|
||||||
"amount_eligible_for_commission",
|
"amount_eligible_for_commission",
|
||||||
"column_break10",
|
"column_break10",
|
||||||
"commission_rate",
|
"commission_rate",
|
||||||
"total_commission",
|
"total_commission",
|
||||||
"section_break2",
|
"sales_team_section",
|
||||||
"sales_team",
|
"sales_team",
|
||||||
"edit_printing_settings",
|
"edit_printing_settings",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
@@ -215,20 +217,21 @@
|
|||||||
"column_break_140",
|
"column_break_140",
|
||||||
"to_date",
|
"to_date",
|
||||||
"update_auto_repeat_reference",
|
"update_auto_repeat_reference",
|
||||||
|
"utm_analytics_section",
|
||||||
|
"utm_source",
|
||||||
|
"utm_medium",
|
||||||
|
"column_break_ixxw",
|
||||||
|
"utm_campaign",
|
||||||
|
"utm_content",
|
||||||
"more_information",
|
"more_information",
|
||||||
"status",
|
"status",
|
||||||
"inter_company_invoice_reference",
|
"remarks",
|
||||||
"represents_company",
|
|
||||||
"customer_group",
|
"customer_group",
|
||||||
"column_break_imbx",
|
"column_break_imbx",
|
||||||
"utm_source",
|
|
||||||
"utm_campaign",
|
|
||||||
"utm_medium",
|
|
||||||
"utm_content",
|
|
||||||
"col_break23",
|
|
||||||
"is_internal_customer",
|
"is_internal_customer",
|
||||||
|
"represents_company",
|
||||||
|
"inter_company_invoice_reference",
|
||||||
"is_discounted",
|
"is_discounted",
|
||||||
"remarks",
|
|
||||||
"connections_tab"
|
"connections_tab"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -703,6 +706,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
|
||||||
"fieldname": "update_stock",
|
"fieldname": "update_stock",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@@ -793,7 +797,8 @@
|
|||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Time Sheets",
|
"label": "Time Sheets",
|
||||||
"options": "Sales Invoice Timesheet",
|
"options": "Sales Invoice Timesheet",
|
||||||
"print_hide": 1
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -1072,14 +1077,6 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "section_break_49",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hide_days": 1,
|
|
||||||
"hide_seconds": 1,
|
|
||||||
"label": "Additional Discount"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "Grand Total",
|
"default": "Grand Total",
|
||||||
"fieldname": "apply_discount_on",
|
"fieldname": "apply_discount_on",
|
||||||
@@ -1124,22 +1121,12 @@
|
|||||||
"options": "currency",
|
"options": "currency",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "totals",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hide_days": 1,
|
|
||||||
"hide_seconds": 1,
|
|
||||||
"label": "Totals",
|
|
||||||
"oldfieldtype": "Section Break",
|
|
||||||
"options": "fa fa-money",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "base_grand_total",
|
"fieldname": "base_grand_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Grand Total (Company Currency)",
|
"label": "Grand Total",
|
||||||
"oldfieldname": "grand_total",
|
"oldfieldname": "grand_total",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
@@ -1153,9 +1140,8 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Rounding Adjustment (Company Currency)",
|
"label": "Rounding Adjustment",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Company:company:default_currency",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -1165,10 +1151,9 @@
|
|||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Rounded Total (Company Currency)",
|
"label": "Rounded Total",
|
||||||
"oldfieldname": "rounded_total",
|
"oldfieldname": "rounded_total",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "Company:company:default_currency",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
@@ -1178,7 +1163,7 @@
|
|||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "In Words (Company Currency)",
|
"label": "In Words",
|
||||||
"length": 240,
|
"length": 240,
|
||||||
"oldfieldname": "in_words",
|
"oldfieldname": "in_words",
|
||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data",
|
||||||
@@ -1271,7 +1256,6 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
|
||||||
"collapsible_depends_on": "advances",
|
"collapsible_depends_on": "advances",
|
||||||
"fieldname": "advances_section",
|
"fieldname": "advances_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@@ -1647,13 +1631,6 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "col_break23",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"hide_days": 1,
|
|
||||||
"hide_seconds": 1,
|
|
||||||
"width": "50%"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "Draft",
|
"default": "Draft",
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
@@ -1705,10 +1682,10 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
|
||||||
"default": "No",
|
"default": "No",
|
||||||
"fieldname": "is_opening",
|
"fieldname": "is_opening",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
|
"hidden": 1,
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Is Opening Entry",
|
"label": "Is Opening Entry",
|
||||||
@@ -1737,18 +1714,6 @@
|
|||||||
"oldfieldtype": "Text",
|
"oldfieldtype": "Text",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"collapsible_depends_on": "sales_partner",
|
|
||||||
"fieldname": "sales_team_section_break",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hide_days": 1,
|
|
||||||
"hide_seconds": 1,
|
|
||||||
"label": "Commission",
|
|
||||||
"oldfieldtype": "Section Break",
|
|
||||||
"options": "fa fa-group",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "sales_partner",
|
"fieldname": "sales_partner",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -1792,16 +1757,6 @@
|
|||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"collapsible_depends_on": "sales_team",
|
|
||||||
"fieldname": "section_break2",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hide_days": 1,
|
|
||||||
"hide_seconds": 1,
|
|
||||||
"label": "Sales Team",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "sales_team",
|
"fieldname": "sales_team",
|
||||||
@@ -2250,7 +2205,8 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Item Wise Tax Details",
|
"label": "Item Wise Tax Details",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Item Wise Tax Detail"
|
"options": "Item Wise Tax Detail",
|
||||||
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -2291,6 +2247,66 @@
|
|||||||
"fieldname": "override_tax_withholding_entries",
|
"fieldname": "override_tax_withholding_entries",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Edit Tax Withholding Entries"
|
"label": "Edit Tax Withholding Entries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "totals_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_days": 1,
|
||||||
|
"hide_seconds": 1,
|
||||||
|
"label": "Totals",
|
||||||
|
"oldfieldtype": "Section Break",
|
||||||
|
"options": "fa fa-money",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_totals_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Totals (Company Currency)",
|
||||||
|
"options": "Company:company:default_currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_xjag",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "additional_discount_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_days": 1,
|
||||||
|
"hide_seconds": 1,
|
||||||
|
"label": "Additional Discount"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "sales_team",
|
||||||
|
"fieldname": "sales_team_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_days": 1,
|
||||||
|
"hide_seconds": 1,
|
||||||
|
"label": "Sales Team",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "sales_partner",
|
||||||
|
"fieldname": "commission_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_days": 1,
|
||||||
|
"hide_seconds": 1,
|
||||||
|
"label": "Commission",
|
||||||
|
"oldfieldtype": "Section Break",
|
||||||
|
"options": "fa fa-group",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ixxw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "utm_analytics_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "UTM Analytics"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -2304,7 +2320,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-12-24 18:29:50.242618",
|
"modified": "2026-02-10 11:59:07.819903",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -2470,7 +2470,10 @@ def make_delivery_note(source_name, target_doc=None):
|
|||||||
"cost_center": "cost_center",
|
"cost_center": "cost_center",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail,
|
"condition": lambda doc: doc.delivered_by_supplier != 1
|
||||||
|
and not doc.scio_detail
|
||||||
|
and not doc.dn_detail
|
||||||
|
and doc.qty - doc.delivered_qty > 0,
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||||
"Sales Team": {
|
"Sales Team": {
|
||||||
|
|||||||
@@ -4739,6 +4739,66 @@ class TestSalesInvoice(ERPNextTestSuite):
|
|||||||
|
|
||||||
doc.db_set("do_not_use_batchwise_valuation", original_value)
|
doc.db_set("do_not_use_batchwise_valuation", original_value)
|
||||||
|
|
||||||
|
@change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True})
|
||||||
|
def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self):
|
||||||
|
item_code = "_Test Item for Expiry Batch Zero Valuation"
|
||||||
|
make_item_for_si(
|
||||||
|
item_code,
|
||||||
|
{
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"has_expiry_date": 1,
|
||||||
|
"shelf_life_in_days": 2,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "TBATCH-EBZV.####",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
qty=10,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
# fetch batch no from bundle
|
||||||
|
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
si = create_sales_invoice(
|
||||||
|
posting_date=add_days(nowdate(), 3),
|
||||||
|
item=item_code,
|
||||||
|
qty=-10,
|
||||||
|
rate=100,
|
||||||
|
is_return=1,
|
||||||
|
update_stock=1,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
do_not_save=1,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
si.items[0].batch_no = batch_no
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
si.reload()
|
||||||
|
# check zero incoming rate in voucher
|
||||||
|
self.assertEqual(si.items[0].incoming_rate, 0.0)
|
||||||
|
|
||||||
|
# chekc zero incoming rate in stock ledger
|
||||||
|
stock_ledger_entry = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{
|
||||||
|
"voucher_type": "Sales Invoice",
|
||||||
|
"voucher_no": si.name,
|
||||||
|
"item_code": item_code,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
},
|
||||||
|
["incoming_rate", "valuation_rate"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
|
||||||
|
|
||||||
|
|
||||||
def make_item_for_si(item_code, properties=None):
|
def make_item_for_si(item_code, properties=None):
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"is_free_item",
|
"is_free_item",
|
||||||
"apply_tds",
|
"apply_tds",
|
||||||
"grant_commission",
|
"grant_commission",
|
||||||
|
"allow_zero_valuation_rate",
|
||||||
"section_break_21",
|
"section_break_21",
|
||||||
"net_rate",
|
"net_rate",
|
||||||
"net_amount",
|
"net_amount",
|
||||||
@@ -88,7 +89,6 @@
|
|||||||
"serial_and_batch_bundle",
|
"serial_and_batch_bundle",
|
||||||
"use_serial_batch_fields",
|
"use_serial_batch_fields",
|
||||||
"col_break5",
|
"col_break5",
|
||||||
"allow_zero_valuation_rate",
|
|
||||||
"incoming_rate",
|
"incoming_rate",
|
||||||
"item_tax_rate",
|
"item_tax_rate",
|
||||||
"actual_batch_qty",
|
"actual_batch_qty",
|
||||||
@@ -580,6 +580,7 @@
|
|||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"collapsible_depends_on": "eval:doc.serial_no || doc.batch_no",
|
"collapsible_depends_on": "eval:doc.serial_no || doc.batch_no",
|
||||||
|
"depends_on": "eval:parent.update_stock",
|
||||||
"fieldname": "warehouse_and_reference",
|
"fieldname": "warehouse_and_reference",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Stock Details"
|
"label": "Stock Details"
|
||||||
@@ -595,7 +596,7 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: parent.is_internal_customer && parent.update_stock",
|
"depends_on": "eval: parent.is_internal_customer",
|
||||||
"fieldname": "target_warehouse",
|
"fieldname": "target_warehouse",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
@@ -613,7 +614,6 @@
|
|||||||
"options": "Quality Inspection"
|
"options": "Quality Inspection"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
|
||||||
"fieldname": "batch_no",
|
"fieldname": "batch_no",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Batch No",
|
"label": "Batch No",
|
||||||
@@ -626,6 +626,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"depends_on": "eval:parent.update_stock",
|
||||||
"fieldname": "allow_zero_valuation_rate",
|
"fieldname": "allow_zero_valuation_rate",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Zero Valuation Rate",
|
"label": "Allow Zero Valuation Rate",
|
||||||
@@ -633,7 +634,6 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.use_serial_batch_fields === 1 && parent.update_stock === 1",
|
|
||||||
"fieldname": "serial_no",
|
"fieldname": "serial_no",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Serial No",
|
"label": "Serial No",
|
||||||
@@ -906,7 +906,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock == 1 && (doc.use_serial_batch_fields === 0 || doc.docstatus === 1)",
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.docstatus === 1",
|
||||||
"fieldname": "serial_and_batch_bundle",
|
"fieldname": "serial_and_batch_bundle",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Serial and Batch Bundle",
|
"label": "Serial and Batch Bundle",
|
||||||
@@ -916,7 +916,7 @@
|
|||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock === 1",
|
"depends_on": "eval:doc.use_serial_batch_fields === 0 && doc.docstatus === 0",
|
||||||
"fieldname": "pick_serial_and_batch",
|
"fieldname": "pick_serial_and_batch",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Pick Serial / Batch No"
|
"label": "Pick Serial / Batch No"
|
||||||
@@ -1009,7 +1009,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-09-04 11:08:25.583561",
|
"modified": "2026-02-15 21:08:57.341638",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
"depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)",
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -85,14 +85,15 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:36.427565",
|
"modified": "2026-02-16 20:46:34.592604",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Payment",
|
"name": "Sales Invoice Payment",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,16 +43,18 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:10:55.008837",
|
"modified": "2025-11-14 16:17:25.584675",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Transaction Deletion Record Details",
|
"name": "Transaction Deletion Record Details",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-xs-6">
|
<div class="col-xs-6">
|
||||||
<table>
|
<table>
|
||||||
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.creation) }}</td></tr>
|
<tr><td><strong>Date: </strong></td><td>{{ frappe.utils.format_date(doc.posting_date) }}</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
collapsible_filters: true,
|
||||||
|
separate_check_filters: true,
|
||||||
|
|
||||||
formatter: function (value, row, column, data, default_formatter) {
|
formatter: function (value, row, column, data, default_formatter) {
|
||||||
value = default_formatter(value, row, column, data);
|
value = default_formatter(value, row, column, data);
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
|||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
collapsible_filters: true,
|
||||||
|
separate_check_filters: true,
|
||||||
|
|
||||||
onload: function (report) {
|
onload: function (report) {
|
||||||
report.page.add_inner_button(__("Accounts Payable"), function () {
|
report.page.add_inner_button(__("Accounts Payable"), function () {
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
collapsible_filters: true,
|
||||||
|
separate_check_filters: true,
|
||||||
|
|
||||||
formatter: function (value, row, column, data, default_formatter) {
|
formatter: function (value, row, column, data, default_formatter) {
|
||||||
value = default_formatter(value, row, column, data);
|
value = default_formatter(value, row, column, data);
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
|||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
collapsible_filters: true,
|
||||||
|
separate_check_filters: true,
|
||||||
|
|
||||||
onload: function (report) {
|
onload: function (report) {
|
||||||
report.page.add_inner_button(__("Accounts Receivable"), function () {
|
report.page.add_inner_button(__("Accounts Receivable"), function () {
|
||||||
|
|||||||
@@ -232,11 +232,11 @@ def get_report_summary(
|
|||||||
|
|
||||||
|
|
||||||
def get_chart_data(filters, columns, asset, liability, equity, currency):
|
def get_chart_data(filters, columns, asset, liability, equity, currency):
|
||||||
labels = [d.get("label") for d in columns[2:]]
|
labels = [d.get("label") for d in columns[4:]]
|
||||||
|
|
||||||
asset_data, liability_data, equity_data = [], [], []
|
asset_data, liability_data, equity_data = [], [], []
|
||||||
|
|
||||||
for p in columns[2:]:
|
for p in columns[4:]:
|
||||||
if asset:
|
if asset:
|
||||||
asset_data.append(asset[-2].get(p.get("fieldname")))
|
asset_data.append(asset[-2].get(p.get("fieldname")))
|
||||||
if liability:
|
if liability:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from frappe.query_builder import Criterion, Tuple
|
|||||||
from frappe.query_builder.functions import IfNull
|
from frappe.query_builder.functions import IfNull
|
||||||
from frappe.utils import getdate, nowdate
|
from frappe.utils import getdate, nowdate
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
from pypika.terms import LiteralValue
|
from pypika.terms import Bracket, LiteralValue
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
@@ -84,10 +84,8 @@ class PartyLedgerSummaryReport:
|
|||||||
|
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
match_conditions = build_match_conditions(party_type)
|
if match_conditions := build_match_conditions(party_type):
|
||||||
|
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||||
if match_conditions:
|
|
||||||
query = query.where(LiteralValue(match_conditions))
|
|
||||||
|
|
||||||
party_details = query.run(as_dict=True)
|
party_details = query.run(as_dict=True)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Max, Min, Sum
|
from frappe.query_builder.functions import Max, Min, Sum
|
||||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||||
from pypika.terms import ExistsCriterion
|
from pypika.terms import Bracket, ExistsCriterion, LiteralValue
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
@@ -564,18 +564,15 @@ def get_accounting_entries(
|
|||||||
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
|
account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry)
|
||||||
query = query.where(ExistsCriterion(account_filter_query))
|
query = query.where(ExistsCriterion(account_filter_query))
|
||||||
|
|
||||||
|
if group_by_account:
|
||||||
|
query = query.groupby("account")
|
||||||
|
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
query, params = query.walk()
|
if match_conditions := build_match_conditions(doctype):
|
||||||
match_conditions = build_match_conditions(doctype)
|
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||||
|
|
||||||
if match_conditions:
|
return query.run(as_dict=True)
|
||||||
query += "and" + match_conditions
|
|
||||||
|
|
||||||
if group_by_account:
|
|
||||||
query += " GROUP BY `account`"
|
|
||||||
|
|
||||||
return frappe.db.sql(query, params, as_dict=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):
|
def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry):
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
collapsible_filters: true,
|
collapsible_filters: true,
|
||||||
seperate_check_filters: true,
|
separate_check_filters: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
erpnext.utils.add_dimensions("General Ledger", 15);
|
erpnext.utils.add_dimensions("General Ledger", 15);
|
||||||
|
|||||||
@@ -324,10 +324,8 @@ def get_conditions(filters):
|
|||||||
|
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
match_conditions = build_match_conditions("GL Entry")
|
if match_conditions := build_match_conditions("GL Entry"):
|
||||||
|
conditions.append(f"({match_conditions})")
|
||||||
if match_conditions:
|
|
||||||
conditions.append(match_conditions)
|
|
||||||
|
|
||||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,16 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, scrub
|
from frappe import _, qb, scrub
|
||||||
from frappe.query_builder import Order
|
from frappe.query_builder import Case, Order
|
||||||
|
from frappe.query_builder.functions import Coalesce
|
||||||
from frappe.utils import cint, flt, formatdate
|
from frappe.utils import cint, flt, formatdate
|
||||||
|
from pypika.terms import ExistsCriterion
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
get_dimension_with_children,
|
get_dimension_with_children,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||||
from erpnext.controllers.queries import get_match_cond
|
|
||||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
from erpnext.stock.utils import get_incoming_rate
|
||||||
|
|
||||||
@@ -176,7 +177,9 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
|||||||
column_names = get_column_names()
|
column_names = get_column_names()
|
||||||
|
|
||||||
# to display item as Item Code: Item Name
|
# to display item as Item Code: Item Name
|
||||||
columns[0] = "Sales Invoice:Link/Item:300"
|
columns[0]["fieldname"] = "sales_invoice"
|
||||||
|
columns[0]["options"] = "Item"
|
||||||
|
columns[0]["width"] = 300
|
||||||
# removing Item Code and Item Name columns
|
# removing Item Code and Item Name columns
|
||||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||||
@@ -203,7 +206,11 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
|||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
total_gross_profit = total_base_amount - total_buying_amount
|
total_gross_profit = flt(
|
||||||
|
total_base_amount + abs(total_buying_amount)
|
||||||
|
if total_buying_amount < 0
|
||||||
|
else total_base_amount - total_buying_amount,
|
||||||
|
)
|
||||||
data.append(
|
data.append(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
@@ -215,7 +222,7 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
|||||||
"buying_amount": total_buying_amount,
|
"buying_amount": total_buying_amount,
|
||||||
"gross_profit": total_gross_profit,
|
"gross_profit": total_gross_profit,
|
||||||
"gross_profit_%": flt(
|
"gross_profit_%": flt(
|
||||||
(total_gross_profit / total_base_amount) * 100.0,
|
(total_gross_profit / abs(total_base_amount)) * 100.0,
|
||||||
cint(frappe.db.get_default("currency_precision")) or 3,
|
cint(frappe.db.get_default("currency_precision")) or 3,
|
||||||
)
|
)
|
||||||
if total_base_amount
|
if total_base_amount
|
||||||
@@ -248,9 +255,13 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
|
|||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
total_gross_profit = total_base_amount - total_buying_amount
|
total_gross_profit = flt(
|
||||||
|
total_base_amount + abs(total_buying_amount)
|
||||||
|
if total_buying_amount < 0
|
||||||
|
else total_base_amount - total_buying_amount,
|
||||||
|
)
|
||||||
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
currency_precision = cint(frappe.db.get_default("currency_precision")) or 3
|
||||||
gross_profit_percent = (total_gross_profit / total_base_amount * 100.0) if total_base_amount else 0
|
gross_profit_percent = (total_gross_profit / abs(total_base_amount) * 100.0) if total_base_amount else 0
|
||||||
|
|
||||||
total_row = {
|
total_row = {
|
||||||
group_columns[0]: "Total",
|
group_columns[0]: "Total",
|
||||||
@@ -581,10 +592,15 @@ class GrossProfitGenerator:
|
|||||||
base_amount += row.base_amount
|
base_amount += row.base_amount
|
||||||
|
|
||||||
# calculate gross profit
|
# calculate gross profit
|
||||||
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
|
row.gross_profit = flt(
|
||||||
|
row.base_amount + abs(row.buying_amount)
|
||||||
|
if row.buying_amount < 0
|
||||||
|
else row.base_amount - row.buying_amount,
|
||||||
|
self.currency_precision,
|
||||||
|
)
|
||||||
if row.base_amount:
|
if row.base_amount:
|
||||||
row.gross_profit_percent = flt(
|
row.gross_profit_percent = flt(
|
||||||
(row.gross_profit / row.base_amount) * 100.0,
|
(row.gross_profit / abs(row.base_amount)) * 100.0,
|
||||||
self.currency_precision,
|
self.currency_precision,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -673,9 +689,14 @@ class GrossProfitGenerator:
|
|||||||
return new_row
|
return new_row
|
||||||
|
|
||||||
def set_average_gross_profit(self, new_row):
|
def set_average_gross_profit(self, new_row):
|
||||||
new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision)
|
new_row.gross_profit = flt(
|
||||||
|
new_row.base_amount + abs(new_row.buying_amount)
|
||||||
|
if new_row.buying_amount < 0
|
||||||
|
else new_row.base_amount - new_row.buying_amount,
|
||||||
|
self.currency_precision,
|
||||||
|
)
|
||||||
new_row.gross_profit_percent = (
|
new_row.gross_profit_percent = (
|
||||||
flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision)
|
flt(((new_row.gross_profit / abs(new_row.base_amount)) * 100.0), self.currency_precision)
|
||||||
if new_row.base_amount
|
if new_row.base_amount
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
@@ -851,129 +872,173 @@ class GrossProfitGenerator:
|
|||||||
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0
|
||||||
|
|
||||||
def load_invoice_items(self):
|
def load_invoice_items(self):
|
||||||
conditions = ""
|
self.si_list = []
|
||||||
if self.filters.company:
|
|
||||||
conditions += " and `tabSales Invoice`.company = %(company)s"
|
SalesInvoice = frappe.qb.DocType("Sales Invoice")
|
||||||
if self.filters.from_date:
|
base_query = self.prepare_invoice_query()
|
||||||
conditions += " and posting_date >= %(from_date)s"
|
|
||||||
if self.filters.to_date:
|
|
||||||
conditions += " and posting_date <= %(to_date)s"
|
|
||||||
|
|
||||||
if self.filters.include_returned_invoices:
|
if self.filters.include_returned_invoices:
|
||||||
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
|
invoice_query = base_query.where(
|
||||||
|
(SalesInvoice.is_return == 0)
|
||||||
|
| ((SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnull())
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
conditions += " and is_return = 0"
|
invoice_query = base_query.where(SalesInvoice.is_return == 0)
|
||||||
|
|
||||||
if self.filters.item_group:
|
self.si_list += invoice_query.run(as_dict=True)
|
||||||
conditions += f" and {get_item_group_condition(self.filters.item_group)}"
|
self.prepare_vouchers_to_ignore()
|
||||||
|
|
||||||
if self.filters.sales_person:
|
ret_invoice_query = base_query.where(
|
||||||
conditions += """
|
(SalesInvoice.is_return == 1) & SalesInvoice.return_against.isnotnull()
|
||||||
and exists(select 1
|
)
|
||||||
from `tabSales Team` st
|
if self.vouchers_to_ignore:
|
||||||
where st.parent = `tabSales Invoice`.name
|
ret_invoice_query = ret_invoice_query.where(
|
||||||
and st.sales_person = %(sales_person)s)
|
SalesInvoice.return_against.notin(self.vouchers_to_ignore)
|
||||||
"""
|
)
|
||||||
|
|
||||||
|
self.si_list += ret_invoice_query.run(as_dict=True)
|
||||||
|
|
||||||
|
def prepare_invoice_query(self):
|
||||||
|
SalesInvoice = frappe.qb.DocType("Sales Invoice")
|
||||||
|
SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item")
|
||||||
|
Item = frappe.qb.DocType("Item")
|
||||||
|
SalesTeam = frappe.qb.DocType("Sales Team")
|
||||||
|
PaymentSchedule = frappe.qb.DocType("Payment Schedule")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(SalesInvoice)
|
||||||
|
.join(SalesInvoiceItem)
|
||||||
|
.on(SalesInvoiceItem.parent == SalesInvoice.name)
|
||||||
|
.join(Item)
|
||||||
|
.on(Item.name == SalesInvoiceItem.item_code)
|
||||||
|
.where((SalesInvoice.docstatus == 1) & (SalesInvoice.is_opening != "Yes"))
|
||||||
|
)
|
||||||
|
|
||||||
|
query = self.apply_common_filters(query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item)
|
||||||
|
|
||||||
|
query = query.select(
|
||||||
|
SalesInvoiceItem.parenttype,
|
||||||
|
SalesInvoiceItem.parent,
|
||||||
|
SalesInvoice.posting_date,
|
||||||
|
SalesInvoice.posting_time,
|
||||||
|
SalesInvoice.project,
|
||||||
|
SalesInvoice.update_stock,
|
||||||
|
SalesInvoice.customer,
|
||||||
|
SalesInvoice.customer_group,
|
||||||
|
SalesInvoice.customer_name,
|
||||||
|
SalesInvoice.territory,
|
||||||
|
SalesInvoiceItem.item_code,
|
||||||
|
SalesInvoice.base_net_total.as_("invoice_base_net_total"),
|
||||||
|
SalesInvoiceItem.item_name,
|
||||||
|
SalesInvoiceItem.description,
|
||||||
|
SalesInvoiceItem.warehouse,
|
||||||
|
SalesInvoiceItem.item_group,
|
||||||
|
SalesInvoiceItem.brand,
|
||||||
|
SalesInvoiceItem.so_detail,
|
||||||
|
SalesInvoiceItem.sales_order,
|
||||||
|
SalesInvoiceItem.dn_detail,
|
||||||
|
SalesInvoiceItem.delivery_note,
|
||||||
|
SalesInvoiceItem.stock_qty.as_("qty"),
|
||||||
|
SalesInvoiceItem.base_net_rate,
|
||||||
|
SalesInvoiceItem.base_net_amount,
|
||||||
|
SalesInvoiceItem.name.as_("item_row"),
|
||||||
|
SalesInvoice.is_return,
|
||||||
|
SalesInvoiceItem.cost_center,
|
||||||
|
SalesInvoiceItem.serial_and_batch_bundle,
|
||||||
|
)
|
||||||
|
|
||||||
if self.filters.group_by == "Sales Person":
|
if self.filters.group_by == "Sales Person":
|
||||||
sales_person_cols = """, sales.sales_person,
|
query = query.select(
|
||||||
sales.allocated_percentage * `tabSales Invoice Item`.base_net_amount / 100 as allocated_amount,
|
SalesTeam.sales_person,
|
||||||
sales.incentives
|
(SalesTeam.allocated_percentage * SalesInvoiceItem.base_net_amount / 100).as_(
|
||||||
"""
|
"allocated_amount"
|
||||||
sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name"
|
),
|
||||||
else:
|
SalesTeam.incentives,
|
||||||
sales_person_cols = ""
|
)
|
||||||
sales_team_table = ""
|
|
||||||
|
query = query.left_join(SalesTeam).on(SalesTeam.parent == SalesInvoice.name)
|
||||||
|
|
||||||
if self.filters.group_by == "Payment Term":
|
if self.filters.group_by == "Payment Term":
|
||||||
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
|
query = query.select(
|
||||||
'{}',
|
Case()
|
||||||
coalesce(schedule.payment_term, '{}')) as payment_term,
|
.when(SalesInvoice.is_return == 1, _("Sales Return"))
|
||||||
schedule.invoice_portion,
|
.else_(Coalesce(PaymentSchedule.payment_term, _("No Terms")))
|
||||||
schedule.payment_amount """.format(_("Sales Return"), _("No Terms"))
|
.as_("payment_term"),
|
||||||
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
|
PaymentSchedule.invoice_portion,
|
||||||
`tabSales Invoice`.is_return = 0 """
|
PaymentSchedule.payment_amount,
|
||||||
else:
|
)
|
||||||
payment_term_cols = ""
|
|
||||||
payment_term_table = ""
|
|
||||||
|
|
||||||
if self.filters.get("sales_invoice"):
|
query = query.left_join(PaymentSchedule).on(
|
||||||
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
(PaymentSchedule.parent == SalesInvoice.name) & (SalesInvoice.is_return == 0)
|
||||||
|
)
|
||||||
|
|
||||||
if self.filters.get("item_code"):
|
query = query.orderby(SalesInvoice.posting_date, order=Order.desc).orderby(
|
||||||
conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s"
|
SalesInvoice.posting_time, order=Order.desc
|
||||||
|
)
|
||||||
|
|
||||||
if self.filters.get("cost_center"):
|
return query
|
||||||
|
|
||||||
|
def apply_common_filters(self, query, SalesInvoice, SalesInvoiceItem, SalesTeam, Item):
|
||||||
|
if self.filters.company:
|
||||||
|
query = query.where(SalesInvoice.company == self.filters.company)
|
||||||
|
|
||||||
|
if self.filters.from_date:
|
||||||
|
query = query.where(SalesInvoice.posting_date >= self.filters.from_date)
|
||||||
|
|
||||||
|
if self.filters.to_date:
|
||||||
|
query = query.where(SalesInvoice.posting_date <= self.filters.to_date)
|
||||||
|
|
||||||
|
if self.filters.item_group:
|
||||||
|
query = query.where(get_item_group_condition(self.filters.item_group, Item))
|
||||||
|
|
||||||
|
if self.filters.sales_person:
|
||||||
|
query = query.where(
|
||||||
|
ExistsCriterion(
|
||||||
|
frappe.qb.from_(SalesTeam)
|
||||||
|
.select(1)
|
||||||
|
.where(
|
||||||
|
(SalesTeam.parent == SalesInvoice.name)
|
||||||
|
& (SalesTeam.sales_person == self.filters.sales_person)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.filters.sales_invoice:
|
||||||
|
query = query.where(SalesInvoice.name == self.filters.sales_invoice)
|
||||||
|
|
||||||
|
if self.filters.item_code:
|
||||||
|
query = query.where(SalesInvoiceItem.item_code == self.filters.item_code)
|
||||||
|
|
||||||
|
if self.filters.cost_center:
|
||||||
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
|
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
|
||||||
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
|
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
|
||||||
conditions += " and `tabSales Invoice Item`.cost_center in %(cost_center)s"
|
query = query.where(SalesInvoiceItem.cost_center.isin(self.filters.cost_center))
|
||||||
|
|
||||||
if self.filters.get("project"):
|
if self.filters.project:
|
||||||
self.filters.project = frappe.parse_json(self.filters.get("project"))
|
self.filters.project = frappe.parse_json(self.filters.get("project"))
|
||||||
conditions += " and `tabSales Invoice Item`.project in %(project)s"
|
query = query.where(SalesInvoiceItem.project.isin(self.filters.project))
|
||||||
|
|
||||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
for dim in get_accounting_dimensions(as_list=False) or []:
|
||||||
if accounting_dimensions:
|
if self.filters.get(dim.fieldname):
|
||||||
for dimension in accounting_dimensions:
|
if frappe.get_cached_value("DocType", dim.document_type, "is_tree"):
|
||||||
if self.filters.get(dimension.fieldname):
|
self.filters[dim.fieldname] = get_dimension_with_children(
|
||||||
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
|
dim.document_type, self.filters.get(dim.fieldname)
|
||||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
)
|
||||||
dimension.document_type, self.filters.get(dimension.fieldname)
|
query = query.where(SalesInvoiceItem[dim.fieldname].isin(self.filters[dim.fieldname]))
|
||||||
)
|
|
||||||
conditions += (
|
|
||||||
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
conditions += (
|
|
||||||
f" and `tabSales Invoice Item`.{dimension.fieldname} in %({dimension.fieldname})s"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.filters.get("warehouse"):
|
if self.filters.warehouse:
|
||||||
warehouse_details = frappe.db.get_value(
|
lft, rgt = frappe.db.get_value("Warehouse", self.filters.warehouse, ["lft", "rgt"])
|
||||||
"Warehouse", self.filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
WH = frappe.qb.DocType("Warehouse")
|
||||||
|
query = query.where(
|
||||||
|
SalesInvoiceItem.warehouse.isin(
|
||||||
|
frappe.qb.from_(WH).select(WH.name).where((WH.lft >= lft) & (WH.rgt <= rgt))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if warehouse_details:
|
|
||||||
conditions += f" and `tabSales Invoice Item`.warehouse in (select name from `tabWarehouse` wh where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)"
|
|
||||||
|
|
||||||
self.si_list = frappe.db.sql(
|
return query
|
||||||
"""
|
|
||||||
select
|
def prepare_vouchers_to_ignore(self):
|
||||||
`tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent,
|
self.vouchers_to_ignore = tuple(row["parent"] for row in self.si_list)
|
||||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time,
|
|
||||||
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
|
|
||||||
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.customer_name,
|
|
||||||
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
|
|
||||||
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
|
|
||||||
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
|
|
||||||
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
|
|
||||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
|
|
||||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.dn_detail,
|
|
||||||
`tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.stock_qty as qty,
|
|
||||||
`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
|
|
||||||
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
|
||||||
`tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.serial_and_batch_bundle
|
|
||||||
{sales_person_cols}
|
|
||||||
{payment_term_cols}
|
|
||||||
from
|
|
||||||
`tabSales Invoice` inner join `tabSales Invoice Item`
|
|
||||||
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
|
||||||
join `tabItem` item on item.name = `tabSales Invoice Item`.item_code
|
|
||||||
{sales_team_table}
|
|
||||||
{payment_term_table}
|
|
||||||
where
|
|
||||||
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
|
||||||
order by
|
|
||||||
`tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format(
|
|
||||||
conditions=conditions,
|
|
||||||
sales_person_cols=sales_person_cols,
|
|
||||||
sales_team_table=sales_team_table,
|
|
||||||
payment_term_cols=payment_term_cols,
|
|
||||||
payment_term_table=payment_term_table,
|
|
||||||
match_cond=get_match_cond("Sales Invoice"),
|
|
||||||
),
|
|
||||||
self.filters,
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_delivery_notes(self):
|
def get_delivery_notes(self):
|
||||||
self.delivery_notes = frappe._dict({})
|
self.delivery_notes = frappe._dict({})
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ class TestGrossProfit(IntegrationTestCase):
|
|||||||
"selling_amount": -100.0,
|
"selling_amount": -100.0,
|
||||||
"buying_amount": 0.0,
|
"buying_amount": 0.0,
|
||||||
"gross_profit": -100.0,
|
"gross_profit": -100.0,
|
||||||
"gross_profit_%": 100.0,
|
"gross_profit_%": -100.0,
|
||||||
}
|
}
|
||||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||||
report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry}
|
report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry}
|
||||||
@@ -649,21 +649,24 @@ class TestGrossProfit(IntegrationTestCase):
|
|||||||
def test_profit_for_later_period_return(self):
|
def test_profit_for_later_period_return(self):
|
||||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||||
|
|
||||||
|
sales_inv_date = month_start_date
|
||||||
|
return_inv_date = add_days(month_end_date, 1)
|
||||||
|
|
||||||
# create sales invoice on month start date
|
# create sales invoice on month start date
|
||||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||||
sinv.set_posting_time = 1
|
sinv.set_posting_time = 1
|
||||||
sinv.posting_date = month_start_date
|
sinv.posting_date = sales_inv_date
|
||||||
sinv.save().submit()
|
sinv.save().submit()
|
||||||
|
|
||||||
# create credit note on next month start date
|
# create credit note on next month start date
|
||||||
cr_note = make_sales_return(sinv.name)
|
cr_note = make_sales_return(sinv.name)
|
||||||
cr_note.set_posting_time = 1
|
cr_note.set_posting_time = 1
|
||||||
cr_note.posting_date = add_days(month_end_date, 1)
|
cr_note.posting_date = return_inv_date
|
||||||
cr_note.save().submit()
|
cr_note.save().submit()
|
||||||
|
|
||||||
# apply filters for invoiced period
|
# apply filters for invoiced period
|
||||||
filters = frappe._dict(
|
filters = frappe._dict(
|
||||||
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
|
company=self.company, from_date=month_start_date, to_date=month_start_date, group_by="Invoice"
|
||||||
)
|
)
|
||||||
|
|
||||||
_, data = execute(filters=filters)
|
_, data = execute(filters=filters)
|
||||||
@@ -675,7 +678,7 @@ class TestGrossProfit(IntegrationTestCase):
|
|||||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||||
|
|
||||||
# extend filters upto returned period
|
# extend filters upto returned period
|
||||||
filters.update(to_date=add_days(month_end_date, 1))
|
filters.update({"to_date": return_inv_date})
|
||||||
|
|
||||||
_, data = execute(filters=filters)
|
_, data = execute(filters=filters)
|
||||||
total = data[-1]
|
total = data[-1]
|
||||||
@@ -684,3 +687,63 @@ class TestGrossProfit(IntegrationTestCase):
|
|||||||
self.assertEqual(total.buying_amount, 0.0)
|
self.assertEqual(total.buying_amount, 0.0)
|
||||||
self.assertEqual(total.gross_profit, 0.0)
|
self.assertEqual(total.gross_profit, 0.0)
|
||||||
self.assertEqual(total.get("gross_profit_%"), 0.0)
|
self.assertEqual(total.get("gross_profit_%"), 0.0)
|
||||||
|
|
||||||
|
# apply filters only on returned period
|
||||||
|
filters.update({"from_date": return_inv_date, "to_date": return_inv_date})
|
||||||
|
_, data = execute(filters=filters)
|
||||||
|
total = data[-1]
|
||||||
|
|
||||||
|
self.assertEqual(total.selling_amount, -100.0)
|
||||||
|
self.assertEqual(total.buying_amount, 0.0)
|
||||||
|
self.assertEqual(total.gross_profit, -100.0)
|
||||||
|
self.assertEqual(total.get("gross_profit_%"), -100.0)
|
||||||
|
|
||||||
|
def test_sales_person_wise_gross_profit(self):
|
||||||
|
sales_person = make_sales_person("_Test Sales Person")
|
||||||
|
|
||||||
|
posting_date = get_first_day(nowdate())
|
||||||
|
qty = 10
|
||||||
|
rate = 100
|
||||||
|
|
||||||
|
sinv = self.create_sales_invoice(qty=qty, rate=rate, do_not_save=True, do_not_submit=True)
|
||||||
|
sinv.set_posting_time = 1
|
||||||
|
sinv.posting_date = posting_date
|
||||||
|
sinv.append(
|
||||||
|
"sales_team",
|
||||||
|
{
|
||||||
|
"sales_person": sales_person.name,
|
||||||
|
"allocated_percentage": 100,
|
||||||
|
"allocated_amount": 1000.0,
|
||||||
|
"commission_rate": 5,
|
||||||
|
"incentives": 5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
sinv.save().submit()
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
company=self.company, from_date=posting_date, to_date=posting_date, group_by="Sales Person"
|
||||||
|
)
|
||||||
|
|
||||||
|
_, data = execute(filters=filters)
|
||||||
|
total = data[-1]
|
||||||
|
|
||||||
|
self.assertEqual(total[5], 1000.0)
|
||||||
|
self.assertEqual(total[6], 0.0)
|
||||||
|
self.assertEqual(total[7], 1000.0)
|
||||||
|
self.assertEqual(total[8], 100.0)
|
||||||
|
|
||||||
|
|
||||||
|
def make_sales_person(sales_person_name="_Test Sales Person"):
|
||||||
|
if not frappe.db.exists("Sales Person", {"sales_person_name": sales_person_name}):
|
||||||
|
sales_person_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Sales Person",
|
||||||
|
"is_group": 0,
|
||||||
|
"parent_sales_person": "Sales Team",
|
||||||
|
"sales_person_name": sales_person_name,
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
else:
|
||||||
|
sales_person_doc = frappe.get_doc("Sales Person", {"sales_person_name": sales_person_name})
|
||||||
|
|
||||||
|
return sales_person_doc
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
from pypika.terms import Bracket, LiteralValue
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||||
@@ -361,15 +362,12 @@ def get_items(filters, additional_table_columns):
|
|||||||
|
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
query, params = query.walk()
|
if match_conditions := build_match_conditions(doctype):
|
||||||
match_conditions = build_match_conditions(doctype)
|
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||||
|
|
||||||
if match_conditions:
|
|
||||||
query += " and " + match_conditions
|
|
||||||
|
|
||||||
query = apply_order_by_conditions(doctype, query, filters)
|
query = apply_order_by_conditions(doctype, query, filters)
|
||||||
|
|
||||||
return frappe.db.sql(query, params, as_dict=True)
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_aii_accounts():
|
def get_aii_accounts():
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from frappe.query_builder import functions as fn
|
|||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
from frappe.utils.xlsxutils import handle_html
|
from frappe.utils.xlsxutils import handle_html
|
||||||
|
from pypika.terms import Bracket, LiteralValue, Order
|
||||||
|
|
||||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||||
from erpnext.accounts.report.utils import get_values_for_columns
|
from erpnext.accounts.report.utils import get_values_for_columns
|
||||||
@@ -390,20 +391,21 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
|
|||||||
|
|
||||||
|
|
||||||
def apply_order_by_conditions(doctype, query, filters):
|
def apply_order_by_conditions(doctype, query, filters):
|
||||||
invoice = f"`tab{doctype}`"
|
invoice = frappe.qb.DocType(doctype)
|
||||||
invoice_item = f"`tab{doctype} Item`"
|
invoice_item = frappe.qb.DocType(f"{doctype} Item")
|
||||||
|
|
||||||
if not filters.get("group_by"):
|
if not filters.get("group_by"):
|
||||||
query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc"
|
query = query.orderby(invoice.posting_date, order=Order.desc)
|
||||||
|
query = query.orderby(invoice_item.item_group, order=Order.desc)
|
||||||
elif filters.get("group_by") == "Invoice":
|
elif filters.get("group_by") == "Invoice":
|
||||||
query += f" order by {invoice_item}.parent desc"
|
query = query.orderby(invoice_item.parent, order=Order.desc)
|
||||||
elif filters.get("group_by") == "Item":
|
elif filters.get("group_by") == "Item":
|
||||||
query += f" order by {invoice_item}.item_code"
|
query = query.orderby(invoice_item.item_code)
|
||||||
elif filters.get("group_by") == "Item Group":
|
elif filters.get("group_by") == "Item Group":
|
||||||
query += f" order by {invoice_item}.item_group"
|
query = query.orderby(invoice_item.item_group)
|
||||||
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
|
||||||
filter_field = frappe.scrub(filters.get("group_by"))
|
filter_field = frappe.scrub(filters.get("group_by"))
|
||||||
query += f" order by {filter_field} desc"
|
query = query.orderby(filter_field, order=Order.desc)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@@ -481,15 +483,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
|||||||
|
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
query, params = query.walk()
|
if match_conditions := build_match_conditions(doctype):
|
||||||
match_conditions = build_match_conditions(doctype)
|
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||||
|
|
||||||
if match_conditions:
|
|
||||||
query += " and " + match_conditions
|
|
||||||
|
|
||||||
query = apply_order_by_conditions(doctype, query, filters)
|
query = apply_order_by_conditions(doctype, query, filters)
|
||||||
|
|
||||||
return frappe.db.sql(query, params, as_dict=True)
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def get_delivery_notes_against_sales_order(item_list):
|
def get_delivery_notes_against_sales_order(item_list):
|
||||||
|
|||||||
@@ -163,11 +163,11 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
|||||||
|
|
||||||
|
|
||||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||||
labels = [d.get("label") for d in columns[2:]]
|
labels = [d.get("label") for d in columns[4:]]
|
||||||
|
|
||||||
income_data, expense_data, net_profit = [], [], []
|
income_data, expense_data, net_profit = [], [], []
|
||||||
|
|
||||||
for p in columns[2:]:
|
for p in columns[4:]:
|
||||||
if income:
|
if income:
|
||||||
income_data.append(income[-2].get(p.get("fieldname")))
|
income_data.append(income[-2].get(p.get("fieldname")))
|
||||||
if expense:
|
if expense:
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, qb
|
||||||
|
from frappe.query_builder import Criterion
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||||
@@ -33,11 +34,19 @@ def execute(filters=None):
|
|||||||
|
|
||||||
def get_accounts_data(based_on, company):
|
def get_accounts_data(based_on, company):
|
||||||
if based_on == "Cost Center":
|
if based_on == "Cost Center":
|
||||||
return frappe.db.sql(
|
cc = qb.DocType("Cost Center")
|
||||||
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
|
return (
|
||||||
from `tabCost Center` where company=%s order by name""",
|
qb.from_(cc)
|
||||||
company,
|
.select(
|
||||||
as_dict=True,
|
cc.name,
|
||||||
|
cc.parent_cost_center.as_("parent_account"),
|
||||||
|
cc.cost_center_name.as_("account_name"),
|
||||||
|
cc.lft,
|
||||||
|
cc.rgt,
|
||||||
|
)
|
||||||
|
.where(cc.company.eq(company))
|
||||||
|
.orderby(cc.name)
|
||||||
|
.run(as_dict=True)
|
||||||
)
|
)
|
||||||
elif based_on == "Project":
|
elif based_on == "Project":
|
||||||
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
|
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
|
||||||
@@ -206,27 +215,38 @@ def set_gl_entries_by_account(
|
|||||||
company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False
|
company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False
|
||||||
):
|
):
|
||||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||||
additional_conditions = []
|
gl = qb.DocType("GL Entry")
|
||||||
|
acc = qb.DocType("Account")
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
conditions.append(gl.company.eq(company))
|
||||||
|
conditions.append(gl[based_on].notnull())
|
||||||
|
conditions.append(gl.is_cancelled.eq(0))
|
||||||
|
|
||||||
|
if from_date and to_date:
|
||||||
|
conditions.append(gl.posting_date.between(from_date, to_date))
|
||||||
|
elif from_date and not to_date:
|
||||||
|
conditions.append(gl.posting_date.gte(from_date))
|
||||||
|
elif not from_date and to_date:
|
||||||
|
conditions.append(gl.posting_date.lte(to_date))
|
||||||
|
|
||||||
if ignore_closing_entries:
|
if ignore_closing_entries:
|
||||||
additional_conditions.append("and voucher_type !='Period Closing Voucher'")
|
conditions.append(gl.voucher_type.ne("Period Closing Voucher"))
|
||||||
|
|
||||||
if from_date:
|
root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account))
|
||||||
additional_conditions.append("and posting_date >= %(from_date)s")
|
gl_entries = (
|
||||||
|
qb.from_(gl)
|
||||||
gl_entries = frappe.db.sql(
|
.select(
|
||||||
"""select posting_date, {based_on} as based_on, debit, credit,
|
gl.posting_date,
|
||||||
is_opening, (select root_type from `tabAccount` where name = account) as type
|
gl[based_on].as_("based_on"),
|
||||||
from `tabGL Entry` where company=%(company)s
|
gl.debit,
|
||||||
{additional_conditions}
|
gl.credit,
|
||||||
and posting_date <= %(to_date)s
|
gl.is_opening,
|
||||||
and {based_on} is not null
|
root_subquery.as_("type"),
|
||||||
and is_cancelled = 0
|
)
|
||||||
order by {based_on}, posting_date""".format(
|
.where(Criterion.all(conditions))
|
||||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
.orderby(gl[based_on], gl.posting_date)
|
||||||
),
|
.run(as_dict=True)
|
||||||
{"company": company, "from_date": from_date, "to_date": to_date},
|
|
||||||
as_dict=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for entry in gl_entries:
|
for entry in gl_entries:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import frappe
|
|||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import flt, getdate
|
from frappe.utils import flt, getdate
|
||||||
|
from pypika.terms import Bracket, LiteralValue, Order
|
||||||
|
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
from erpnext.accounts.report.utils import (
|
from erpnext.accounts.report.utils import (
|
||||||
@@ -421,15 +422,13 @@ def get_invoices(filters, additional_query_columns):
|
|||||||
|
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
query, params = query.walk()
|
if match_conditions := build_match_conditions("Purchase Invoice"):
|
||||||
match_conditions = build_match_conditions("Purchase Invoice")
|
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||||
|
|
||||||
if match_conditions:
|
query = query.orderby("posting_date", order=Order.desc)
|
||||||
query += " and " + match_conditions
|
query = query.orderby("name", order=Order.desc)
|
||||||
|
|
||||||
query += " order by posting_date desc, name desc"
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
return frappe.db.sql(query, params, as_dict=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filters, query, doctype):
|
def get_conditions(filters, query, doctype):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from frappe import _, msgprint
|
|||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import flt, getdate
|
from frappe.utils import flt, getdate
|
||||||
|
from pypika.terms import Bracket, LiteralValue, Order
|
||||||
|
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
from erpnext.accounts.report.utils import (
|
from erpnext.accounts.report.utils import (
|
||||||
@@ -457,15 +458,13 @@ def get_invoices(filters, additional_query_columns):
|
|||||||
|
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
|
|
||||||
query, params = query.walk()
|
if match_conditions := build_match_conditions("Sales Invoice"):
|
||||||
match_conditions = build_match_conditions("Sales Invoice")
|
query = query.where(Bracket(LiteralValue(match_conditions)))
|
||||||
|
|
||||||
if match_conditions:
|
query = query.orderby("posting_date", order=Order.desc)
|
||||||
query += " and " + match_conditions
|
query = query.orderby("name", order=Order.desc)
|
||||||
|
|
||||||
query += " order by posting_date desc, name desc"
|
return query.run(as_dict=True)
|
||||||
|
|
||||||
return frappe.db.sql(query, params, as_dict=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_conditions(filters, query, doctype):
|
def get_conditions(filters, query, doctype):
|
||||||
|
|||||||
@@ -154,17 +154,11 @@ def get_columns(filters):
|
|||||||
"width": 60,
|
"width": 60,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Total Amount"),
|
"label": _("Taxable Amount"),
|
||||||
"fieldname": "total_amount",
|
"fieldname": "total_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"width": 120,
|
"width": 120,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": _("Base Total"),
|
|
||||||
"fieldname": "base_total",
|
|
||||||
"fieldtype": "Currency",
|
|
||||||
"width": 120,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": _("Tax Amount"),
|
"label": _("Tax Amount"),
|
||||||
"fieldname": "tax_amount",
|
"fieldname": "tax_amount",
|
||||||
@@ -172,10 +166,16 @@ def get_columns(filters):
|
|||||||
"width": 120,
|
"width": 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Grand Total"),
|
"label": _("Grand Total (Company Currency)"),
|
||||||
|
"fieldname": "base_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"width": 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Grand Total (Transaction Currency)"),
|
||||||
"fieldname": "grand_total",
|
"fieldname": "grand_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"width": 120,
|
"width": 170,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Reference Date"),
|
"label": _("Reference Date"),
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ def get_columns(filters):
|
|||||||
"width": 120,
|
"width": 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Total Amount"),
|
"label": _("Total Taxable Amount"),
|
||||||
"fieldname": "total_amount",
|
"fieldname": "total_amount",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"width": 120,
|
"width": 120,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import frappe.defaults
|
|||||||
from frappe import _, qb, throw
|
from frappe import _, qb, throw
|
||||||
from frappe.desk.reportview import build_match_conditions
|
from frappe.desk.reportview import build_match_conditions
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
|
from frappe.model.naming import determine_consecutive_week_number
|
||||||
from frappe.query_builder import AliasedQuery, Case, Criterion, Field, Table
|
from frappe.query_builder import AliasedQuery, Case, Criterion, Field, Table
|
||||||
from frappe.query_builder.functions import Count, IfNull, Max, Round, Sum
|
from frappe.query_builder.functions import Count, IfNull, Max, Round, Sum
|
||||||
from frappe.query_builder.utils import DocType
|
from frappe.query_builder.utils import DocType
|
||||||
@@ -25,6 +26,7 @@ from frappe.utils import (
|
|||||||
get_number_format_info,
|
get_number_format_info,
|
||||||
getdate,
|
getdate,
|
||||||
now,
|
now,
|
||||||
|
now_datetime,
|
||||||
nowdate,
|
nowdate,
|
||||||
)
|
)
|
||||||
from frappe.utils.caching import site_cache
|
from frappe.utils.caching import site_cache
|
||||||
@@ -66,6 +68,7 @@ def get_fiscal_year(
|
|||||||
as_dict=False,
|
as_dict=False,
|
||||||
boolean=None,
|
boolean=None,
|
||||||
raise_on_missing=True,
|
raise_on_missing=True,
|
||||||
|
truncate=False,
|
||||||
):
|
):
|
||||||
if isinstance(raise_on_missing, str):
|
if isinstance(raise_on_missing, str):
|
||||||
raise_on_missing = loads(raise_on_missing)
|
raise_on_missing = loads(raise_on_missing)
|
||||||
@@ -79,7 +82,14 @@ def get_fiscal_year(
|
|||||||
fiscal_years = get_fiscal_years(
|
fiscal_years = get_fiscal_years(
|
||||||
date, fiscal_year, label, verbose, company, as_dict=as_dict, raise_on_missing=raise_on_missing
|
date, fiscal_year, label, verbose, company, as_dict=as_dict, raise_on_missing=raise_on_missing
|
||||||
)
|
)
|
||||||
return False if not fiscal_years else fiscal_years[0]
|
|
||||||
|
if fiscal_years:
|
||||||
|
fiscal_year = fiscal_years[0]
|
||||||
|
if truncate:
|
||||||
|
return ("-".join(y[-2:] for y in fiscal_year[0].split("-")), fiscal_year[1], fiscal_year[2])
|
||||||
|
return fiscal_year
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_fiscal_years(
|
def get_fiscal_years(
|
||||||
@@ -547,6 +557,7 @@ def reconcile_against_document(
|
|||||||
doc.make_advance_gl_entries(entry=row)
|
doc.make_advance_gl_entries(entry=row)
|
||||||
else:
|
else:
|
||||||
_delete_pl_entries(voucher_type, voucher_no)
|
_delete_pl_entries(voucher_type, voucher_no)
|
||||||
|
_delete_adv_pl_entries(voucher_type, voucher_no)
|
||||||
gl_map = doc.build_gl_map()
|
gl_map = doc.build_gl_map()
|
||||||
# Make sure there is no overallocation
|
# Make sure there is no overallocation
|
||||||
from erpnext.accounts.general_ledger import process_debit_credit_difference
|
from erpnext.accounts.general_ledger import process_debit_credit_difference
|
||||||
@@ -662,6 +673,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
|||||||
d["allocated_amount"] = d["allocated_amount"] * -1
|
d["allocated_amount"] = d["allocated_amount"] * -1
|
||||||
d["unadjusted_amount"] = d["unadjusted_amount"] * -1
|
d["unadjusted_amount"] = d["unadjusted_amount"] * -1
|
||||||
|
|
||||||
|
insert_position = -1
|
||||||
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
||||||
# adjust the unreconciled balance
|
# adjust the unreconciled balance
|
||||||
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
|
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
|
||||||
@@ -673,9 +685,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
journal_entry.remove(jv_detail)
|
journal_entry.remove(jv_detail)
|
||||||
|
insert_position += jv_detail.idx
|
||||||
|
|
||||||
# new row with references
|
# new row with references
|
||||||
new_row = journal_entry.append("accounts")
|
new_row = journal_entry.append("accounts", position=insert_position)
|
||||||
|
|
||||||
# Copy field values into new row
|
# Copy field values into new row
|
||||||
[
|
[
|
||||||
@@ -1500,14 +1513,14 @@ def get_autoname_with_number(number_value, doc_title, company):
|
|||||||
|
|
||||||
|
|
||||||
def parse_naming_series_variable(doc, variable):
|
def parse_naming_series_variable(doc, variable):
|
||||||
if variable == "FY":
|
if variable in ["FY", "TFY"]:
|
||||||
if doc:
|
if doc:
|
||||||
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
|
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
|
||||||
company = doc.get("company")
|
company = doc.get("company")
|
||||||
else:
|
else:
|
||||||
date = getdate()
|
date = getdate()
|
||||||
company = None
|
company = None
|
||||||
return get_fiscal_year(date=date, company=company)[0]
|
return get_fiscal_year(date=date, company=company, truncate=variable == "TFY")[0]
|
||||||
|
|
||||||
elif variable == "ABBR":
|
elif variable == "ABBR":
|
||||||
if doc:
|
if doc:
|
||||||
@@ -1517,6 +1530,18 @@ def parse_naming_series_variable(doc, variable):
|
|||||||
|
|
||||||
return frappe.db.get_value("Company", company, "abbr") if company else ""
|
return frappe.db.get_value("Company", company, "abbr") if company else ""
|
||||||
|
|
||||||
|
else:
|
||||||
|
data = {"YY": "%y", "YYYY": "%Y", "MM": "%m", "DD": "%d", "JJJ": "%j"}
|
||||||
|
date = (
|
||||||
|
(
|
||||||
|
getdate(doc.get("posting_date") or doc.get("transaction_date") or doc.get("posting_datetime"))
|
||||||
|
or now_datetime()
|
||||||
|
)
|
||||||
|
if frappe.get_single_value("Global Defaults", "use_posting_datetime_for_naming_documents")
|
||||||
|
else now_datetime()
|
||||||
|
)
|
||||||
|
return date.strftime(data[variable]) if variable in data else determine_consecutive_week_number(date)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_coa(doctype, parent, is_root=None, chart=None):
|
def get_coa(doctype, parent, is_root=None, chart=None):
|
||||||
@@ -1946,6 +1971,7 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
|
|||||||
account=gle.account,
|
account=gle.account,
|
||||||
party_type=gle.party_type,
|
party_type=gle.party_type,
|
||||||
party=gle.party,
|
party=gle.party,
|
||||||
|
project=gle.project,
|
||||||
cost_center=gle.cost_center,
|
cost_center=gle.cost_center,
|
||||||
finance_book=gle.finance_book,
|
finance_book=gle.finance_book,
|
||||||
due_date=gle.due_date,
|
due_date=gle.due_date,
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
"for_user": "",
|
"for_user": "",
|
||||||
"hide_custom": 0,
|
"hide_custom": 0,
|
||||||
"icon": "accounting",
|
"icon": "accounting",
|
||||||
"idx": 3,
|
"idx": 4,
|
||||||
"indicator_color": "",
|
"indicator_color": "",
|
||||||
"is_hidden": 0,
|
"is_hidden": 0,
|
||||||
"label": "Accounting",
|
"label": "Invoicing",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -587,10 +587,10 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-12-24 13:20:34.857205",
|
"modified": "2026-01-23 11:05:47.246213",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounting",
|
"name": "Invoicing",
|
||||||
"number_cards": [
|
"number_cards": [
|
||||||
{
|
{
|
||||||
"label": "Outgoing Bills",
|
"label": "Outgoing Bills",
|
||||||
@@ -617,6 +617,6 @@
|
|||||||
"roles": [],
|
"roles": [],
|
||||||
"sequence_id": 2.0,
|
"sequence_id": 2.0,
|
||||||
"shortcuts": [],
|
"shortcuts": [],
|
||||||
"title": "Accounting",
|
"title": "Invoicing",
|
||||||
"type": "Workspace"
|
"type": "Workspace"
|
||||||
}
|
}
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Dashboard Chart",
|
"doctype": "Dashboard Chart",
|
||||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
|
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
|
||||||
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Asset Category\",\"is_existing_asset\":0}",
|
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Asset Category\",\"asset_type\":[\"!=\",\"Existing Asset\"]}",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"modified": "2020-10-28 23:16:16.939070",
|
"modified": "2026-02-03 15:48:13.407835",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Category-wise Asset Value",
|
"name": "Category-wise Asset Value",
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Dashboard Chart",
|
"doctype": "Dashboard Chart",
|
||||||
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
|
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_date\":\"frappe.datetime.add_months(frappe.datetime.nowdate(), -12)\",\"to_date\":\"frappe.datetime.nowdate()\"}",
|
||||||
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Location\",\"is_existing_asset\":0}",
|
"filters_json": "{\"status\":\"In Location\",\"group_by\":\"Location\",\"asset_type\":[\"!=\",\"Existing Asset\"]}",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"modified": "2020-10-28 23:16:07.883312",
|
"modified": "2026-02-03 15:48:13.407835",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Location-wise Asset Value",
|
"name": "Location-wise Asset Value",
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ def get_charts(fiscal_year, year_start_date, year_end_date):
|
|||||||
"company": company,
|
"company": company,
|
||||||
"status": "In Location",
|
"status": "In Location",
|
||||||
"group_by": "Asset Category",
|
"group_by": "Asset Category",
|
||||||
"is_existing_asset": 0,
|
"asset_type": ["!=", "Existing Asset"],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"type": "Donut",
|
"type": "Donut",
|
||||||
@@ -126,7 +126,12 @@ def get_charts(fiscal_year, year_start_date, year_end_date):
|
|||||||
"x_field": "location",
|
"x_field": "location",
|
||||||
"timeseries": 0,
|
"timeseries": 0,
|
||||||
"filters_json": json.dumps(
|
"filters_json": json.dumps(
|
||||||
{"company": company, "status": "In Location", "group_by": "Location", "is_existing_asset": 0}
|
{
|
||||||
|
"company": company,
|
||||||
|
"status": "In Location",
|
||||||
|
"group_by": "Location",
|
||||||
|
"asset_type": ["!=", "Existing Asset"],
|
||||||
|
}
|
||||||
),
|
),
|
||||||
"type": "Donut",
|
"type": "Donut",
|
||||||
"doctype": "Dashboard Chart",
|
"doctype": "Dashboard Chart",
|
||||||
|
|||||||
@@ -81,23 +81,79 @@ frappe.ui.form.on("Asset", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
before_submit: function (frm) {
|
before_submit: function (frm) {
|
||||||
if (frm.doc.is_composite_asset && !frm.has_active_capitalization) {
|
if (frm.doc.asset_type == "Composite Asset" && !frm.has_active_capitalization) {
|
||||||
frappe.throw(__("Please capitalize this asset before submitting."));
|
frappe.throw(__("Please capitalize this asset before submitting."));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: async function (frm) {
|
||||||
frappe.ui.form.trigger("Asset", "is_existing_asset");
|
frappe.ui.form.trigger("Asset", "asset_type");
|
||||||
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
|
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
|
||||||
|
|
||||||
|
let has_create_buttons = false;
|
||||||
if (frm.doc.docstatus == 1) {
|
if (frm.doc.docstatus == 1) {
|
||||||
|
if (["Submitted", "Partially Depreciated"].includes(frm.doc.status)) {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Asset Value Adjustment"),
|
||||||
|
function () {
|
||||||
|
frm.trigger("create_asset_value_adjustment");
|
||||||
|
},
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Asset Repair"),
|
||||||
|
function () {
|
||||||
|
frm.trigger("create_asset_repair");
|
||||||
|
},
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
has_create_buttons = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!frm.doc.calculate_depreciation &&
|
||||||
|
["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)
|
||||||
|
) {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Depreciation Entry"),
|
||||||
|
function () {
|
||||||
|
frm.trigger("make_journal_entry");
|
||||||
|
},
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
has_create_buttons = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_create_buttons) {
|
||||||
|
frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
|
}
|
||||||
|
|
||||||
if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) {
|
if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) {
|
||||||
|
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Maintain Asset"),
|
||||||
|
function () {
|
||||||
|
frm.trigger("create_asset_maintenance");
|
||||||
|
},
|
||||||
|
__("Actions")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Split Asset"),
|
||||||
|
function () {
|
||||||
|
frm.trigger("split_asset");
|
||||||
|
},
|
||||||
|
__("Actions")
|
||||||
|
);
|
||||||
|
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Transfer Asset"),
|
__("Transfer Asset"),
|
||||||
function () {
|
function () {
|
||||||
erpnext.asset.transfer_asset(frm);
|
erpnext.asset.transfer_asset(frm);
|
||||||
},
|
},
|
||||||
__("Manage")
|
__("Actions")
|
||||||
);
|
);
|
||||||
|
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
@@ -105,7 +161,7 @@ frappe.ui.form.on("Asset", {
|
|||||||
function () {
|
function () {
|
||||||
erpnext.asset.scrap_asset(frm);
|
erpnext.asset.scrap_asset(frm);
|
||||||
},
|
},
|
||||||
__("Manage")
|
__("Actions")
|
||||||
);
|
);
|
||||||
|
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
@@ -113,23 +169,7 @@ frappe.ui.form.on("Asset", {
|
|||||||
function () {
|
function () {
|
||||||
frm.trigger("sell_asset");
|
frm.trigger("sell_asset");
|
||||||
},
|
},
|
||||||
__("Manage")
|
__("Actions")
|
||||||
);
|
|
||||||
|
|
||||||
frm.add_custom_button(
|
|
||||||
__("Repair Asset"),
|
|
||||||
function () {
|
|
||||||
frm.trigger("create_asset_repair");
|
|
||||||
},
|
|
||||||
__("Manage")
|
|
||||||
);
|
|
||||||
|
|
||||||
frm.add_custom_button(
|
|
||||||
__("Split Asset"),
|
|
||||||
function () {
|
|
||||||
frm.trigger("split_asset");
|
|
||||||
},
|
|
||||||
__("Manage")
|
|
||||||
);
|
);
|
||||||
} else if (frm.doc.status == "Scrapped") {
|
} else if (frm.doc.status == "Scrapped") {
|
||||||
frm.add_custom_button(__("Restore Asset"), function () {
|
frm.add_custom_button(__("Restore Asset"), function () {
|
||||||
@@ -137,39 +177,9 @@ frappe.ui.form.on("Asset", {
|
|||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
|
if (await frm.events.should_show_accounting_ledger(frm)) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Maintain Asset"),
|
__("Accounting Ledger"),
|
||||||
function () {
|
|
||||||
frm.trigger("create_asset_maintenance");
|
|
||||||
},
|
|
||||||
__("Manage")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (["Submitted", "Partially Depreciated"].includes(frm.doc.status)) {
|
|
||||||
frm.add_custom_button(
|
|
||||||
__("Adjust Asset Value"),
|
|
||||||
function () {
|
|
||||||
frm.trigger("create_asset_value_adjustment");
|
|
||||||
},
|
|
||||||
__("Manage")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!frm.doc.calculate_depreciation) {
|
|
||||||
frm.add_custom_button(
|
|
||||||
__("Create Depreciation Entry"),
|
|
||||||
function () {
|
|
||||||
frm.trigger("make_journal_entry");
|
|
||||||
},
|
|
||||||
__("Manage")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
|
|
||||||
frm.add_custom_button(
|
|
||||||
__("View General Ledger"),
|
|
||||||
function () {
|
function () {
|
||||||
frappe.route_options = {
|
frappe.route_options = {
|
||||||
voucher_no: frm.doc.name,
|
voucher_no: frm.doc.name,
|
||||||
@@ -179,7 +189,7 @@ frappe.ui.form.on("Asset", {
|
|||||||
};
|
};
|
||||||
frappe.set_route("query-report", "General Ledger");
|
frappe.set_route("query-report", "General Ledger");
|
||||||
},
|
},
|
||||||
__("Manage")
|
__("View")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +205,7 @@ frappe.ui.form.on("Asset", {
|
|||||||
if (frm.doc.docstatus == 0) {
|
if (frm.doc.docstatus == 0) {
|
||||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||||
|
|
||||||
if (frm.doc.is_composite_asset) {
|
if (frm.doc.asset_type == "Composite Asset") {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
|
method: "erpnext.assets.doctype.asset.asset.has_active_capitalization",
|
||||||
args: {
|
args: {
|
||||||
@@ -217,6 +227,28 @@ frappe.ui.form.on("Asset", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
should_show_accounting_ledger: async function (frm) {
|
||||||
|
if (["Capitalized"].includes(frm.doc.status)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!frm.doc.purchase_receipt &&
|
||||||
|
!frm.doc.purchase_invoice &&
|
||||||
|
["Existing Asset", "Composite Component"].includes(frm.doc.asset_type)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset_category = await frappe.db.get_value(
|
||||||
|
"Asset Category",
|
||||||
|
frm.doc.asset_category,
|
||||||
|
"enable_cwip_accounting"
|
||||||
|
);
|
||||||
|
|
||||||
|
return !!asset_category.message?.enable_cwip_accounting;
|
||||||
|
},
|
||||||
|
|
||||||
set_depr_posting_failure_alert: function (frm) {
|
set_depr_posting_failure_alert: function (frm) {
|
||||||
const alert = `
|
const alert = `
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -232,7 +264,8 @@ frappe.ui.form.on("Asset", {
|
|||||||
|
|
||||||
toggle_reference_doc: function (frm) {
|
toggle_reference_doc: function (frm) {
|
||||||
const is_submitted = frm.doc.docstatus === 1;
|
const is_submitted = frm.doc.docstatus === 1;
|
||||||
const is_special_asset = frm.doc.is_existing_asset || frm.doc.is_composite_asset;
|
const is_special_asset =
|
||||||
|
frm.doc.asset_type == "Existing Asset" || frm.doc.asset_type == "Composite Asset";
|
||||||
|
|
||||||
const clear_field = (field) => {
|
const clear_field = (field) => {
|
||||||
if (frm.doc[field]) {
|
if (frm.doc[field]) {
|
||||||
@@ -508,15 +541,13 @@ frappe.ui.form.on("Asset", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
is_existing_asset: function (frm) {
|
asset_type: function (frm) {
|
||||||
frm.trigger("toggle_reference_doc");
|
if (frm.doc.docstatus == 0) {
|
||||||
},
|
if (frm.doc.asset_type == "Composite Asset") {
|
||||||
|
frm.set_value("net_purchase_amount", 0);
|
||||||
is_composite_asset: function (frm) {
|
} else {
|
||||||
if (frm.doc.is_composite_asset) {
|
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
||||||
frm.set_value("net_purchase_amount", 0);
|
}
|
||||||
} else {
|
|
||||||
frm.set_df_property("net_purchase_amount", "read_only", 0);
|
|
||||||
}
|
}
|
||||||
frm.trigger("toggle_reference_doc");
|
frm.trigger("toggle_reference_doc");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,20 +9,17 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"naming_series",
|
"naming_series",
|
||||||
|
"company",
|
||||||
"item_code",
|
"item_code",
|
||||||
"item_name",
|
"item_name",
|
||||||
"asset_name",
|
"asset_name",
|
||||||
"asset_category",
|
|
||||||
"location",
|
|
||||||
"image",
|
"image",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"status",
|
"location",
|
||||||
"company",
|
"asset_category",
|
||||||
"asset_owner",
|
"asset_type",
|
||||||
"asset_owner_company",
|
"maintenance_required",
|
||||||
"is_existing_asset",
|
"calculate_depreciation",
|
||||||
"is_composite_asset",
|
|
||||||
"is_composite_component",
|
|
||||||
"purchase_details_section",
|
"purchase_details_section",
|
||||||
"purchase_receipt",
|
"purchase_receipt",
|
||||||
"purchase_receipt_item",
|
"purchase_receipt_item",
|
||||||
@@ -30,31 +27,44 @@
|
|||||||
"purchase_invoice_item",
|
"purchase_invoice_item",
|
||||||
"purchase_date",
|
"purchase_date",
|
||||||
"available_for_use_date",
|
"available_for_use_date",
|
||||||
|
"disposal_date",
|
||||||
"column_break_23",
|
"column_break_23",
|
||||||
"net_purchase_amount",
|
"net_purchase_amount",
|
||||||
"purchase_amount",
|
"purchase_amount",
|
||||||
"asset_quantity",
|
"asset_quantity",
|
||||||
"additional_asset_cost",
|
"additional_asset_cost",
|
||||||
|
"section_break_uiyd",
|
||||||
|
"column_break_bbwr",
|
||||||
|
"column_break_bfkm",
|
||||||
"total_asset_cost",
|
"total_asset_cost",
|
||||||
"disposal_date",
|
|
||||||
"depreciation_tab",
|
"depreciation_tab",
|
||||||
"calculate_depreciation",
|
"column_break_wqzi",
|
||||||
"column_break_33",
|
|
||||||
"opening_accumulated_depreciation",
|
"opening_accumulated_depreciation",
|
||||||
"opening_number_of_booked_depreciations",
|
|
||||||
"is_fully_depreciated",
|
"is_fully_depreciated",
|
||||||
|
"column_break_33",
|
||||||
|
"opening_number_of_booked_depreciations",
|
||||||
"section_break_36",
|
"section_break_36",
|
||||||
"finance_books",
|
"finance_books",
|
||||||
"section_break_33",
|
"section_break_33",
|
||||||
"depreciation_method",
|
"depreciation_method",
|
||||||
"value_after_depreciation",
|
"value_after_depreciation",
|
||||||
"total_number_of_depreciations",
|
|
||||||
"column_break_24",
|
|
||||||
"frequency_of_depreciation",
|
"frequency_of_depreciation",
|
||||||
|
"column_break_24",
|
||||||
"next_depreciation_date",
|
"next_depreciation_date",
|
||||||
|
"total_number_of_depreciations",
|
||||||
"depreciation_schedule_sb",
|
"depreciation_schedule_sb",
|
||||||
"depreciation_schedule_view",
|
"depreciation_schedule_view",
|
||||||
"insurance_details_tab",
|
"other_info_tab",
|
||||||
|
"accounting_dimensions_section",
|
||||||
|
"cost_center",
|
||||||
|
"column_break_rjyw",
|
||||||
|
"asset_owner_section",
|
||||||
|
"asset_owner",
|
||||||
|
"column_break_yeds",
|
||||||
|
"asset_owner_company",
|
||||||
|
"customer",
|
||||||
|
"supplier",
|
||||||
|
"insurance_section",
|
||||||
"policy_number",
|
"policy_number",
|
||||||
"insurer",
|
"insurer",
|
||||||
"insured_value",
|
"insured_value",
|
||||||
@@ -62,22 +72,17 @@
|
|||||||
"insurance_start_date",
|
"insurance_start_date",
|
||||||
"insurance_end_date",
|
"insurance_end_date",
|
||||||
"comprehensive_insurance",
|
"comprehensive_insurance",
|
||||||
"other_info_tab",
|
|
||||||
"accounting_dimensions_section",
|
|
||||||
"cost_center",
|
|
||||||
"section_break_jtou",
|
"section_break_jtou",
|
||||||
|
"status",
|
||||||
"custodian",
|
"custodian",
|
||||||
|
"department",
|
||||||
"default_finance_book",
|
"default_finance_book",
|
||||||
"depr_entry_posting_status",
|
"depr_entry_posting_status",
|
||||||
"booked_fixed_asset",
|
|
||||||
"customer",
|
|
||||||
"supplier",
|
|
||||||
"column_break_51",
|
"column_break_51",
|
||||||
"department",
|
|
||||||
"split_from",
|
|
||||||
"journal_entry_for_scrap",
|
"journal_entry_for_scrap",
|
||||||
|
"split_from",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"maintenance_required",
|
"booked_fixed_asset",
|
||||||
"connections_tab"
|
"connections_tab"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -106,13 +111,6 @@
|
|||||||
"options": "Item",
|
"options": "Item",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "item_code",
|
|
||||||
"fetch_from": "item_code.item_name",
|
|
||||||
"fieldname": "item_name",
|
|
||||||
"fieldtype": "Read Only",
|
|
||||||
"label": "Item Name"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "item_code",
|
"depends_on": "item_code",
|
||||||
"fetch_from": "item_code.asset_category",
|
"fetch_from": "item_code.asset_category",
|
||||||
@@ -207,7 +205,7 @@
|
|||||||
"fieldname": "purchase_date",
|
"fieldname": "purchase_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Purchase Date",
|
"label": "Purchase Date",
|
||||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
|
"read_only_depends_on": "eval:doc.asset_type != \"Existing Asset\" && doc.asset_type != \"Composite Asset\"",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -229,25 +227,18 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "available_for_use_date",
|
"fieldname": "available_for_use_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Available-for-use Date",
|
"label": "Available for Use Date",
|
||||||
"mandatory_depends_on": "eval:(!(doc.is_composite_component || doc.is_composite_asset) || doc.docstatus==1)"
|
"mandatory_depends_on": "eval:(!(doc.asset_type == \"Composite Component\" || doc.asset_type == \"Composite Asset\") || doc.docstatus==1)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "calculate_depreciation",
|
"fieldname": "calculate_depreciation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Calculate Depreciation",
|
"label": "Calculate Depreciation",
|
||||||
"read_only_depends_on": "eval:(doc.is_composite_asset && !doc.net_purchase_amount) || doc.is_composite_component"
|
"read_only_depends_on": "eval:(doc.asset_type == \"Composite Asset\" && !doc.net_purchase_amount) || doc.asset_type == \"Composite Component\""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"depends_on": "eval:(doc.asset_type == \"Existing Asset\")",
|
||||||
"depends_on": "eval:(!doc.is_composite_asset && !doc.is_composite_component)",
|
|
||||||
"fieldname": "is_existing_asset",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Existing Asset"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval:(doc.is_existing_asset)",
|
|
||||||
"fieldname": "opening_accumulated_depreciation",
|
"fieldname": "opening_accumulated_depreciation",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Opening Accumulated Depreciation",
|
"label": "Opening Accumulated Depreciation",
|
||||||
@@ -257,18 +248,20 @@
|
|||||||
"columns": 10,
|
"columns": 10,
|
||||||
"fieldname": "finance_books",
|
"fieldname": "finance_books",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
|
"label": "Finance Books",
|
||||||
"options": "Asset Finance Book"
|
"options": "Asset Finance Book"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_33",
|
"fieldname": "section_break_33",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 1
|
"hidden": 1,
|
||||||
|
"label": "Depreciation Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "depreciation_method",
|
"fieldname": "depreciation_method",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Depreciation Method",
|
"label": "Depreciation Method",
|
||||||
"options": "\nStraight Line\nDouble Declining Balance\nManual"
|
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "value_after_depreciation",
|
"fieldname": "value_after_depreciation",
|
||||||
@@ -295,6 +288,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "next_depreciation_date",
|
"fieldname": "next_depreciation_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Next Depreciation Date",
|
"label": "Next Depreciation Date",
|
||||||
"no_copy": 1
|
"no_copy": 1
|
||||||
},
|
},
|
||||||
@@ -364,7 +358,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
"depends_on": "eval:doc.asset_type != \"Composite Asset\" && doc.asset_type != \"Existing Asset\"",
|
||||||
"fieldname": "purchase_receipt",
|
"fieldname": "purchase_receipt",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Purchase Receipt",
|
"label": "Purchase Receipt",
|
||||||
@@ -373,7 +367,7 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
"depends_on": "eval:doc.asset_type != \"Composite Asset\" && doc.asset_type != \"Existing Asset\"",
|
||||||
"fieldname": "purchase_invoice",
|
"fieldname": "purchase_invoice",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Purchase Invoice",
|
"label": "Purchase Invoice",
|
||||||
@@ -399,7 +393,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible_depends_on": "is_existing_asset",
|
"collapsible_depends_on": "eval:doc.asset_type == \"Existing Asset\"",
|
||||||
"fieldname": "purchase_details_section",
|
"fieldname": "purchase_details_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Purchase Details"
|
"label": "Purchase Details"
|
||||||
@@ -413,10 +407,9 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "calculate_depreciation",
|
"depends_on": "eval: doc.calculate_depreciation",
|
||||||
"fieldname": "section_break_36",
|
"fieldname": "section_break_36",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"label": "Finance Books"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "split_from",
|
"fieldname": "split_from",
|
||||||
@@ -455,18 +448,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:(doc.is_existing_asset)",
|
|
||||||
"fieldname": "is_fully_depreciated",
|
"fieldname": "is_fully_depreciated",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Is Fully Depreciated"
|
"label": "Is Fully Depreciated"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "eval:(!doc.is_existing_asset && !doc.is_composite_component)",
|
|
||||||
"fieldname": "is_composite_asset",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Composite Asset"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.docstatus > 0",
|
"depends_on": "eval:doc.docstatus > 0",
|
||||||
"fieldname": "total_asset_cost",
|
"fieldname": "total_asset_cost",
|
||||||
@@ -496,7 +482,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:(doc.is_existing_asset)",
|
"depends_on": "eval:(doc.asset_type == \"Existing Asset\")",
|
||||||
"fieldname": "opening_number_of_booked_depreciations",
|
"fieldname": "opening_number_of_booked_depreciations",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Opening Number of Booked Depreciations"
|
"label": "Opening Number of Booked Depreciations"
|
||||||
@@ -513,15 +499,10 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Purchase Invoice Item"
|
"label": "Purchase Invoice Item"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "insurance_details_tab",
|
|
||||||
"fieldtype": "Tab Break",
|
|
||||||
"label": "Insurance"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "other_info_tab",
|
"fieldname": "other_info_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Other Info"
|
"label": "More Info"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "connections_tab",
|
"fieldname": "connections_tab",
|
||||||
@@ -530,6 +511,7 @@
|
|||||||
"show_dashboard": 1
|
"show_dashboard": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.calculate_depreciation || doc.asset_type == \"Existing Asset\"",
|
||||||
"fieldname": "depreciation_tab",
|
"fieldname": "depreciation_tab",
|
||||||
"fieldtype": "Tab Break",
|
"fieldtype": "Tab Break",
|
||||||
"label": "Depreciation"
|
"label": "Depreciation"
|
||||||
@@ -544,20 +526,61 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Additional Info"
|
"label": "Additional Info"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "eval:(!doc.is_existing_asset && !doc.is_composite_asset)",
|
|
||||||
"fieldname": "is_composite_component",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Composite Component"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "net_purchase_amount",
|
"fieldname": "net_purchase_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Net Purchase Amount",
|
"label": "Net Purchase Amount",
|
||||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
"mandatory_depends_on": "eval:(doc.asset_type != \"Composite Asset\" || doc.docstatus==1)",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only_depends_on": "eval: doc.is_composite_asset"
|
"read_only_depends_on": "eval: doc.asset_type == \"Composite Asset\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "asset_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Asset Type",
|
||||||
|
"options": "\nExisting Asset\nComposite Asset\nComposite Component"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wqzi",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_rjyw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "insurance_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Insurance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_uiyd",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bbwr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bfkm",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Read Only",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Item Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "asset_owner_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Ownership"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_yeds",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 72,
|
"idx": 72,
|
||||||
@@ -601,7 +624,7 @@
|
|||||||
"link_fieldname": "target_asset"
|
"link_fieldname": "target_asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-12-18 16:36:40.904246",
|
"modified": "2026-02-05 12:42:45.350216",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class Asset(AccountsController):
|
|||||||
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
|
asset_owner: DF.Literal["", "Company", "Supplier", "Customer"]
|
||||||
asset_owner_company: DF.Link | None
|
asset_owner_company: DF.Link | None
|
||||||
asset_quantity: DF.Int
|
asset_quantity: DF.Int
|
||||||
|
asset_type: DF.Literal["", "Existing Asset", "Composite Asset", "Composite Component"]
|
||||||
available_for_use_date: DF.Date | None
|
available_for_use_date: DF.Date | None
|
||||||
booked_fixed_asset: DF.Check
|
booked_fixed_asset: DF.Check
|
||||||
calculate_depreciation: DF.Check
|
calculate_depreciation: DF.Check
|
||||||
@@ -67,7 +68,9 @@ class Asset(AccountsController):
|
|||||||
default_finance_book: DF.Link | None
|
default_finance_book: DF.Link | None
|
||||||
department: DF.Link | None
|
department: DF.Link | None
|
||||||
depr_entry_posting_status: DF.Literal["", "Successful", "Failed"]
|
depr_entry_posting_status: DF.Literal["", "Successful", "Failed"]
|
||||||
depreciation_method: DF.Literal["", "Straight Line", "Double Declining Balance", "Manual"]
|
depreciation_method: DF.Literal[
|
||||||
|
"", "Straight Line", "Double Declining Balance", "Written Down Value", "Manual"
|
||||||
|
]
|
||||||
disposal_date: DF.Date | None
|
disposal_date: DF.Date | None
|
||||||
finance_books: DF.Table[AssetFinanceBook]
|
finance_books: DF.Table[AssetFinanceBook]
|
||||||
frequency_of_depreciation: DF.Int
|
frequency_of_depreciation: DF.Int
|
||||||
@@ -76,9 +79,6 @@ class Asset(AccountsController):
|
|||||||
insurance_start_date: DF.Date | None
|
insurance_start_date: DF.Date | None
|
||||||
insured_value: DF.Data | None
|
insured_value: DF.Data | None
|
||||||
insurer: DF.Data | None
|
insurer: DF.Data | None
|
||||||
is_composite_asset: DF.Check
|
|
||||||
is_composite_component: DF.Check
|
|
||||||
is_existing_asset: DF.Check
|
|
||||||
is_fully_depreciated: DF.Check
|
is_fully_depreciated: DF.Check
|
||||||
item_code: DF.Link
|
item_code: DF.Link
|
||||||
item_name: DF.ReadOnly | None
|
item_name: DF.ReadOnly | None
|
||||||
@@ -243,14 +243,20 @@ class Asset(AccountsController):
|
|||||||
self.set_total_booked_depreciations()
|
self.set_total_booked_depreciations()
|
||||||
|
|
||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
if self.is_composite_asset and not has_active_capitalization(self.name):
|
if self.asset_type == "Composite Asset" and not has_active_capitalization(self.name):
|
||||||
|
if self.split_from and has_active_capitalization(self.split_from):
|
||||||
|
return
|
||||||
frappe.throw(_("Please capitalize this asset before submitting."))
|
frappe.throw(_("Please capitalize this asset before submitting."))
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_in_use_date()
|
self.validate_in_use_date()
|
||||||
self.make_asset_movement()
|
self.make_asset_movement()
|
||||||
self.reload()
|
self.reload()
|
||||||
if not self.booked_fixed_asset and not self.is_composite_component and self.validate_make_gl_entry():
|
if (
|
||||||
|
not self.booked_fixed_asset
|
||||||
|
and self.asset_type != "Composite Component"
|
||||||
|
and self.validate_make_gl_entry()
|
||||||
|
):
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
if self.calculate_depreciation and not self.split_from:
|
if self.calculate_depreciation and not self.split_from:
|
||||||
convert_draft_asset_depr_schedules_into_active(self)
|
convert_draft_asset_depr_schedules_into_active(self)
|
||||||
@@ -265,7 +271,7 @@ class Asset(AccountsController):
|
|||||||
cancel_asset_depr_schedules(self)
|
cancel_asset_depr_schedules(self)
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||||
if not self.is_composite_component:
|
if self.asset_type != "Composite Component":
|
||||||
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
|
||||||
self.db_set("booked_fixed_asset", 0)
|
self.db_set("booked_fixed_asset", 0)
|
||||||
add_asset_activity(self.name, _("Asset cancelled"))
|
add_asset_activity(self.name, _("Asset cancelled"))
|
||||||
@@ -283,7 +289,7 @@ class Asset(AccountsController):
|
|||||||
add_asset_activity(self.name, _("Asset deleted"))
|
add_asset_activity(self.name, _("Asset deleted"))
|
||||||
|
|
||||||
def set_purchase_doc_row_item(self):
|
def set_purchase_doc_row_item(self):
|
||||||
if self.is_existing_asset or self.is_composite_asset:
|
if self.asset_type == "Existing Asset" or self.asset_type == "Composite Asset":
|
||||||
return
|
return
|
||||||
|
|
||||||
self.purchase_amount = self.net_purchase_amount
|
self.purchase_amount = self.net_purchase_amount
|
||||||
@@ -326,7 +332,7 @@ class Asset(AccountsController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.is_existing_asset and self.purchase_invoice:
|
if self.asset_type == "Existing Asset" and self.purchase_invoice:
|
||||||
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
|
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
|
||||||
|
|
||||||
def validate_item(self):
|
def validate_item(self):
|
||||||
@@ -372,7 +378,7 @@ class Asset(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_in_use_date(self):
|
def validate_in_use_date(self):
|
||||||
if not self.available_for_use_date and not self.is_composite_component:
|
if not self.available_for_use_date and self.asset_type != "Composite Component":
|
||||||
frappe.throw(_("Available for use date is required"))
|
frappe.throw(_("Available for use date is required"))
|
||||||
|
|
||||||
for d in self.finance_books:
|
for d in self.finance_books:
|
||||||
@@ -420,12 +426,15 @@ class Asset(AccountsController):
|
|||||||
non_depreciable_category = frappe.db.get_value(
|
non_depreciable_category = frappe.db.get_value(
|
||||||
"Asset Category", self.asset_category, "non_depreciable_category"
|
"Asset Category", self.asset_category, "non_depreciable_category"
|
||||||
)
|
)
|
||||||
if self.calculate_depreciation and non_depreciable_category:
|
if self.calculate_depreciation:
|
||||||
frappe.throw(
|
if non_depreciable_category:
|
||||||
_(
|
frappe.throw(
|
||||||
"This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category."
|
_(
|
||||||
|
"This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category."
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
# validate accounts required for asset depreciation
|
||||||
|
get_depreciation_accounts(self.asset_category, self.company)
|
||||||
|
|
||||||
def validate_precision(self):
|
def validate_precision(self):
|
||||||
if self.net_purchase_amount:
|
if self.net_purchase_amount:
|
||||||
@@ -440,13 +449,13 @@ class Asset(AccountsController):
|
|||||||
if not self.asset_category:
|
if not self.asset_category:
|
||||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||||
|
|
||||||
if not flt(self.net_purchase_amount) and not self.is_composite_asset:
|
if not flt(self.net_purchase_amount) and self.asset_type != "Composite Asset":
|
||||||
frappe.throw(_("Net Purchase Amount is mandatory"), frappe.MandatoryError)
|
frappe.throw(_("Net Purchase Amount is mandatory"), frappe.MandatoryError)
|
||||||
|
|
||||||
if is_cwip_accounting_enabled(self.asset_category):
|
if is_cwip_accounting_enabled(self.asset_category):
|
||||||
if (
|
if (
|
||||||
not self.is_existing_asset
|
not self.asset_type == "Existing Asset"
|
||||||
and not self.is_composite_asset
|
and not self.asset_type == "Composite Asset"
|
||||||
and not self.purchase_receipt
|
and not self.purchase_receipt
|
||||||
and not self.purchase_invoice
|
and not self.purchase_invoice
|
||||||
):
|
):
|
||||||
@@ -475,7 +484,7 @@ class Asset(AccountsController):
|
|||||||
if self.is_fully_depreciated:
|
if self.is_fully_depreciated:
|
||||||
frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
|
frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
|
||||||
|
|
||||||
if self.is_existing_asset:
|
if self.asset_type == "Existing Asset":
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
|
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
|
||||||
@@ -547,7 +556,7 @@ class Asset(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_gross_and_purchase_amount(self):
|
def validate_gross_and_purchase_amount(self):
|
||||||
if self.is_existing_asset:
|
if self.asset_type == "Existing Asset":
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.net_purchase_amount and self.net_purchase_amount != self.purchase_amount:
|
if self.net_purchase_amount and self.net_purchase_amount != self.purchase_amount:
|
||||||
@@ -613,7 +622,7 @@ class Asset(AccountsController):
|
|||||||
self.validate_depreciation_start_date(row)
|
self.validate_depreciation_start_date(row)
|
||||||
self.validate_total_number_of_depreciations_and_frequency(row)
|
self.validate_total_number_of_depreciations_and_frequency(row)
|
||||||
|
|
||||||
if not self.is_existing_asset:
|
if self.asset_type != "Existing Asset":
|
||||||
self.opening_accumulated_depreciation = 0
|
self.opening_accumulated_depreciation = 0
|
||||||
self.opening_number_of_booked_depreciations = 0
|
self.opening_number_of_booked_depreciations = 0
|
||||||
else:
|
else:
|
||||||
@@ -766,7 +775,7 @@ class Asset(AccountsController):
|
|||||||
def get_status(self):
|
def get_status(self):
|
||||||
"""Returns status based on whether it is draft, submitted, scrapped or depreciated"""
|
"""Returns status based on whether it is draft, submitted, scrapped or depreciated"""
|
||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
if self.is_composite_asset:
|
if self.asset_type == "Composite Asset":
|
||||||
status = "Work In Progress"
|
status = "Work In Progress"
|
||||||
else:
|
else:
|
||||||
status = "Draft"
|
status = "Draft"
|
||||||
@@ -786,13 +795,12 @@ class Asset(AccountsController):
|
|||||||
].expected_value_after_useful_life
|
].expected_value_after_useful_life
|
||||||
value_after_depreciation = self.finance_books[idx].value_after_depreciation
|
value_after_depreciation = self.finance_books[idx].value_after_depreciation
|
||||||
|
|
||||||
if (
|
if flt(value_after_depreciation) <= expected_value_after_useful_life:
|
||||||
flt(value_after_depreciation) <= expected_value_after_useful_life
|
|
||||||
or self.is_fully_depreciated
|
|
||||||
):
|
|
||||||
status = "Fully Depreciated"
|
status = "Fully Depreciated"
|
||||||
elif flt(value_after_depreciation) < flt(self.net_purchase_amount):
|
elif flt(value_after_depreciation) < flt(self.net_purchase_amount):
|
||||||
status = "Partially Depreciated"
|
status = "Partially Depreciated"
|
||||||
|
elif self.is_fully_depreciated:
|
||||||
|
status = "Fully Depreciated"
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
status = "Cancelled"
|
status = "Cancelled"
|
||||||
return status
|
return status
|
||||||
@@ -839,7 +847,7 @@ class Asset(AccountsController):
|
|||||||
return records
|
return records
|
||||||
|
|
||||||
def validate_make_gl_entry(self):
|
def validate_make_gl_entry(self):
|
||||||
if self.is_composite_asset:
|
if self.asset_type == "Composite Asset":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
purchase_document = self.get_purchase_document()
|
purchase_document = self.get_purchase_document()
|
||||||
@@ -920,7 +928,7 @@ class Asset(AccountsController):
|
|||||||
purchase_document = self.get_purchase_document()
|
purchase_document = self.get_purchase_document()
|
||||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||||
|
|
||||||
if (self.is_composite_asset or (purchase_document and self.purchase_amount)) and getdate(
|
if (self.asset_type == "Composite Asset" or (purchase_document and self.purchase_amount)) and getdate(
|
||||||
self.available_for_use_date
|
self.available_for_use_date
|
||||||
) <= getdate():
|
) <= getdate():
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
@@ -960,7 +968,7 @@ class Asset(AccountsController):
|
|||||||
self.db_set("booked_fixed_asset", 1)
|
self.db_set("booked_fixed_asset", 1)
|
||||||
|
|
||||||
def check_asset_capitalization_gl_entries(self):
|
def check_asset_capitalization_gl_entries(self):
|
||||||
if self.is_composite_asset:
|
if self.asset_type == "Composite Asset":
|
||||||
result = frappe.db.get_value(
|
result = frappe.db.get_value(
|
||||||
"Asset Capitalization",
|
"Asset Capitalization",
|
||||||
{"target_asset": self.name, "docstatus": 1},
|
{"target_asset": self.name, "docstatus": 1},
|
||||||
@@ -1084,7 +1092,7 @@ def get_asset_naming_series():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
|
def make_sales_invoice(asset: str, item_code: str, company: str, sell_qty: int, serial_no: str | None = None):
|
||||||
asset_doc = frappe.get_doc("Asset", asset)
|
asset_doc = frappe.get_doc("Asset", asset)
|
||||||
si = frappe.new_doc("Sales Invoice")
|
si = frappe.new_doc("Sales Invoice")
|
||||||
si.company = company
|
si.company = company
|
||||||
@@ -1117,7 +1125,13 @@ def make_sales_invoice(asset, item_code, company, sell_qty, serial_no=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_asset_maintenance(asset, item_code, item_name, asset_category, company):
|
def create_asset_maintenance(
|
||||||
|
asset: str,
|
||||||
|
item_code: str,
|
||||||
|
item_name: str,
|
||||||
|
asset_category: str,
|
||||||
|
company: str,
|
||||||
|
):
|
||||||
asset_maintenance = frappe.new_doc("Asset Maintenance")
|
asset_maintenance = frappe.new_doc("Asset Maintenance")
|
||||||
asset_maintenance.update(
|
asset_maintenance.update(
|
||||||
{
|
{
|
||||||
@@ -1132,14 +1146,23 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_asset_repair(company, asset, asset_name):
|
def create_asset_repair(
|
||||||
|
company: str,
|
||||||
|
asset: str,
|
||||||
|
asset_name: str,
|
||||||
|
):
|
||||||
asset_repair = frappe.new_doc("Asset Repair")
|
asset_repair = frappe.new_doc("Asset Repair")
|
||||||
asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
|
asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
|
||||||
return asset_repair
|
return asset_repair
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_asset_capitalization(company, asset, asset_name, item_code):
|
def create_asset_capitalization(
|
||||||
|
company: str,
|
||||||
|
asset: str,
|
||||||
|
asset_name: str,
|
||||||
|
item_code: str,
|
||||||
|
):
|
||||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||||
asset_capitalization.update(
|
asset_capitalization.update(
|
||||||
{
|
{
|
||||||
@@ -1153,35 +1176,22 @@ def create_asset_capitalization(company, asset, asset_name, item_code):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_asset_value_adjustment(asset, asset_category, company):
|
def create_asset_value_adjustment(
|
||||||
|
asset: str,
|
||||||
|
asset_category: str,
|
||||||
|
company: str,
|
||||||
|
):
|
||||||
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
||||||
asset_value_adjustment.update({"asset": asset, "company": company, "asset_category": asset_category})
|
asset_value_adjustment.update({"asset": asset, "company": company, "asset_category": asset_category})
|
||||||
return asset_value_adjustment
|
return asset_value_adjustment
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def transfer_asset(args):
|
def get_item_details(
|
||||||
args = json.loads(args)
|
item_code: str,
|
||||||
|
asset_category: str,
|
||||||
if args.get("serial_no"):
|
net_purchase_amount: float,
|
||||||
args["quantity"] = len(args.get("serial_no").split("\n"))
|
):
|
||||||
|
|
||||||
movement_entry = frappe.new_doc("Asset Movement")
|
|
||||||
movement_entry.update(args)
|
|
||||||
movement_entry.insert()
|
|
||||||
movement_entry.submit()
|
|
||||||
|
|
||||||
frappe.db.commit()
|
|
||||||
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Asset Movement record {0} created")
|
|
||||||
.format("<a href='/app/Form/Asset Movement/{0}'>{0}</a>")
|
|
||||||
.format(movement_entry.name)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_item_details(item_code, asset_category, net_purchase_amount):
|
|
||||||
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
|
asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category)
|
||||||
books = []
|
books = []
|
||||||
for d in asset_category_doc.finance_books:
|
for d in asset_category_doc.finance_books:
|
||||||
@@ -1231,7 +1241,7 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_journal_entry(asset_name):
|
def make_journal_entry(asset_name: str):
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
(
|
(
|
||||||
fixed_asset_account,
|
fixed_asset_account,
|
||||||
@@ -1273,7 +1283,10 @@ def make_journal_entry(asset_name):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_asset_movement(assets, purpose=None):
|
def make_asset_movement(
|
||||||
|
assets: list[dict] | str,
|
||||||
|
purpose: str = "Transfer",
|
||||||
|
):
|
||||||
import json
|
import json
|
||||||
|
|
||||||
if isinstance(assets, str):
|
if isinstance(assets, str):
|
||||||
@@ -1283,7 +1296,7 @@ def make_asset_movement(assets, purpose=None):
|
|||||||
frappe.throw(_("At least one asset has to be selected."))
|
frappe.throw(_("At least one asset has to be selected."))
|
||||||
|
|
||||||
asset_movement = frappe.new_doc("Asset Movement")
|
asset_movement = frappe.new_doc("Asset Movement")
|
||||||
asset_movement.quantity = len(assets)
|
asset_movement.purpose = purpose
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
asset = frappe.get_doc("Asset", asset.get("name"))
|
asset = frappe.get_doc("Asset", asset.get("name"))
|
||||||
asset_movement.company = asset.get("company")
|
asset_movement.company = asset.get("company")
|
||||||
@@ -1305,7 +1318,10 @@ def is_cwip_accounting_enabled(asset_category):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
def get_asset_value_after_depreciation(
|
||||||
|
asset_name: str,
|
||||||
|
finance_book: str | None = None,
|
||||||
|
):
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
if not asset.calculate_depreciation:
|
if not asset.calculate_depreciation:
|
||||||
return flt(asset.value_after_depreciation)
|
return flt(asset.value_after_depreciation)
|
||||||
@@ -1314,7 +1330,7 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def has_active_capitalization(asset):
|
def has_active_capitalization(asset: str):
|
||||||
active_capitalizations = frappe.db.count(
|
active_capitalizations = frappe.db.count(
|
||||||
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
|
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
|
||||||
)
|
)
|
||||||
@@ -1322,7 +1338,11 @@ def has_active_capitalization(asset):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
def get_values_from_purchase_doc(
|
||||||
|
purchase_doc_name: str,
|
||||||
|
item_code: str,
|
||||||
|
doctype: str,
|
||||||
|
):
|
||||||
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
|
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
|
||||||
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
|
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
|
||||||
|
|
||||||
@@ -1344,7 +1364,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def split_asset(asset_name, split_qty):
|
def split_asset(asset_name: str, split_qty: int):
|
||||||
"""Split an asset into two based on the given quantity."""
|
"""Split an asset into two based on the given quantity."""
|
||||||
existing_asset = frappe.get_doc("Asset", asset_name)
|
existing_asset = frappe.get_doc("Asset", asset_name)
|
||||||
split_qty = cint(split_qty)
|
split_qty = cint(split_qty)
|
||||||
@@ -1391,7 +1411,7 @@ def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_a
|
|||||||
|
|
||||||
def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
|
def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
|
||||||
asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
asset_doc.net_purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
||||||
asset_doc.purchase_amount = existing_asset.net_purchase_amount
|
asset_doc.purchase_amount = existing_asset.net_purchase_amount * scaling_factor
|
||||||
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
|
asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
|
||||||
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
|
asset_doc.total_asset_cost = asset_doc.net_purchase_amount + asset_doc.additional_asset_cost
|
||||||
asset_doc.opening_accumulated_depreciation = (
|
asset_doc.opening_accumulated_depreciation = (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from frappe import _
|
|||||||
from frappe.query_builder import Order
|
from frappe.query_builder import Order
|
||||||
from frappe.query_builder.functions import Max, Min
|
from frappe.query_builder.functions import Max, Min
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
|
DateTimeLikeObject,
|
||||||
add_months,
|
add_months,
|
||||||
cint,
|
cint,
|
||||||
flt,
|
flt,
|
||||||
@@ -161,11 +162,11 @@ def get_depr_cost_center_and_series():
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_depreciation_entry(
|
def make_depreciation_entry(
|
||||||
depr_schedule_name,
|
depr_schedule_name: str,
|
||||||
date=None,
|
date: DateTimeLikeObject | None = None,
|
||||||
sch_start_idx=None,
|
sch_start_idx: int | None = None,
|
||||||
sch_end_idx=None,
|
sch_end_idx: int | None = None,
|
||||||
accounting_dimensions=None,
|
accounting_dimensions: list[dict] | None = None,
|
||||||
):
|
):
|
||||||
frappe.has_permission("Journal Entry", throw=True)
|
frappe.has_permission("Journal Entry", throw=True)
|
||||||
date = date or today()
|
date = date or today()
|
||||||
@@ -246,7 +247,9 @@ def _make_journal_entry_for_depreciation(
|
|||||||
|
|
||||||
def setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset):
|
def setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset):
|
||||||
je.voucher_type = "Depreciation Entry"
|
je.voucher_type = "Depreciation Entry"
|
||||||
je.naming_series = depr_series
|
if depr_series:
|
||||||
|
je.naming_series = depr_series
|
||||||
|
|
||||||
je.posting_date = depr_schedule.schedule_date
|
je.posting_date = depr_schedule.schedule_date
|
||||||
je.company = asset.company
|
je.company = asset.company
|
||||||
je.finance_book = depr_schedule_doc.finance_book
|
je.finance_book = depr_schedule_doc.finance_book
|
||||||
@@ -354,7 +357,7 @@ def get_message_for_depr_entry_posting_error(asset_links, error_log_links):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def scrap_asset(asset_name, scrap_date=None):
|
def scrap_asset(asset_name: str, scrap_date: DateTimeLikeObject | None = None):
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
scrap_date = getdate(scrap_date) or getdate(today())
|
scrap_date = getdate(scrap_date) or getdate(today())
|
||||||
asset.db_set("disposal_date", scrap_date)
|
asset.db_set("disposal_date", scrap_date)
|
||||||
@@ -443,7 +446,7 @@ def create_journal_entry_for_scrap(asset, scrap_date):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def restore_asset(asset_name):
|
def restore_asset(asset_name: str):
|
||||||
asset = frappe.get_doc("Asset", asset_name)
|
asset = frappe.get_doc("Asset", asset_name)
|
||||||
reverse_depreciation_entry_made_on_disposal(asset)
|
reverse_depreciation_entry_made_on_disposal(asset)
|
||||||
reset_depreciation_schedule(asset, get_note_for_restore(asset))
|
reset_depreciation_schedule(asset, get_note_for_restore(asset))
|
||||||
@@ -770,7 +773,7 @@ def get_profit_gl_entries(
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_disposal_account_and_cost_center(company):
|
def get_disposal_account_and_cost_center(company: str):
|
||||||
disposal_account, depreciation_cost_center = frappe.get_cached_value(
|
disposal_account, depreciation_cost_center = frappe.get_cached_value(
|
||||||
"Company", company, ["disposal_account", "depreciation_cost_center"]
|
"Company", company, ["disposal_account", "depreciation_cost_center"]
|
||||||
)
|
)
|
||||||
@@ -784,10 +787,14 @@ def get_disposal_account_and_cost_center(company):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
|
def get_value_after_depreciation_on_disposal_date(
|
||||||
|
asset: str,
|
||||||
|
disposal_date: DateTimeLikeObject,
|
||||||
|
finance_book: str | None = None,
|
||||||
|
):
|
||||||
asset_doc = frappe.get_doc("Asset", asset)
|
asset_doc = frappe.get_doc("Asset", asset)
|
||||||
|
|
||||||
if asset_doc.is_composite_component:
|
if asset_doc.asset_type == "Composite Component":
|
||||||
validate_disposal_date(asset_doc.purchase_date, getdate(disposal_date), "purchase")
|
validate_disposal_date(asset_doc.purchase_date, getdate(disposal_date), "purchase")
|
||||||
return flt(asset_doc.value_after_depreciation)
|
return flt(asset_doc.value_after_depreciation)
|
||||||
|
|
||||||
|
|||||||
@@ -70,16 +70,16 @@ class TestAsset(AssetSetup):
|
|||||||
self.assertRaises(frappe.MandatoryError, asset.save)
|
self.assertRaises(frappe.MandatoryError, asset.save)
|
||||||
|
|
||||||
def test_pr_or_pi_mandatory_if_not_existing_asset(self):
|
def test_pr_or_pi_mandatory_if_not_existing_asset(self):
|
||||||
"""Tests if either PI or PR is present if CWIP is enabled and is_existing_asset=0."""
|
"""Tests if either PI or PR is present if CWIP is enabled and asset_type == Existing Asset."""
|
||||||
|
|
||||||
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
||||||
asset.is_existing_asset = 0
|
asset.asset_type = ""
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, asset.save)
|
self.assertRaises(frappe.ValidationError, asset.save)
|
||||||
|
|
||||||
def test_available_for_use_date_is_after_purchase_date(self):
|
def test_available_for_use_date_is_after_purchase_date(self):
|
||||||
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, do_not_save=1)
|
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, do_not_save=1)
|
||||||
asset.is_existing_asset = 0
|
asset.asset_type = ""
|
||||||
asset.purchase_date = getdate("2021-10-10")
|
asset.purchase_date = getdate("2021-10-10")
|
||||||
asset.available_for_use_date = getdate("2021-10-1")
|
asset.available_for_use_date = getdate("2021-10-1")
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ class TestAsset(AssetSetup):
|
|||||||
asset.submit()
|
asset.submit()
|
||||||
|
|
||||||
def test_is_fixed_asset_set(self):
|
def test_is_fixed_asset_set(self):
|
||||||
asset = create_asset(is_existing_asset=1)
|
asset = create_asset(asset_type="Existing Asset")
|
||||||
doc = frappe.new_doc("Purchase Invoice")
|
doc = frappe.new_doc("Purchase Invoice")
|
||||||
doc.company = "_Test Company"
|
doc.company = "_Test Company"
|
||||||
doc.supplier = "_Test Supplier"
|
doc.supplier = "_Test Supplier"
|
||||||
@@ -709,7 +709,7 @@ class TestAsset(AssetSetup):
|
|||||||
# create an asset
|
# create an asset
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
item_code="Macbook Pro",
|
item_code="Macbook Pro",
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
available_for_use_date=purchase_date,
|
available_for_use_date=purchase_date,
|
||||||
purchase_date=purchase_date,
|
purchase_date=purchase_date,
|
||||||
@@ -823,6 +823,92 @@ class TestAsset(AssetSetup):
|
|||||||
|
|
||||||
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 0)
|
frappe.db.set_value("Item", asset_item, "is_grouped_asset", 0)
|
||||||
|
|
||||||
|
def test_is_fully_depreciated_asset_status(self):
|
||||||
|
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
||||||
|
asset.is_fully_depreciated = 1
|
||||||
|
asset.save().submit()
|
||||||
|
self.assertEqual(asset.status, "Fully Depreciated")
|
||||||
|
|
||||||
|
def test_depreciation_accounts_is_set_for_depreciable_assets(self):
|
||||||
|
company_depreciation_accounts = frappe.db.get_value(
|
||||||
|
"Company",
|
||||||
|
"_Test Company",
|
||||||
|
[
|
||||||
|
"accumulated_depreciation_account",
|
||||||
|
"depreciation_expense_account",
|
||||||
|
],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
"_Test Company",
|
||||||
|
{
|
||||||
|
"accumulated_depreciation_account": "",
|
||||||
|
"depreciation_expense_account": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
asset_category_name = "Computers"
|
||||||
|
asset_category_account = None
|
||||||
|
if frappe.db.exists("Asset Category", asset_category_name):
|
||||||
|
filters = {
|
||||||
|
"parent": asset_category_name,
|
||||||
|
"company_name": "_Test Company",
|
||||||
|
}
|
||||||
|
fieldname = [
|
||||||
|
"name",
|
||||||
|
"accumulated_depreciation_account",
|
||||||
|
"depreciation_expense_account",
|
||||||
|
]
|
||||||
|
asset_category_account = frappe.db.get_value(
|
||||||
|
"Asset Category Account",
|
||||||
|
filters=filters,
|
||||||
|
fieldname=fieldname,
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
if asset_category_account and (
|
||||||
|
asset_category_account.accumulated_depreciation_account
|
||||||
|
or asset_category_account.depreciation_expense_account
|
||||||
|
):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Asset Category Account",
|
||||||
|
asset_category_account.name,
|
||||||
|
{
|
||||||
|
"accumulated_depreciation_account": "",
|
||||||
|
"depreciation_expense_account": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
asset_category = frappe.new_doc("Asset Category")
|
||||||
|
asset_category.asset_category_name = asset_category_name
|
||||||
|
asset_category.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"company_name": "_Test Company",
|
||||||
|
"fixed_asset_account": "_Test Fixed Asset - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
asset_category.insert()
|
||||||
|
try:
|
||||||
|
asset = create_asset(asset_category=asset_category_name, calculate_depreciation=1, do_not_save=1)
|
||||||
|
with self.assertRaises(frappe.ValidationError) as err:
|
||||||
|
asset.save()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
"Please set Depreciation related Accounts in Asset Category Computers or Company"
|
||||||
|
in str(err.exception)
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
frappe.db.set_value("Company", "_Test Company", company_depreciation_accounts)
|
||||||
|
if asset_category_account:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Asset Category Account",
|
||||||
|
asset_category_account.name,
|
||||||
|
{
|
||||||
|
"accumulated_depreciation_account": asset_category_account.accumulated_depreciation_account,
|
||||||
|
"depreciation_expense_account": asset_category_account.depreciation_expense_account,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDepreciationMethods(AssetSetup):
|
class TestDepreciationMethods(AssetSetup):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -901,7 +987,7 @@ class TestDepreciationMethods(AssetSetup):
|
|||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
available_for_use_date="2030-06-06",
|
available_for_use_date="2030-06-06",
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
opening_number_of_booked_depreciations=2,
|
opening_number_of_booked_depreciations=2,
|
||||||
opening_accumulated_depreciation=47178.08,
|
opening_accumulated_depreciation=47178.08,
|
||||||
expected_value_after_useful_life=10000,
|
expected_value_after_useful_life=10000,
|
||||||
@@ -950,7 +1036,7 @@ class TestDepreciationMethods(AssetSetup):
|
|||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
available_for_use_date="2030-01-01",
|
available_for_use_date="2030-01-01",
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
depreciation_method="Double Declining Balance",
|
depreciation_method="Double Declining Balance",
|
||||||
opening_number_of_booked_depreciations=1,
|
opening_number_of_booked_depreciations=1,
|
||||||
opening_accumulated_depreciation=50000,
|
opening_accumulated_depreciation=50000,
|
||||||
@@ -1691,7 +1777,7 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
self.assertEqual(asset.finance_books[0].value_after_depreciation, 100000.0)
|
self.assertEqual(asset.finance_books[0].value_after_depreciation, 100000.0)
|
||||||
|
|
||||||
def test_asset_cost_center(self):
|
def test_asset_cost_center(self):
|
||||||
asset = create_asset(is_existing_asset=1, do_not_save=1)
|
asset = create_asset(asset_type="Existing Asset", do_not_save=1)
|
||||||
asset.cost_center = "Main - WP"
|
asset.cost_center = "Main - WP"
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, asset.submit)
|
self.assertRaises(frappe.ValidationError, asset.submit)
|
||||||
@@ -1728,7 +1814,7 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
def test_manual_depreciation_for_existing_asset(self):
|
def test_manual_depreciation_for_existing_asset(self):
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
item_code="Macbook Pro",
|
item_code="Macbook Pro",
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
purchase_date="2020-01-30",
|
purchase_date="2020-01-30",
|
||||||
available_for_use_date="2020-01-30",
|
available_for_use_date="2020-01-30",
|
||||||
submit=1,
|
submit=1,
|
||||||
@@ -1828,6 +1914,71 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
pr.submit()
|
pr.submit()
|
||||||
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
|
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
|
||||||
|
|
||||||
|
def test_split_asset_created_via_capitalization(self):
|
||||||
|
"""Test that assets created via Asset Capitalization can be split without capitalization error"""
|
||||||
|
from erpnext.assets.doctype.asset_capitalization.test_asset_capitalization import (
|
||||||
|
create_asset_capitalization,
|
||||||
|
create_asset_capitalization_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure test data exists
|
||||||
|
create_asset_capitalization_data()
|
||||||
|
|
||||||
|
company = "_Test Company with perpetual inventory"
|
||||||
|
set_depreciation_settings_in_company(company=company)
|
||||||
|
name = frappe.db.get_value(
|
||||||
|
"Asset Category Account",
|
||||||
|
filters={"parent": "Computers", "company_name": company},
|
||||||
|
fieldname=["name"],
|
||||||
|
)
|
||||||
|
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||||
|
|
||||||
|
stock_rate = 1000
|
||||||
|
stock_qty = 2
|
||||||
|
total_amount = 2000
|
||||||
|
|
||||||
|
# Create composite asset
|
||||||
|
wip_composite_asset = create_asset(
|
||||||
|
asset_name="Asset Capitalization WIP Composite Asset for Split",
|
||||||
|
asset_type="Composite Asset",
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
company=company,
|
||||||
|
asset_quantity=2, # Set quantity > 1 to allow splitting
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create and submit Asset Capitalization
|
||||||
|
asset_capitalization = create_asset_capitalization(
|
||||||
|
target_asset=wip_composite_asset.name,
|
||||||
|
stock_qty=stock_qty,
|
||||||
|
stock_rate=stock_rate,
|
||||||
|
company=company,
|
||||||
|
submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify asset was capitalized
|
||||||
|
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||||
|
self.assertEqual(target_asset.net_purchase_amount, total_amount)
|
||||||
|
self.assertEqual(target_asset.status, "Work In Progress")
|
||||||
|
|
||||||
|
# Submit the capitalized asset
|
||||||
|
target_asset.submit()
|
||||||
|
self.assertEqual(target_asset.status, "Submitted")
|
||||||
|
|
||||||
|
# Split the asset - this should work without capitalization error
|
||||||
|
split_qty = 1
|
||||||
|
splitted_asset = split_asset(target_asset.name, split_qty)
|
||||||
|
|
||||||
|
# Verify split asset was created and submitted successfully
|
||||||
|
self.assertIsNotNone(splitted_asset)
|
||||||
|
self.assertEqual(splitted_asset.asset_quantity, split_qty)
|
||||||
|
self.assertEqual(splitted_asset.split_from, target_asset.name)
|
||||||
|
self.assertEqual(splitted_asset.docstatus, 1) # Should be submitted
|
||||||
|
self.assertEqual(splitted_asset.status, "Submitted")
|
||||||
|
|
||||||
|
# Verify original asset was updated
|
||||||
|
target_asset.reload()
|
||||||
|
self.assertEqual(target_asset.asset_quantity, 1) # Remaining quantity
|
||||||
|
|
||||||
|
|
||||||
def get_gl_entries(doctype, docname):
|
def get_gl_entries(doctype, docname):
|
||||||
gl_entry = frappe.qb.DocType("GL Entry")
|
gl_entry = frappe.qb.DocType("GL Entry")
|
||||||
@@ -1883,9 +2034,7 @@ def create_asset(**args):
|
|||||||
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
"available_for_use_date": args.available_for_use_date or "2020-06-06",
|
||||||
"location": args.location or "Test Location",
|
"location": args.location or "Test Location",
|
||||||
"asset_owner": args.asset_owner or "Company",
|
"asset_owner": args.asset_owner or "Company",
|
||||||
"is_existing_asset": args.is_existing_asset or 1,
|
"asset_type": args.asset_type or "Existing Asset",
|
||||||
"is_composite_asset": args.is_composite_asset or 0,
|
|
||||||
"is_composite_component": args.is_composite_component or 0,
|
|
||||||
"asset_quantity": args.get("asset_quantity") or 1,
|
"asset_quantity": args.get("asset_quantity") or 1,
|
||||||
"depr_entry_posting_status": args.depr_entry_posting_status or "",
|
"depr_entry_posting_status": args.depr_entry_posting_status or "",
|
||||||
}
|
}
|
||||||
@@ -1907,7 +2056,7 @@ def create_asset(**args):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if asset.is_composite_asset:
|
if asset.asset_type == "Composite Asset":
|
||||||
asset.net_purchase_amount = 0
|
asset.net_purchase_amount = 0
|
||||||
asset.purchase_amount = 0
|
asset.purchase_amount = 0
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
refresh() {
|
refresh() {
|
||||||
this.show_general_ledger();
|
this.show_general_ledger();
|
||||||
|
|
||||||
if (
|
if (this.frm.doc.stock_items && this.frm.doc.stock_items.length) {
|
||||||
(this.frm.doc.stock_items && this.frm.doc.stock_items.length) ||
|
|
||||||
!this.frm.doc.target_is_fixed_asset
|
|
||||||
) {
|
|
||||||
this.show_stock_ledger();
|
this.show_stock_ledger();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +38,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
|
|
||||||
me.frm.set_query("target_asset", function () {
|
me.frm.set_query("target_asset", function () {
|
||||||
return {
|
return {
|
||||||
filters: { is_composite_asset: 1, docstatus: 0 },
|
filters: { asset_type: "Composite Asset", docstatus: 0 },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -240,10 +237,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
this.calculate_totals();
|
this.calculate_totals();
|
||||||
}
|
}
|
||||||
|
|
||||||
target_qty() {
|
|
||||||
this.calculate_totals();
|
|
||||||
}
|
|
||||||
|
|
||||||
rate() {
|
rate() {
|
||||||
this.calculate_totals();
|
this.calculate_totals();
|
||||||
}
|
}
|
||||||
@@ -403,7 +396,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details",
|
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details",
|
||||||
child: item,
|
child: item,
|
||||||
args: {
|
args: {
|
||||||
args: {
|
ctx: {
|
||||||
item_code: item.item_code,
|
item_code: item.item_code,
|
||||||
warehouse: cstr(item.warehouse),
|
warehouse: cstr(item.warehouse),
|
||||||
qty: -1 * flt(item.stock_qty),
|
qty: -1 * flt(item.stock_qty),
|
||||||
@@ -485,10 +478,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
|||||||
me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total;
|
me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total;
|
||||||
me.frm.doc.total_value = flt(me.frm.doc.total_value, precision("total_value"));
|
me.frm.doc.total_value = flt(me.frm.doc.total_value, precision("total_value"));
|
||||||
|
|
||||||
me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision("target_qty"));
|
me.frm.doc.target_incoming_rate = me.frm.doc.total_value;
|
||||||
me.frm.doc.target_incoming_rate = me.frm.doc.target_qty
|
|
||||||
? me.frm.doc.total_value / flt(me.frm.doc.target_qty)
|
|
||||||
: me.frm.doc.total_value;
|
|
||||||
|
|
||||||
me.frm.refresh_fields();
|
me.frm.refresh_fields();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,30 +9,33 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"naming_series",
|
"naming_series",
|
||||||
|
"company",
|
||||||
"target_asset",
|
"target_asset",
|
||||||
"target_asset_name",
|
"target_asset_name",
|
||||||
"target_item_code",
|
|
||||||
"finance_book",
|
|
||||||
"target_qty",
|
|
||||||
"column_break_9",
|
"column_break_9",
|
||||||
"company",
|
"finance_book",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"posting_time",
|
"posting_time",
|
||||||
"set_posting_time",
|
"set_posting_time",
|
||||||
"target_batch_no",
|
"target_item_code",
|
||||||
"target_serial_no",
|
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"target_is_fixed_asset",
|
|
||||||
"target_has_batch_no",
|
|
||||||
"target_has_serial_no",
|
|
||||||
"section_break_16",
|
"section_break_16",
|
||||||
"stock_items",
|
"stock_items",
|
||||||
|
"section_break_urtz",
|
||||||
|
"column_break_gqep",
|
||||||
|
"column_break_yvlx",
|
||||||
"stock_items_total",
|
"stock_items_total",
|
||||||
"section_break_26",
|
"section_break_26",
|
||||||
"asset_items",
|
"asset_items",
|
||||||
|
"section_break_arbh",
|
||||||
|
"column_break_boeu",
|
||||||
|
"column_break_qecy",
|
||||||
"asset_items_total",
|
"asset_items_total",
|
||||||
"service_expenses_section",
|
"service_expenses_section",
|
||||||
"service_items",
|
"service_items",
|
||||||
|
"section_break_ptna",
|
||||||
|
"column_break_szvh",
|
||||||
|
"column_break_katv",
|
||||||
"service_items_total",
|
"service_items_total",
|
||||||
"totals_section",
|
"totals_section",
|
||||||
"total_value",
|
"total_value",
|
||||||
@@ -55,20 +58,12 @@
|
|||||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal)",
|
"depends_on": "eval:(doc.target_item_code && !doc.__islocal)",
|
||||||
"fieldname": "target_item_code",
|
"fieldname": "target_item_code",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"hidden": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Target Item Code",
|
"label": "Target Item Code",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fetch_from": "target_item_code.is_fixed_asset",
|
|
||||||
"fieldname": "target_is_fixed_asset",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Target Is Fixed Asset",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "target_asset",
|
"fieldname": "target_asset",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -143,6 +138,7 @@
|
|||||||
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
|
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
|
||||||
"fieldname": "section_break_16",
|
"fieldname": "section_break_16",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1,
|
||||||
"label": "Consumed Stock Items"
|
"label": "Consumed Stock Items"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -151,49 +147,11 @@
|
|||||||
"label": "Stock Items",
|
"label": "Stock Items",
|
||||||
"options": "Asset Capitalization Stock Item"
|
"options": "Asset Capitalization Stock Item"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "target_has_batch_no",
|
|
||||||
"fieldname": "target_batch_no",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Target Batch No",
|
|
||||||
"options": "Batch"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"fieldname": "target_qty",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Target Qty",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fetch_from": "target_item_code.has_batch_no",
|
|
||||||
"fieldname": "target_has_batch_no",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Target Has Batch No",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fetch_from": "target_item_code.has_serial_no",
|
|
||||||
"fieldname": "target_has_serial_no",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Target Has Serial No",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "target_has_serial_no",
|
|
||||||
"fieldname": "target_serial_no",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Target Serial No"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)",
|
"depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)",
|
||||||
"fieldname": "section_break_26",
|
"fieldname": "section_break_26",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1,
|
||||||
"label": "Consumed Assets"
|
"label": "Consumed Assets"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -203,6 +161,7 @@
|
|||||||
"options": "Asset Capitalization Asset Item"
|
"options": "Asset Capitalization Asset Item"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.stock_items_total",
|
||||||
"fieldname": "stock_items_total",
|
"fieldname": "stock_items_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Consumed Stock Total Value",
|
"label": "Consumed Stock Total Value",
|
||||||
@@ -210,6 +169,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.asset_items_total",
|
||||||
"fieldname": "asset_items_total",
|
"fieldname": "asset_items_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Consumed Asset Total Value",
|
"label": "Consumed Asset Total Value",
|
||||||
@@ -226,6 +186,7 @@
|
|||||||
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
|
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
|
||||||
"fieldname": "service_expenses_section",
|
"fieldname": "service_expenses_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1,
|
||||||
"label": "Service Expenses"
|
"label": "Service Expenses"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -235,6 +196,7 @@
|
|||||||
"options": "Asset Capitalization Service Item"
|
"options": "Asset Capitalization Service Item"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.service_items_total",
|
||||||
"fieldname": "service_items_total",
|
"fieldname": "service_items_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Service Expense Total Amount",
|
"label": "Service Expense Total Amount",
|
||||||
@@ -277,10 +239,10 @@
|
|||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project"
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
@@ -292,12 +254,48 @@
|
|||||||
"label": "Target Fixed Asset Account",
|
"label": "Target Fixed Asset Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_urtz",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_gqep",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_yvlx",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_arbh",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_boeu",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_qecy",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_ptna",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_szvh",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_katv",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-05-20 15:15:12.110035",
|
"modified": "2026-02-06 01:52:41.890713",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Capitalization",
|
"name": "Asset Capitalization",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
@@ -38,9 +39,6 @@ force_fields = [
|
|||||||
"target_asset_name",
|
"target_asset_name",
|
||||||
"item_name",
|
"item_name",
|
||||||
"asset_name",
|
"asset_name",
|
||||||
"target_is_fixed_asset",
|
|
||||||
"target_has_serial_no",
|
|
||||||
"target_has_batch_no",
|
|
||||||
"stock_uom",
|
"stock_uom",
|
||||||
"fixed_asset_account",
|
"fixed_asset_account",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
@@ -75,6 +73,7 @@ class AssetCapitalization(StockController):
|
|||||||
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
|
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
|
||||||
posting_date: DF.Date
|
posting_date: DF.Date
|
||||||
posting_time: DF.Time
|
posting_time: DF.Time
|
||||||
|
project: DF.Link | None
|
||||||
service_items: DF.Table[AssetCapitalizationServiceItem]
|
service_items: DF.Table[AssetCapitalizationServiceItem]
|
||||||
service_items_total: DF.Currency
|
service_items_total: DF.Currency
|
||||||
set_posting_time: DF.Check
|
set_posting_time: DF.Check
|
||||||
@@ -82,15 +81,9 @@ class AssetCapitalization(StockController):
|
|||||||
stock_items_total: DF.Currency
|
stock_items_total: DF.Currency
|
||||||
target_asset: DF.Link | None
|
target_asset: DF.Link | None
|
||||||
target_asset_name: DF.Data | None
|
target_asset_name: DF.Data | None
|
||||||
target_batch_no: DF.Link | None
|
|
||||||
target_fixed_asset_account: DF.Link | None
|
target_fixed_asset_account: DF.Link | None
|
||||||
target_has_batch_no: DF.Check
|
|
||||||
target_has_serial_no: DF.Check
|
|
||||||
target_incoming_rate: DF.Currency
|
target_incoming_rate: DF.Currency
|
||||||
target_is_fixed_asset: DF.Check
|
|
||||||
target_item_code: DF.Link | None
|
target_item_code: DF.Link | None
|
||||||
target_qty: DF.Float
|
|
||||||
target_serial_no: DF.SmallText | None
|
|
||||||
title: DF.Data | None
|
title: DF.Data | None
|
||||||
total_value: DF.Currency
|
total_value: DF.Currency
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
@@ -189,22 +182,13 @@ class AssetCapitalization(StockController):
|
|||||||
if not target_item.is_fixed_asset:
|
if not target_item.is_fixed_asset:
|
||||||
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
|
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
|
||||||
|
|
||||||
if target_item.is_fixed_asset:
|
|
||||||
self.target_qty = 1
|
|
||||||
if flt(self.target_qty) <= 0:
|
|
||||||
frappe.throw(_("Target Qty must be a positive number"))
|
|
||||||
if not target_item.has_batch_no:
|
|
||||||
self.target_batch_no = None
|
|
||||||
if not target_item.has_serial_no:
|
|
||||||
self.target_serial_no = ""
|
|
||||||
|
|
||||||
self.validate_item(target_item)
|
self.validate_item(target_item)
|
||||||
|
|
||||||
def validate_target_asset(self):
|
def validate_target_asset(self):
|
||||||
if self.target_asset:
|
if self.target_asset:
|
||||||
target_asset = self.get_asset_for_validation(self.target_asset)
|
target_asset = self.get_asset_for_validation(self.target_asset)
|
||||||
|
|
||||||
if not target_asset.is_composite_asset:
|
if not target_asset.asset_type == "Composite Asset":
|
||||||
frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name))
|
frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name))
|
||||||
|
|
||||||
if target_asset.item_code != self.target_item_code:
|
if target_asset.item_code != self.target_item_code:
|
||||||
@@ -313,7 +297,7 @@ class AssetCapitalization(StockController):
|
|||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Asset",
|
"Asset",
|
||||||
asset,
|
asset,
|
||||||
["name", "item_code", "company", "status", "docstatus", "is_composite_asset"],
|
["name", "item_code", "company", "status", "docstatus", "asset_type"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -379,8 +363,7 @@ class AssetCapitalization(StockController):
|
|||||||
self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total
|
self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total
|
||||||
self.total_value = flt(self.total_value, self.precision("total_value"))
|
self.total_value = flt(self.total_value, self.precision("total_value"))
|
||||||
|
|
||||||
self.target_qty = flt(self.target_qty, self.precision("target_qty"))
|
self.target_incoming_rate = self.total_value
|
||||||
self.target_incoming_rate = self.total_value / self.target_qty
|
|
||||||
|
|
||||||
def update_stock_ledger(self):
|
def update_stock_ledger(self):
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
@@ -488,7 +471,7 @@ class AssetCapitalization(StockController):
|
|||||||
for item in self.asset_items:
|
for item in self.asset_items:
|
||||||
asset = frappe.get_doc("Asset", item.asset)
|
asset = frappe.get_doc("Asset", item.asset)
|
||||||
|
|
||||||
if not asset.is_composite_component:
|
if asset.asset_type != "Composite Component":
|
||||||
if asset.calculate_depreciation:
|
if asset.calculate_depreciation:
|
||||||
notes = _(
|
notes = _(
|
||||||
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
"This schedule was created when Asset {0} was consumed through Asset Capitalization {1}."
|
||||||
@@ -541,30 +524,29 @@ class AssetCapitalization(StockController):
|
|||||||
def get_composite_component_value(self):
|
def get_composite_component_value(self):
|
||||||
composite_component_value = 0
|
composite_component_value = 0
|
||||||
for item in self.asset_items:
|
for item in self.asset_items:
|
||||||
asset = frappe.db.get_value("Asset", item.asset, ["is_composite_component"], as_dict=True)
|
asset = frappe.db.get_value("Asset", item.asset, ["asset_type"], as_dict=True)
|
||||||
if asset and asset.is_composite_component:
|
if asset and asset.asset_type == "Composite Component":
|
||||||
composite_component_value += flt(item.asset_value, item.precision("asset_value"))
|
composite_component_value += flt(item.asset_value, item.precision("asset_value"))
|
||||||
return composite_component_value
|
return composite_component_value
|
||||||
|
|
||||||
def get_gl_entries_for_target_item(
|
def get_gl_entries_for_target_item(
|
||||||
self, gl_entries, target_account, target_against, precision, composite_component_value
|
self, gl_entries, target_account, target_against, precision, composite_component_value
|
||||||
):
|
):
|
||||||
if self.target_is_fixed_asset:
|
total_value = flt(self.total_value - composite_component_value, precision)
|
||||||
total_value = flt(self.total_value - composite_component_value, precision)
|
if total_value:
|
||||||
if total_value:
|
# Capitalization
|
||||||
# Capitalization
|
gl_entries.append(
|
||||||
gl_entries.append(
|
self.get_gl_dict(
|
||||||
self.get_gl_dict(
|
{
|
||||||
{
|
"account": target_account,
|
||||||
"account": target_account,
|
"against": ", ".join(target_against),
|
||||||
"against": ", ".join(target_against),
|
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
"debit": total_value,
|
||||||
"debit": total_value,
|
"cost_center": self.get("cost_center"),
|
||||||
"cost_center": self.get("cost_center"),
|
},
|
||||||
},
|
item=self,
|
||||||
item=self,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def update_target_asset(self):
|
def update_target_asset(self):
|
||||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||||
@@ -573,13 +555,19 @@ class AssetCapitalization(StockController):
|
|||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value
|
net_purchase_amount = asset_doc.net_purchase_amount - total_target_asset_value
|
||||||
purchase_amount = asset_doc.purchase_amount - total_target_asset_value
|
purchase_amount = asset_doc.purchase_amount - total_target_asset_value
|
||||||
asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value)
|
total_asset_cost = asset_doc.total_asset_cost - total_target_asset_value
|
||||||
else:
|
else:
|
||||||
net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value
|
net_purchase_amount = asset_doc.net_purchase_amount + total_target_asset_value
|
||||||
purchase_amount = asset_doc.purchase_amount + total_target_asset_value
|
purchase_amount = asset_doc.purchase_amount + total_target_asset_value
|
||||||
|
total_asset_cost = asset_doc.total_asset_cost + total_target_asset_value
|
||||||
|
|
||||||
asset_doc.db_set("net_purchase_amount", net_purchase_amount)
|
asset_doc.db_set(
|
||||||
asset_doc.db_set("purchase_amount", purchase_amount)
|
{
|
||||||
|
"net_purchase_amount": net_purchase_amount,
|
||||||
|
"purchase_amount": purchase_amount,
|
||||||
|
"total_asset_cost": total_asset_cost,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(
|
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(
|
||||||
@@ -604,14 +592,13 @@ class AssetCapitalization(StockController):
|
|||||||
|
|
||||||
def set_consumed_asset_status(self, asset):
|
def set_consumed_asset_status(self, asset):
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
if self.target_is_fixed_asset:
|
asset.set_status("Capitalized")
|
||||||
asset.set_status("Capitalized")
|
add_asset_activity(
|
||||||
add_asset_activity(
|
asset.name,
|
||||||
asset.name,
|
_("Asset capitalized after Asset Capitalization {0} was submitted").format(
|
||||||
_("Asset capitalized after Asset Capitalization {0} was submitted").format(
|
get_link_to_form("Asset Capitalization", self.name)
|
||||||
get_link_to_form("Asset Capitalization", self.name)
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
asset.set_status()
|
asset.set_status()
|
||||||
add_asset_activity(
|
add_asset_activity(
|
||||||
@@ -623,7 +610,7 @@ class AssetCapitalization(StockController):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_target_item_details(item_code=None, company=None):
|
def get_target_item_details(item_code: str | None = None, company: str | None = None):
|
||||||
out = frappe._dict()
|
out = frappe._dict()
|
||||||
|
|
||||||
# Get Item Details
|
# Get Item Details
|
||||||
@@ -633,17 +620,6 @@ def get_target_item_details(item_code=None, company=None):
|
|||||||
|
|
||||||
# Set Item Details
|
# Set Item Details
|
||||||
out.target_item_name = item.item_name
|
out.target_item_name = item.item_name
|
||||||
out.target_is_fixed_asset = cint(item.is_fixed_asset)
|
|
||||||
out.target_has_batch_no = cint(item.has_batch_no)
|
|
||||||
out.target_has_serial_no = cint(item.has_serial_no)
|
|
||||||
|
|
||||||
if out.target_is_fixed_asset:
|
|
||||||
out.target_qty = 1
|
|
||||||
|
|
||||||
if not out.target_has_batch_no:
|
|
||||||
out.target_batch_no = None
|
|
||||||
if not out.target_has_serial_no:
|
|
||||||
out.target_serial_no = ""
|
|
||||||
|
|
||||||
# Cost Center
|
# Cost Center
|
||||||
item_defaults = get_item_defaults(item.name, company)
|
item_defaults = get_item_defaults(item.name, company)
|
||||||
@@ -660,7 +636,7 @@ def get_target_item_details(item_code=None, company=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_target_asset_details(asset=None, company=None):
|
def get_target_asset_details(asset: str | None = None, company: str | None = None):
|
||||||
out = frappe._dict()
|
out = frappe._dict()
|
||||||
|
|
||||||
# Get Asset Details
|
# Get Asset Details
|
||||||
@@ -735,24 +711,22 @@ def get_consumed_stock_item_details(ctx: ItemDetailsCtx):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_warehouse_details(args):
|
@erpnext.normalize_ctx_input(ItemDetailsCtx)
|
||||||
if isinstance(args, str):
|
def get_warehouse_details(ctx: ItemDetailsCtx) -> frappe._dict:
|
||||||
args = json.loads(args)
|
out = frappe._dict()
|
||||||
|
if ctx.warehouse and ctx.item_code:
|
||||||
args = frappe._dict(args)
|
out = frappe._dict(
|
||||||
|
{
|
||||||
out = {}
|
"actual_qty": get_previous_sle(ctx).get("qty_after_transaction") or 0,
|
||||||
if args.warehouse and args.item_code:
|
"valuation_rate": get_incoming_rate(ctx, raise_error_if_no_rate=False),
|
||||||
out = {
|
}
|
||||||
"actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0,
|
)
|
||||||
"valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False),
|
|
||||||
}
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@erpnext.normalize_ctx_input(ItemDetailsCtx)
|
@erpnext.normalize_ctx_input(ItemDetailsCtx)
|
||||||
def get_consumed_asset_details(ctx):
|
def get_consumed_asset_details(ctx: ItemDetailsCtx) -> frappe._dict:
|
||||||
out = frappe._dict()
|
out = frappe._dict()
|
||||||
|
|
||||||
asset_details = frappe._dict()
|
asset_details = frappe._dict()
|
||||||
@@ -798,7 +772,7 @@ def get_consumed_asset_details(ctx):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@erpnext.normalize_ctx_input(ItemDetailsCtx)
|
@erpnext.normalize_ctx_input(ItemDetailsCtx)
|
||||||
def get_service_item_details(ctx):
|
def get_service_item_details(ctx: ItemDetailsCtx) -> frappe._dict:
|
||||||
out = frappe._dict()
|
out = frappe._dict()
|
||||||
|
|
||||||
item = frappe._dict()
|
item = frappe._dict()
|
||||||
@@ -820,7 +794,7 @@ def get_service_item_details(ctx):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_items_tagged_to_wip_composite_asset(params):
|
def get_items_tagged_to_wip_composite_asset(params: dict | str):
|
||||||
if isinstance(params, str):
|
if isinstance(params, str):
|
||||||
params = json.loads(params)
|
params = json.loads(params)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
|||||||
from erpnext.assets.doctype.asset.test_asset import (
|
from erpnext.assets.doctype.asset.test_asset import (
|
||||||
create_asset,
|
create_asset,
|
||||||
create_asset_data,
|
create_asset_data,
|
||||||
|
create_fixed_asset_item,
|
||||||
set_depreciation_settings_in_company,
|
set_depreciation_settings_in_company,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||||
make_serial_batch_bundle,
|
make_serial_batch_bundle,
|
||||||
)
|
)
|
||||||
@@ -57,7 +59,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
|
|
||||||
wip_composite_asset = create_asset(
|
wip_composite_asset = create_asset(
|
||||||
asset_name="Asset Capitalization WIP Composite Asset",
|
asset_name="Asset Capitalization WIP Composite Asset",
|
||||||
is_composite_asset=1,
|
asset_type="Composite Asset",
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
company=company,
|
company=company,
|
||||||
)
|
)
|
||||||
@@ -77,7 +79,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Test Asset Capitalization values
|
# Test Asset Capitalization values
|
||||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
|
||||||
|
|
||||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||||
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
||||||
@@ -152,7 +153,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
|
|
||||||
wip_composite_asset = create_asset(
|
wip_composite_asset = create_asset(
|
||||||
asset_name="Asset Capitalization WIP Composite Asset",
|
asset_name="Asset Capitalization WIP Composite Asset",
|
||||||
is_composite_asset=1,
|
asset_type="Composite Asset",
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
company=company,
|
company=company,
|
||||||
)
|
)
|
||||||
@@ -172,8 +173,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Test Asset Capitalization values
|
# Test Asset Capitalization values
|
||||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
|
||||||
|
|
||||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||||
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
||||||
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
||||||
@@ -241,7 +240,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
|
|
||||||
wip_composite_asset = create_asset(
|
wip_composite_asset = create_asset(
|
||||||
asset_name="Asset Capitalization WIP Composite Asset",
|
asset_name="Asset Capitalization WIP Composite Asset",
|
||||||
is_composite_asset=1,
|
asset_type="Composite Asset",
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
company=company,
|
company=company,
|
||||||
)
|
)
|
||||||
@@ -258,8 +257,6 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Test Asset Capitalization values
|
# Test Asset Capitalization values
|
||||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
|
||||||
|
|
||||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||||
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
||||||
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
||||||
@@ -309,7 +306,7 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
|
|
||||||
wip_composite_asset = create_asset(
|
wip_composite_asset = create_asset(
|
||||||
asset_name="Asset Capitalization WIP Composite Asset",
|
asset_name="Asset Capitalization WIP Composite Asset",
|
||||||
is_composite_asset=1,
|
asset_type="Composite Asset",
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
company=company,
|
company=company,
|
||||||
)
|
)
|
||||||
@@ -357,33 +354,45 @@ class TestAssetCapitalization(IntegrationTestCase):
|
|||||||
|
|
||||||
wip_composite_asset = create_asset(
|
wip_composite_asset = create_asset(
|
||||||
asset_name="Asset Capitalization WIP Composite Asset",
|
asset_name="Asset Capitalization WIP Composite Asset",
|
||||||
is_composite_asset=1,
|
asset_type="Composite Asset",
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
company=company,
|
company=company,
|
||||||
)
|
)
|
||||||
|
|
||||||
consumed_asset_value = 100000
|
consumed_asset_value = 100000
|
||||||
|
|
||||||
consumed_asset = create_asset(
|
item = create_fixed_asset_item("Asset Capitalization Consumable Asset")
|
||||||
asset_name="Asset Capitalization Consumable Asset",
|
|
||||||
asset_value=consumed_asset_value,
|
pr = make_purchase_receipt(
|
||||||
submit=1,
|
item_code=item.item_code,
|
||||||
warehouse="Stores - _TC",
|
qty=1,
|
||||||
is_composite_component=1,
|
rate=consumed_asset_value,
|
||||||
company=company,
|
company=company,
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
)
|
)
|
||||||
|
consumed_asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||||
|
consumed_asset_doc = frappe.get_doc("Asset", consumed_asset_name)
|
||||||
|
|
||||||
|
consumed_asset_doc.update(
|
||||||
|
{
|
||||||
|
"asset_type": "Composite Component",
|
||||||
|
"purchase_date": pr.posting_date,
|
||||||
|
"available_for_use_date": pr.posting_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
consumed_asset_doc.save()
|
||||||
|
consumed_asset_doc.submit()
|
||||||
|
|
||||||
# Create and submit Asset Captitalization
|
# Create and submit Asset Captitalization
|
||||||
asset_capitalization = create_asset_capitalization(
|
asset_capitalization = create_asset_capitalization(
|
||||||
target_asset=wip_composite_asset.name,
|
target_asset=wip_composite_asset.name,
|
||||||
target_asset_location="Test Location",
|
target_asset_location="Test Location",
|
||||||
consumed_asset=consumed_asset.name,
|
consumed_asset=consumed_asset_doc.name,
|
||||||
company=company,
|
company=company,
|
||||||
submit=1,
|
submit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test Asset Capitalization values
|
# Test Asset Capitalization values
|
||||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
|
||||||
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
|
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
|
||||||
|
|
||||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||||
@@ -417,9 +426,6 @@ def create_asset_capitalization(**args):
|
|||||||
"target_item_code": target_item_code,
|
"target_item_code": target_item_code,
|
||||||
"target_asset": target_asset.name,
|
"target_asset": target_asset.name,
|
||||||
"target_asset_location": "Test Location",
|
"target_asset_location": "Test Location",
|
||||||
"target_qty": flt(args.target_qty) or 1,
|
|
||||||
"target_batch_no": args.target_batch_no,
|
|
||||||
"target_serial_no": args.target_serial_no,
|
|
||||||
"finance_book": args.finance_book,
|
"finance_book": args.finance_book,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -512,7 +518,7 @@ def create_depreciation_asset(**args):
|
|||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
asset = frappe.new_doc("Asset")
|
asset = frappe.new_doc("Asset")
|
||||||
asset.is_existing_asset = 1
|
asset.asset_type = args.asset_type or "Existing Asset"
|
||||||
asset.calculate_depreciation = 1
|
asset.calculate_depreciation = 1
|
||||||
asset.asset_owner = "Company"
|
asset.asset_owner = "Company"
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class AssetCategory(Document):
|
|||||||
self.validate_finance_books()
|
self.validate_finance_books()
|
||||||
self.validate_account_types()
|
self.validate_account_types()
|
||||||
self.validate_account_currency()
|
self.validate_account_currency()
|
||||||
self.valide_cwip_account()
|
self.validate_accounts()
|
||||||
|
|
||||||
def validate_finance_books(self):
|
def validate_finance_books(self):
|
||||||
for d in self.finance_books:
|
for d in self.finance_books:
|
||||||
@@ -97,11 +97,21 @@ class AssetCategory(Document):
|
|||||||
title=_("Invalid Account"),
|
title=_("Invalid Account"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def valide_cwip_account(self):
|
def validate_accounts(self):
|
||||||
|
self.validate_duplicate_rows()
|
||||||
|
self.validate_cwip_accounts()
|
||||||
|
self.validate_depreciation_accounts()
|
||||||
|
|
||||||
|
def validate_duplicate_rows(self):
|
||||||
|
companies = {row.company_name for row in self.accounts}
|
||||||
|
if len(companies) != len(self.accounts):
|
||||||
|
frappe.throw(_("Cannot set multiple account rows for the same company"))
|
||||||
|
|
||||||
|
def validate_cwip_accounts(self):
|
||||||
if self.enable_cwip_accounting:
|
if self.enable_cwip_accounting:
|
||||||
missing_cwip_accounts_for_company = []
|
missing_cwip_accounts_for_company = []
|
||||||
for d in self.accounts:
|
for d in self.accounts:
|
||||||
if not d.capital_work_in_progress_account and not frappe.db.get_value(
|
if not d.capital_work_in_progress_account and not frappe.get_cached_value(
|
||||||
"Company", d.company_name, "capital_work_in_progress_account"
|
"Company", d.company_name, "capital_work_in_progress_account"
|
||||||
):
|
):
|
||||||
missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
|
missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
|
||||||
@@ -115,6 +125,71 @@ class AssetCategory(Document):
|
|||||||
)
|
)
|
||||||
frappe.throw(msg, title=_("Missing Account"))
|
frappe.throw(msg, title=_("Missing Account"))
|
||||||
|
|
||||||
|
def validate_depreciation_accounts(self):
|
||||||
|
depreciation_account_map = {
|
||||||
|
"accumulated_depreciation_account": "Accumulated Depreciation Account",
|
||||||
|
"depreciation_expense_account": "Depreciation Expense Account",
|
||||||
|
}
|
||||||
|
|
||||||
|
error_msg = []
|
||||||
|
companies_with_accounts = set()
|
||||||
|
|
||||||
|
def validate_company_accounts(company, acc_row=None):
|
||||||
|
default_accounts = frappe.get_cached_value(
|
||||||
|
"Company",
|
||||||
|
company,
|
||||||
|
["accumulated_depreciation_account", "depreciation_expense_account"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
for fieldname, label in depreciation_account_map.items():
|
||||||
|
row_value = acc_row.get(fieldname) if acc_row else None
|
||||||
|
if not row_value and not default_accounts.get(fieldname):
|
||||||
|
if acc_row:
|
||||||
|
error_msg.append(
|
||||||
|
_("Row #{0}: Missing <b>{1}</b> for company <b>{2}</b>.").format(
|
||||||
|
acc_row.idx,
|
||||||
|
label,
|
||||||
|
get_link_to_form("Company", company),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = _("Missing account configuration for company <b>{0}</b>.").format(
|
||||||
|
get_link_to_form("Company", company),
|
||||||
|
)
|
||||||
|
if msg not in error_msg:
|
||||||
|
error_msg.append(msg)
|
||||||
|
|
||||||
|
companies_with_assets = frappe.db.get_all(
|
||||||
|
"Asset",
|
||||||
|
{
|
||||||
|
"calculate_depreciation": 1,
|
||||||
|
"asset_category": self.name,
|
||||||
|
"status": ["in", ("Submitted", "Partially Depreciated")],
|
||||||
|
},
|
||||||
|
pluck="company",
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for acc_row in self.accounts:
|
||||||
|
companies_with_accounts.add(acc_row.company_name)
|
||||||
|
if acc_row.company_name in companies_with_assets:
|
||||||
|
validate_company_accounts(acc_row.company_name, acc_row)
|
||||||
|
|
||||||
|
for company in companies_with_assets:
|
||||||
|
if company not in companies_with_accounts:
|
||||||
|
validate_company_accounts(company)
|
||||||
|
|
||||||
|
if error_msg:
|
||||||
|
msg = _(
|
||||||
|
"Since there are active depreciable assets under this category, the following accounts are required. <br><br>"
|
||||||
|
)
|
||||||
|
msg += _(
|
||||||
|
"You can either configure default depreciation accounts in the Company or set the required accounts in the following rows: <br><br>"
|
||||||
|
)
|
||||||
|
msg += "<br>".join(error_msg)
|
||||||
|
|
||||||
|
frappe.throw(msg, title=_("Missing Accounts"))
|
||||||
|
|
||||||
|
|
||||||
def get_asset_category_account(
|
def get_asset_category_account(
|
||||||
fieldname, item=None, asset=None, account=None, asset_category=None, company=None
|
fieldname, item=None, asset=None, account=None, asset_category=None, company=None
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.tests import IntegrationTestCase
|
from frappe.tests import IntegrationTestCase
|
||||||
|
|
||||||
|
from erpnext.assets.doctype.asset.test_asset import create_asset
|
||||||
|
|
||||||
|
|
||||||
class TestAssetCategory(IntegrationTestCase):
|
class TestAssetCategory(IntegrationTestCase):
|
||||||
def test_mandatory_fields(self):
|
def test_mandatory_fields(self):
|
||||||
@@ -50,3 +52,67 @@ class TestAssetCategory(IntegrationTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, asset_category.insert)
|
self.assertRaises(frappe.ValidationError, asset_category.insert)
|
||||||
|
|
||||||
|
def test_duplicate_company_accounts(self):
|
||||||
|
asset_category = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Asset Category",
|
||||||
|
"asset_category_name": "Computers",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"company_name": "_Test Company",
|
||||||
|
"fixed_asset_account": "_Test Fixed Asset - _TC",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company_name": "_Test Company",
|
||||||
|
"fixed_asset_account": "_Test Fixed Asset - _TC",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with self.assertRaises(frappe.ValidationError) as err:
|
||||||
|
asset_category.save()
|
||||||
|
self.assertTrue("Cannot set multiple account rows for the same company" in str(err.exception))
|
||||||
|
|
||||||
|
def test_depreciation_accounts_required_for_existing_depreciable_assets(self):
|
||||||
|
asset = create_asset(
|
||||||
|
asset_category="Computers",
|
||||||
|
calculate_depreciation=1,
|
||||||
|
company="_Test Company",
|
||||||
|
submit=1,
|
||||||
|
)
|
||||||
|
company_acccount_depreciation = frappe.db.get_value(
|
||||||
|
"Company",
|
||||||
|
asset.company,
|
||||||
|
[
|
||||||
|
"accumulated_depreciation_account",
|
||||||
|
"depreciation_expense_account",
|
||||||
|
],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Company",
|
||||||
|
asset.company,
|
||||||
|
{
|
||||||
|
"accumulated_depreciation_account": "",
|
||||||
|
"depreciation_expense_account": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
asset_category = frappe.get_doc("Asset Category", asset.asset_category)
|
||||||
|
asset_category.enable_cwip_accounting = 0
|
||||||
|
for row in asset_category.accounts:
|
||||||
|
if row.company_name == asset.company and (
|
||||||
|
row.accumulated_depreciation_account or row.depreciation_expense_account
|
||||||
|
):
|
||||||
|
row.accumulated_depreciation_account = None
|
||||||
|
row.depreciation_expense_account = None
|
||||||
|
with self.assertRaises(frappe.ValidationError) as err:
|
||||||
|
asset_category.save()
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
"Since there are active depreciable assets under this category, the following accounts are required."
|
||||||
|
in str(err.exception)
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
frappe.db.set_value("Company", asset.company, company_acccount_depreciation)
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ def get_asset_shift_factors_map():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_depr_schedule(asset_name, status, finance_book=None):
|
def get_depr_schedule(asset_name: str, status: str, finance_book: str | None = None):
|
||||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book)
|
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book)
|
||||||
|
|
||||||
if not asset_depr_schedule_doc:
|
if not asset_depr_schedule_doc:
|
||||||
@@ -281,13 +281,13 @@ def get_depr_schedule(asset_name, status, finance_book=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_asset_depr_schedule_doc(asset_name, status=None, finance_book=None):
|
def get_asset_depr_schedule_doc(asset_name: str, status: str | None = None, finance_book: str | None = None):
|
||||||
asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
||||||
|
|
||||||
if not asset_depr_schedule:
|
if not asset_depr_schedule:
|
||||||
return
|
return
|
||||||
|
|
||||||
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule[0].name)
|
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule)
|
||||||
|
|
||||||
return asset_depr_schedule_doc
|
return asset_depr_schedule_doc
|
||||||
|
|
||||||
@@ -299,21 +299,23 @@ def get_asset_depr_schedule_name(asset_name, status=None, finance_book=None):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
if isinstance(status, str):
|
status_list = [status] if isinstance(status, str) else status
|
||||||
status = [status]
|
filters.append(["status", "in", status_list])
|
||||||
filters.append(["status", "in", status])
|
|
||||||
|
|
||||||
if finance_book:
|
finance_book_filter = (
|
||||||
filters.append(["finance_book", "=", finance_book])
|
["finance_book", "=", finance_book] if finance_book else ["finance_book", "is", "not set"]
|
||||||
else:
|
)
|
||||||
filters.append(["finance_book", "is", "not set"])
|
filters.append(finance_book_filter)
|
||||||
|
|
||||||
return frappe.get_all(
|
depreciation_schedules = frappe.get_all(
|
||||||
doctype="Asset Depreciation Schedule",
|
doctype="Asset Depreciation Schedule",
|
||||||
filters=filters,
|
filters=filters,
|
||||||
|
fields=["name"],
|
||||||
limit=1,
|
limit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return depreciation_schedules[0].name if depreciation_schedules else None
|
||||||
|
|
||||||
|
|
||||||
def is_first_day_of_the_month(date):
|
def is_first_day_of_the_month(date):
|
||||||
first_day_of_the_month = get_first_day(date)
|
first_day_of_the_month = get_first_day(date)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
|||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
depreciation_method="Straight Line",
|
depreciation_method="Straight Line",
|
||||||
available_for_use_date="2023-10-10",
|
available_for_use_date="2023-10-10",
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
opening_number_of_booked_depreciations=9,
|
opening_number_of_booked_depreciations=9,
|
||||||
opening_accumulated_depreciation=265,
|
opening_accumulated_depreciation=265,
|
||||||
depreciation_start_date="2024-07-31",
|
depreciation_start_date="2024-07-31",
|
||||||
@@ -127,7 +127,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
|||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
depreciation_method="Straight Line",
|
depreciation_method="Straight Line",
|
||||||
available_for_use_date="2023-10-10",
|
available_for_use_date="2023-10-10",
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
opening_number_of_booked_depreciations=9,
|
opening_number_of_booked_depreciations=9,
|
||||||
opening_accumulated_depreciation=265.30,
|
opening_accumulated_depreciation=265.30,
|
||||||
depreciation_start_date="2024-07-31",
|
depreciation_start_date="2024-07-31",
|
||||||
@@ -165,7 +165,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
|||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
depreciation_method="Straight Line",
|
depreciation_method="Straight Line",
|
||||||
available_for_use_date="2023-11-01",
|
available_for_use_date="2023-11-01",
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
opening_number_of_booked_depreciations=4,
|
opening_number_of_booked_depreciations=4,
|
||||||
opening_accumulated_depreciation=223.15,
|
opening_accumulated_depreciation=223.15,
|
||||||
depreciation_start_date="2024-12-31",
|
depreciation_start_date="2024-12-31",
|
||||||
@@ -529,7 +529,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
|||||||
depreciation_start_date="2023-03-31",
|
depreciation_start_date="2023-03-31",
|
||||||
frequency_of_depreciation=1,
|
frequency_of_depreciation=1,
|
||||||
total_number_of_depreciations=12,
|
total_number_of_depreciations=12,
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
opening_accumulated_depreciation=64.52,
|
opening_accumulated_depreciation=64.52,
|
||||||
opening_number_of_booked_depreciations=2,
|
opening_number_of_booked_depreciations=2,
|
||||||
submit=1,
|
submit=1,
|
||||||
@@ -851,7 +851,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
|||||||
depreciation_start_date="2023-03-31",
|
depreciation_start_date="2023-03-31",
|
||||||
frequency_of_depreciation=1,
|
frequency_of_depreciation=1,
|
||||||
total_number_of_depreciations=12,
|
total_number_of_depreciations=12,
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
opening_accumulated_depreciation=64.52,
|
opening_accumulated_depreciation=64.52,
|
||||||
opening_number_of_booked_depreciations=2,
|
opening_number_of_booked_depreciations=2,
|
||||||
submit=1,
|
submit=1,
|
||||||
@@ -925,7 +925,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
|||||||
depreciation_start_date="2021-12-31",
|
depreciation_start_date="2021-12-31",
|
||||||
frequency_of_depreciation=12,
|
frequency_of_depreciation=12,
|
||||||
total_number_of_depreciations=3,
|
total_number_of_depreciations=3,
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
submit=1,
|
submit=1,
|
||||||
)
|
)
|
||||||
post_depreciation_entries(date="2021-12-31")
|
post_depreciation_entries(date="2021-12-31")
|
||||||
@@ -1014,7 +1014,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
|||||||
depreciation_start_date="2021-12-31",
|
depreciation_start_date="2021-12-31",
|
||||||
frequency_of_depreciation=12,
|
frequency_of_depreciation=12,
|
||||||
total_number_of_depreciations=3,
|
total_number_of_depreciations=3,
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
submit=1,
|
submit=1,
|
||||||
)
|
)
|
||||||
post_depreciation_entries(date="2021-12-31")
|
post_depreciation_entries(date="2021-12-31")
|
||||||
@@ -1093,7 +1093,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
|
|||||||
rate_of_depreciation=50,
|
rate_of_depreciation=50,
|
||||||
frequency_of_depreciation=12,
|
frequency_of_depreciation=12,
|
||||||
total_number_of_depreciations=3,
|
total_number_of_depreciations=3,
|
||||||
is_existing_asset=1,
|
asset_type="Existing Asset",
|
||||||
submit=1,
|
submit=1,
|
||||||
)
|
)
|
||||||
post_depreciation_entries(date="2021-12-31")
|
post_depreciation_entries(date="2021-12-31")
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, throw
|
from frappe import _, throw
|
||||||
from frappe.desk.form import assign_to
|
from frappe.desk.form import assign_to
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_days, add_months, add_years, getdate, nowdate
|
from frappe.utils import DateTimeLikeObject, add_days, add_months, add_years, getdate, nowdate
|
||||||
|
|
||||||
|
|
||||||
class AssetMaintenance(Document):
|
class AssetMaintenance(Document):
|
||||||
@@ -90,7 +92,11 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def calculate_next_due_date(
|
def calculate_next_due_date(
|
||||||
periodicity, start_date=None, end_date=None, last_completion_date=None, next_due_date=None
|
periodicity: str,
|
||||||
|
start_date: DateTimeLikeObject | None = None,
|
||||||
|
end_date: DateTimeLikeObject | None = None,
|
||||||
|
last_completion_date: DateTimeLikeObject | None = None,
|
||||||
|
next_due_date: DateTimeLikeObject | None = None,
|
||||||
):
|
):
|
||||||
if not start_date and not last_completion_date:
|
if not start_date and not last_completion_date:
|
||||||
start_date = frappe.utils.now()
|
start_date = frappe.utils.now()
|
||||||
@@ -164,19 +170,30 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_team_members(doctype, txt, searchfield, start, page_len, filters):
|
def get_team_members(
|
||||||
|
doctype: str,
|
||||||
|
txt: str,
|
||||||
|
searchfield: str,
|
||||||
|
start: int,
|
||||||
|
page_len: int,
|
||||||
|
filters: dict[str, Any],
|
||||||
|
) -> list[tuple[str]]:
|
||||||
return frappe.db.get_values(
|
return frappe.db.get_values(
|
||||||
"Maintenance Team Member", {"parent": filters.get("maintenance_team")}, "team_member"
|
"Maintenance Team Member",
|
||||||
|
{"parent": filters.get("maintenance_team")},
|
||||||
|
"team_member",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_maintenance_log(asset_name):
|
def get_maintenance_log(asset_name: str):
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select maintenance_status, count(asset_name) as count, asset_name
|
select maintenance_status, count(asset_name) as count, asset_name
|
||||||
from `tabAsset Maintenance Log`
|
from `tabAsset Maintenance Log`
|
||||||
where asset_name=%s group by maintenance_status""",
|
where asset_name=%s
|
||||||
(asset_name),
|
group by maintenance_status
|
||||||
|
""",
|
||||||
|
(asset_name,),
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr, get_link_to_form
|
from frappe.utils import cstr, get_datetime, get_link_to_form
|
||||||
|
|
||||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ class AssetMovement(Document):
|
|||||||
for d in self.assets:
|
for d in self.assets:
|
||||||
self.validate_asset(d)
|
self.validate_asset(d)
|
||||||
self.validate_movement(d)
|
self.validate_movement(d)
|
||||||
|
self.validate_transaction_date(d)
|
||||||
|
|
||||||
def validate_asset(self, d):
|
def validate_asset(self, d):
|
||||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||||
@@ -51,6 +52,18 @@ class AssetMovement(Document):
|
|||||||
else:
|
else:
|
||||||
self.validate_employee(d)
|
self.validate_employee(d)
|
||||||
|
|
||||||
|
def validate_transaction_date(self, d):
|
||||||
|
previous_movement_date = frappe.db.get_value(
|
||||||
|
"Asset Movement",
|
||||||
|
[["Asset Movement Item", "asset", "=", d.asset], ["docstatus", "=", 1]],
|
||||||
|
"transaction_date",
|
||||||
|
order_by="transaction_date desc",
|
||||||
|
)
|
||||||
|
if previous_movement_date and get_datetime(previous_movement_date) > get_datetime(
|
||||||
|
self.transaction_date
|
||||||
|
):
|
||||||
|
frappe.throw(_("Transaction date can't be earlier than previous movement date"))
|
||||||
|
|
||||||
def validate_location_and_employee(self, d):
|
def validate_location_and_employee(self, d):
|
||||||
self.validate_location(d)
|
self.validate_location(d)
|
||||||
self.validate_employee(d)
|
self.validate_employee(d)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests import IntegrationTestCase
|
from frappe.tests import IntegrationTestCase
|
||||||
from frappe.utils import now
|
from frappe.utils import add_days, now
|
||||||
|
|
||||||
from erpnext.assets.doctype.asset.test_asset import create_asset_data
|
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
|
||||||
from erpnext.setup.doctype.employee.test_employee import make_employee
|
from erpnext.setup.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
|
||||||
@@ -146,6 +146,33 @@ class TestAssetMovement(IntegrationTestCase):
|
|||||||
movement1.cancel()
|
movement1.cancel()
|
||||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
|
||||||
|
|
||||||
|
def test_movement_transaction_date(self):
|
||||||
|
asset = create_asset(item_code="Macbook Pro", do_not_save=1)
|
||||||
|
asset.save().submit()
|
||||||
|
|
||||||
|
if not frappe.db.exists("Location", "Test Location 2"):
|
||||||
|
frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
|
||||||
|
|
||||||
|
asset_creation_date = frappe.db.get_value(
|
||||||
|
"Asset Movement",
|
||||||
|
[["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]],
|
||||||
|
"transaction_date",
|
||||||
|
)
|
||||||
|
asset_movement = create_asset_movement(
|
||||||
|
purpose="Transfer",
|
||||||
|
company=asset.company,
|
||||||
|
assets=[
|
||||||
|
{
|
||||||
|
"asset": asset.name,
|
||||||
|
"source_location": "Test Location",
|
||||||
|
"target_location": "Test Location 2",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
transaction_date=add_days(asset_creation_date, -1),
|
||||||
|
do_not_save=True,
|
||||||
|
)
|
||||||
|
self.assertRaises(frappe.ValidationError, asset_movement.save)
|
||||||
|
|
||||||
|
|
||||||
def create_asset_movement(**args):
|
def create_asset_movement(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
@@ -164,9 +191,10 @@ def create_asset_movement(**args):
|
|||||||
"reference_name": args.reference_name,
|
"reference_name": args.reference_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if not args.do_not_save:
|
||||||
movement.insert()
|
movement.insert(ignore_if_duplicate=True)
|
||||||
movement.submit()
|
if not args.do_not_submit:
|
||||||
|
movement.submit()
|
||||||
|
|
||||||
return movement
|
return movement
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"naming_series",
|
"naming_series",
|
||||||
|
"company",
|
||||||
"asset",
|
"asset",
|
||||||
"asset_name",
|
"asset_name",
|
||||||
"company",
|
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"repair_status",
|
"repair_status",
|
||||||
"failure_date",
|
"failure_date",
|
||||||
@@ -28,10 +28,6 @@
|
|||||||
"column_break_ajbh",
|
"column_break_ajbh",
|
||||||
"column_break_hkem",
|
"column_break_hkem",
|
||||||
"repair_cost",
|
"repair_cost",
|
||||||
"accounting_dimensions_section",
|
|
||||||
"cost_center",
|
|
||||||
"column_break_14",
|
|
||||||
"project",
|
|
||||||
"stock_consumption_details_section",
|
"stock_consumption_details_section",
|
||||||
"stock_items",
|
"stock_items",
|
||||||
"section_break_ltbb",
|
"section_break_ltbb",
|
||||||
@@ -43,7 +39,12 @@
|
|||||||
"capitalize_repair_cost",
|
"capitalize_repair_cost",
|
||||||
"increase_in_asset_life",
|
"increase_in_asset_life",
|
||||||
"column_break_xebe",
|
"column_break_xebe",
|
||||||
"total_repair_cost"
|
"total_repair_cost",
|
||||||
|
"accounting_dimensions_section",
|
||||||
|
"cost_center",
|
||||||
|
"column_break_14",
|
||||||
|
"project",
|
||||||
|
"connection_tab"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -149,8 +150,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "accounting_details",
|
"fieldname": "accounting_details",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hide_border": 1,
|
"hide_border": 1
|
||||||
"label": "Repair Purchase Invoices"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "stock_items",
|
"fieldname": "stock_items",
|
||||||
@@ -206,6 +206,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "invoices",
|
"fieldname": "invoices",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
|
"label": "Repair Purchase Invoices",
|
||||||
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;",
|
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Asset Repair Purchase Invoice"
|
"options": "Asset Repair Purchase Invoice"
|
||||||
@@ -244,6 +245,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.consumed_items_cost",
|
||||||
"fieldname": "consumed_items_cost",
|
"fieldname": "consumed_items_cost",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Consumed Items Cost"
|
"label": "Consumed Items Cost"
|
||||||
@@ -256,7 +258,13 @@
|
|||||||
"depends_on": "capitalize_repair_cost",
|
"depends_on": "capitalize_repair_cost",
|
||||||
"fieldname": "accounting_dimensions_section",
|
"fieldname": "accounting_dimensions_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Accounting Dimensions"
|
"label": "Accounting Dimension"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "connection_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Connection",
|
||||||
|
"show_dashboard": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@@ -267,7 +275,7 @@
|
|||||||
"link_fieldname": "asset_repair"
|
"link_fieldname": "asset_repair"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2026-01-06 15:48:13.862505",
|
"modified": "2026-02-06 14:57:54.257572",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Repair",
|
"name": "Asset Repair",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder import DocType
|
from frappe.query_builder import DocType
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours
|
from frappe.utils import DateTimeLikeObject, cint, flt, get_link_to_form, getdate, time_diff_in_hours
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -448,14 +448,21 @@ class AssetRepair(AccountsController):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_downtime(failure_date, completion_date):
|
def get_downtime(failure_date: DateTimeLikeObject, completion_date: DateTimeLikeObject):
|
||||||
downtime = time_diff_in_hours(completion_date, failure_date)
|
downtime = time_diff_in_hours(completion_date, failure_date)
|
||||||
return round(downtime, 2)
|
return round(downtime, 2)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
|
def get_purchase_invoice(
|
||||||
|
doctype: str,
|
||||||
|
txt: str,
|
||||||
|
searchfield: str,
|
||||||
|
start: int,
|
||||||
|
page_len: int,
|
||||||
|
filters: dict,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Get Purchase Invoices that have expense accounts for non-stock items.
|
Get Purchase Invoices that have expense accounts for non-stock items.
|
||||||
Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account.
|
Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account.
|
||||||
@@ -490,7 +497,14 @@ def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters):
|
def get_expense_accounts(
|
||||||
|
doctype: str,
|
||||||
|
txt: str,
|
||||||
|
searchfield: str,
|
||||||
|
start: int,
|
||||||
|
page_len: int,
|
||||||
|
filters: dict,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Get expense accounts for non-stock (service) items from the purchase invoice.
|
Get expense accounts for non-stock (service) items from the purchase invoice.
|
||||||
Used as a query function for link fields.
|
Used as a query function for link fields.
|
||||||
@@ -548,7 +562,7 @@ def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[st
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_unallocated_repair_cost(
|
def get_unallocated_repair_cost(
|
||||||
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
|
purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None
|
||||||
) -> float:
|
):
|
||||||
"""
|
"""
|
||||||
Calculate the unused repair cost for a purchase invoice and expense account.
|
Calculate the unused repair cost for a purchase invoice and expense account.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -210,26 +210,29 @@ class TestAssetRepair(IntegrationTestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, asset_repair2.save)
|
self.assertRaises(frappe.ValidationError, asset_repair2.save)
|
||||||
|
|
||||||
def test_gl_entries_with_perpetual_inventory(self):
|
def test_gl_entries_with_perpetual_inventory(self):
|
||||||
set_depreciation_settings_in_company(company="_Test Company with perpetual inventory")
|
company = "_Test Company with perpetual inventory"
|
||||||
|
set_depreciation_settings_in_company(company)
|
||||||
|
|
||||||
asset_category = frappe.get_doc("Asset Category", "Computers")
|
asset_category = frappe.get_doc("Asset Category", "Computers")
|
||||||
asset_category.append(
|
|
||||||
"accounts",
|
if not any(row.company_name == company for row in asset_category.accounts):
|
||||||
{
|
asset_category.append(
|
||||||
"company_name": "_Test Company with perpetual inventory",
|
"accounts",
|
||||||
"fixed_asset_account": "_Test Fixed Asset - TCP1",
|
{
|
||||||
"accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1",
|
"company_name": company,
|
||||||
"depreciation_expense_account": "_Test Depreciations - TCP1",
|
"fixed_asset_account": "_Test Fixed Asset - TCP1",
|
||||||
"capital_work_in_progress_account": "CWIP Account - TCP1",
|
"accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1",
|
||||||
},
|
"depreciation_expense_account": "_Test Depreciations - TCP1",
|
||||||
)
|
"capital_work_in_progress_account": "CWIP Account - TCP1",
|
||||||
asset_category.save()
|
},
|
||||||
|
)
|
||||||
|
asset_category.save()
|
||||||
|
|
||||||
asset_repair = create_asset_repair(
|
asset_repair = create_asset_repair(
|
||||||
capitalize_repair_cost=1,
|
capitalize_repair_cost=1,
|
||||||
stock_consumption=1,
|
stock_consumption=1,
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
company="_Test Company with perpetual inventory",
|
company=company,
|
||||||
pi_expense_account1="Administrative Expenses - TCP1",
|
pi_expense_account1="Administrative Expenses - TCP1",
|
||||||
pi_expense_account2="Legal Expenses - TCP1",
|
pi_expense_account2="Legal Expenses - TCP1",
|
||||||
item="_Test Non Stock Item",
|
item="_Test Non Stock Item",
|
||||||
@@ -359,7 +362,7 @@ class TestAssetRepair(IntegrationTestCase):
|
|||||||
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
|
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
|
||||||
|
|
||||||
def test_gl_entries_with_capitalized_asset_repair(self):
|
def test_gl_entries_with_capitalized_asset_repair(self):
|
||||||
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
|
asset = create_asset(asset_type="Existing Asset", calculate_depreciation=1, submit=1)
|
||||||
asset_repair = create_asset_repair(
|
asset_repair = create_asset_repair(
|
||||||
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
|
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
|
||||||
)
|
)
|
||||||
@@ -399,7 +402,7 @@ def create_asset_repair(**args):
|
|||||||
if args.asset:
|
if args.asset:
|
||||||
asset = args.asset
|
asset = args.asset
|
||||||
else:
|
else:
|
||||||
asset = create_asset(is_existing_asset=1, submit=1, company=args.company)
|
asset = create_asset(asset_type=args.asset_type or "Existing Asset", submit=1, company=args.company)
|
||||||
asset_repair = frappe.new_doc("Asset Repair")
|
asset_repair = frappe.new_doc("Asset Repair")
|
||||||
asset_repair.update(
|
asset_repair.update(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -227,6 +227,6 @@ class AssetValueAdjustment(Document):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_value_of_accounting_dimensions(asset_name):
|
def get_value_of_accounting_dimensions(asset_name: str):
|
||||||
dimension_fields = [*frappe.get_list("Accounting Dimension", pluck="fieldname"), "cost_center"]
|
dimension_fields = [*frappe.get_list("Accounting Dimension", pluck="fieldname"), "cost_center"]
|
||||||
return frappe.db.get_value("Asset", asset_name, fieldname=dimension_fields, as_dict=True)
|
return frappe.db.get_value("Asset", asset_name, fieldname=dimension_fields, as_dict=True)
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ def _ring_area(coords):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_children(doctype, parent=None, location=None, is_root=False):
|
def get_children(doctype: str, parent: str | None = None, location: str | None = None, is_root: bool = False):
|
||||||
if parent is None or parent == "All Locations":
|
if parent is None or parent == "All Locations":
|
||||||
parent = ""
|
parent = ""
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ def get_conditions(filters):
|
|||||||
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
|
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
|
||||||
|
|
||||||
if filters.get("only_existing_assets"):
|
if filters.get("only_existing_assets"):
|
||||||
conditions["is_existing_asset"] = filters.get("only_existing_assets")
|
conditions["asset_type"] = "Existing Asset"
|
||||||
if filters.get("asset_category"):
|
if filters.get("asset_category"):
|
||||||
conditions["asset_category"] = filters.get("asset_category")
|
conditions["asset_category"] = filters.get("asset_category")
|
||||||
if filters.get("cost_center"):
|
if filters.get("cost_center"):
|
||||||
@@ -273,7 +273,7 @@ def get_asset_depreciation_amount_map(filters, finance_book):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if filters.only_existing_assets:
|
if filters.only_existing_assets:
|
||||||
query = query.where(asset.is_existing_asset == 1)
|
query = query.where(asset.asset_type == "Existing Asset")
|
||||||
if filters.asset_category:
|
if filters.asset_category:
|
||||||
query = query.where(asset.asset_category == filters.asset_category)
|
query = query.where(asset.asset_category == filters.asset_category)
|
||||||
if filters.cost_center:
|
if filters.cost_center:
|
||||||
@@ -324,7 +324,7 @@ def get_asset_value_adjustment_map(filters, finance_book):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if filters.only_existing_assets:
|
if filters.only_existing_assets:
|
||||||
query = query.where(asset.is_existing_asset == 1)
|
query = query.where(asset.asset_type == "Existing Asset")
|
||||||
if filters.asset_category:
|
if filters.asset_category:
|
||||||
query = query.where(asset.asset_category == filters.asset_category)
|
query = query.where(asset.asset_category == filters.asset_category)
|
||||||
if filters.cost_center:
|
if filters.cost_center:
|
||||||
|
|||||||
@@ -461,27 +461,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get_items_from_open_material_requests() {
|
|
||||||
erpnext.utils.map_current_doc({
|
|
||||||
method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order_based_on_supplier",
|
|
||||||
args: {
|
|
||||||
supplier: this.frm.doc.supplier,
|
|
||||||
},
|
|
||||||
source_doctype: "Material Request",
|
|
||||||
source_name: this.frm.doc.supplier,
|
|
||||||
target: this.frm,
|
|
||||||
setters: {
|
|
||||||
company: this.frm.doc.company,
|
|
||||||
},
|
|
||||||
get_query_filters: {
|
|
||||||
docstatus: ["!=", 2],
|
|
||||||
supplier: this.frm.doc.supplier,
|
|
||||||
},
|
|
||||||
get_query_method:
|
|
||||||
"erpnext.stock.doctype.material_request.material_request.get_material_requests_based_on_supplier",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
set_schedule_date(this.frm);
|
set_schedule_date(this.frm);
|
||||||
}
|
}
|
||||||
@@ -803,7 +782,7 @@ frappe.ui.form.on("Purchase Order", "is_subcontracted", function (frm) {
|
|||||||
|
|
||||||
function prevent_past_schedule_dates(frm) {
|
function prevent_past_schedule_dates(frm) {
|
||||||
if (frm.doc.transaction_date) {
|
if (frm.doc.transaction_date) {
|
||||||
frm.fields_dict["schedule_date"].datepicker.update({
|
frm.fields_dict["schedule_date"].datepicker?.update({
|
||||||
minDate: new Date(frm.doc.transaction_date),
|
minDate: new Date(frm.doc.transaction_date),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user