Compare commits

..

29 Commits

Author SHA1 Message Date
Khushi Rawat
d3cd887f5e Merge pull request #51666 from aerele/fix-asset-value-adjustment-cancel
fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled
2026-01-13 12:38:44 +05:30
Navin-S-R
d65cd605a1 fix: move validation to before_cancel 2026-01-13 12:16:52 +05:30
Ankush Menat
6ec41fa47e build: Bump dev version 2026-01-13 12:15:21 +05:30
Khushi Rawat
d879a91165 Merge pull request #51509 from khushi8112/fix-test-cases
fix: use system configured float precision for depreciation rate
2026-01-13 11:58:58 +05:30
Khushi Rawat
d21cfae095 Merge pull request #51363 from aerele/asset-partial-sales
fix(asset): handle partial asset sales by splitting remaining quantity
2026-01-13 11:54:20 +05:30
Mihir Kandoi
be5f2b6cf0 Merge pull request #51650 from mihir-kandoi/v16-prep 2026-01-13 11:06:02 +05:30
ruthra kumar
37b3a22825 Merge pull request #51412 from ljain112/fix-tds-customer
fix(tds): correct tax logic for customer
2026-01-13 11:05:31 +05:30
Mihir Kandoi
bb307dec0a chore: add v14
remove when EOL reached
2026-01-13 11:04:17 +05:30
Nabin Hait
3bc58fb46f fix: Redirect to Desktop after signup (#51696) 2026-01-12 19:20:27 +05:30
Navin-S-R
73b038084b fix: prevent manual cancellation of the linked Revaluation Journal Entry 2026-01-12 18:18:22 +05:30
Navin-S-R
eeb6d0e9bf fix: remove the redundant purchase receipt submit 2026-01-12 16:45:04 +05:30
Navin-S-R
ca97f34092 fix: use new_asset instead of asset_doc when checking values after splitting 2026-01-12 16:36:34 +05:30
Navin-S-R
500c44e3f5 fix: ignore permissions when cancelling revaluation journal entry 2026-01-11 21:30:09 +05:30
Navin-S-R
5f00239bba refactor(journal entry): replace raw SQL with query builder to unlink asset value adjustment 2026-01-11 19:25:22 +05:30
Navin-S-R
b1704ccef1 fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled 2026-01-11 19:20:01 +05:30
Mihir Kandoi
4987b2fe26 ci: ignore ci folder for tests 2026-01-10 17:01:04 +05:30
Mihir Kandoi
7e7e83440f ci: version 16 related changes 2026-01-10 16:43:39 +05:30
khushi8112
c0a85faa68 test: set up float precision 2026-01-06 23:49:21 +05:30
khushi8112
825e3717ca fix: do not update float precision on setup 2026-01-06 14:41:36 +05:30
khushi8112
007258d657 refactor: modify test cases to handle float precision rounded to 2 decimals 2026-01-06 14:39:31 +05:30
khushi8112
8d186d6b3f fix: use correct test class 2026-01-05 16:47:32 +05:30
khushi8112
1296829b9c fix(test): Use the system-configured float precision 2026-01-05 16:44:06 +05:30
ljain112
86b0f67dbc fix(tds): correct tax logic for customer 2025-12-31 14:26:22 +05:30
Navin-S-R
4adeaedfde test: validate asset split for auto created asset from purchase voucher 2025-12-30 16:29:46 +05:30
Navin-S-R
23b094f151 fix(asset): handle same asset being sold in multiple line items in sales invoice 2025-12-30 14:47:28 +05:30
Navin-S-R
e7e6567792 fix(asset): skip purchase document validation while splitting existing asset 2025-12-30 12:09:37 +05:30
Navin-S-R
9eeccb765d test: validate asset partial sales 2025-12-29 22:14:26 +05:30
Navin-S-R
a88fe2ecab fix: refactor older testcases 2025-12-29 15:53:31 +05:30
Navin-S-R
9a2710b9d7 fix(asset): handle partial asset sales by splitting remaining quantity 2025-12-29 15:40:46 +05:30
391 changed files with 5685 additions and 21841 deletions

View File

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

View File

@@ -11,6 +11,7 @@ on:
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
- '.github/**'
workflow_dispatch:
permissions:
@@ -113,8 +114,8 @@ jobs:
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
wget https://frappe.io/files/erpnext-v14.sql.gz
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
wget https://erpnext.com/files/v13-erpnext.sql.gz
bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
@@ -142,6 +143,7 @@ jobs:
bench --site test_site migrate
}
update_to_version 14 3.11
update_to_version 15 3.13
echo "Updating to latest version"

View File

@@ -2,7 +2,7 @@ name: Generate Semantic Release
on:
push:
branches:
- version-16
- version-13
permissions:
contents: read

View File

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

View File

@@ -13,6 +13,7 @@ on:
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
- '.github/**'
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"

View File

@@ -50,13 +50,13 @@ pull_request_rules:
- version-15-hotfix
assignees:
- "{{ author }}"
- name: backport to version-16-beta
- name: backport to version-16-hotfix
conditions:
- label="backport version-16-beta"
- label="backport version-16-hotfix"
actions:
backport:
branches:
- version-16-beta
- version-16-hotfix
assignees:
- "{{ author }}"
- name: Automatic merge on CI success and review

View File

@@ -1,5 +1,5 @@
{
"branches": ["version-16"],
"branches": ["version-13"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular",
@@ -21,4 +21,4 @@
],
"@semantic-release/github"
]
}
}

View File

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

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.7.3"
__version__ = "17.0.0-dev"
def get_default_company(user=None):

View File

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

View File

@@ -33,17 +33,6 @@
},
"account_number": "1151.000"
},
"Pajak Dibayar di Muka": {
"PPN Masukan": {
"account_number": "1152.001",
"account_type": "Tax"
},
"PPh 23 Dibayar di Muka": {
"account_number": "1152.002",
"account_type": "Tax"
},
"account_number": "1152.000"
},
"account_number": "1150.000"
},
"Kas": {
@@ -108,6 +97,17 @@
},
"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"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -139,8 +139,6 @@ class BankTransaction(Document):
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry"]
for payment_entry in self.payment_entries:
self.delink_payment_entry(payment_entry)
@@ -375,12 +373,11 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
("unallocated_amount", "bank_account"),
as_dict=True,
)
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
if bt_bank_account != gl_bank_account:
if bt.bank_account != gl_bank_account:
frappe.throw(
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
bt_bank_account, payment_entry.payment_entry, gl_bank_account
bt.bank_account, payment_entry.payment_entry, gl_bank_account
)
)

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
import frappe
from dateutil.relativedelta import relativedelta
from frappe import _, cint
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate
@@ -33,6 +33,24 @@ class FiscalYear(Document):
self.validate_dates()
self.validate_overlap()
if not self.is_new():
year_start_end_dates = frappe.db.sql(
"""select year_start_date, year_end_date
from `tabFiscal Year` where name=%s""",
(self.name),
)
if year_start_end_dates:
if (
getdate(self.year_start_date) != year_start_end_dates[0][0]
or getdate(self.year_end_date) != year_start_end_dates[0][1]
):
frappe.throw(
_(
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
)
)
def validate_dates(self):
self.validate_from_to_dates("year_start_date", "year_end_date")
if self.is_short_year:
@@ -48,20 +66,28 @@ class FiscalYear(Document):
frappe.exceptions.InvalidDates,
)
def on_update(self):
check_duplicate_fiscal_year(self)
frappe.cache().delete_value("fiscal_years")
def on_trash(self):
frappe.cache().delete_value("fiscal_years")
def validate_overlap(self):
fy = frappe.qb.DocType("Fiscal Year")
name = self.name or self.year
existing_fiscal_years = (
frappe.qb.from_(fy)
.select(fy.name)
.where(
(fy.year_start_date <= self.year_end_date)
& (fy.year_end_date >= self.year_start_date)
& (fy.name != name)
)
.run(as_dict=True)
existing_fiscal_years = frappe.db.sql(
"""select name from `tabFiscal Year`
where (
(%(year_start_date)s between year_start_date and year_end_date)
or (%(year_end_date)s between year_start_date and year_end_date)
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
) and name!=%(name)s""",
{
"year_start_date": self.year_start_date,
"year_end_date": self.year_end_date,
"name": self.name or "No Name",
},
as_dict=True,
)
if existing_fiscal_years:
@@ -84,30 +110,37 @@ class FiscalYear(Document):
frappe.throw(
_(
"Year start date or end date is overlapping with {0}. To avoid please set company"
).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
).format(existing.name),
frappe.NameError,
)
def auto_create_fiscal_year():
fy = frappe.qb.DocType("Fiscal Year")
# Skipped auto-creating Short Year, as it has very rare use case.
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
follow_up_date = add_days(getdate(), days=3)
fiscal_year = (
frappe.qb.from_(fy)
.select(fy.name)
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
.run()
@frappe.whitelist()
def check_duplicate_fiscal_year(doc):
year_start_end_dates = frappe.db.sql(
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
(doc.name),
)
for fiscal_year, ysd, yed in year_start_end_dates:
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
not frappe.in_test
):
frappe.throw(
_(
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
).format(fiscal_year)
)
for d in fiscal_year:
@frappe.whitelist()
def auto_create_fiscal_year():
for d in frappe.db.sql(
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
):
try:
current_fy = frappe.get_doc("Fiscal Year", d[0])
new_fy = frappe.new_doc("Fiscal Year")
new_fy.disabled = cint(current_fy.disabled)
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False)
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
@@ -115,10 +148,6 @@ def auto_create_fiscal_year():
start_year = cstr(new_fy.year_start_date.year)
end_year = cstr(new_fy.year_end_date.year)
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
for row in current_fy.companies:
new_fy.append("companies", {"company": row.company})
new_fy.auto_created = 1
new_fy.insert(ignore_permissions=True)

View File

@@ -15,22 +15,20 @@
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
"options": "Company"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-02-20 23:02:26.193606",
"modified": "2024-03-27 13:09:44.659251",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year Company",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link
company: DF.Link | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

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

View File

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

View File

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

View File

@@ -185,7 +185,7 @@
"fieldtype": "Select",
"label": "Reference Type",
"no_copy": 1,
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
"search_index": 1
},
{
@@ -198,7 +198,7 @@
"search_index": 1
},
{
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
"fieldname": "reference_due_date",
"fieldtype": "Date",
"label": "Reference Due Date",
@@ -294,7 +294,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-02-19 17:01:22.642454",
"modified": "2025-11-27 12:23:33.157655",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -55,7 +55,6 @@ class JournalEntryAccount(Document):
"Fees",
"Full and Final Statement",
"Payment Entry",
"Bank Transaction",
]
user_remark: DF.SmallText | None
# end: auto-generated types

View File

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

View File

@@ -3,7 +3,6 @@
import frappe
from frappe import _
from frappe.model.document import Document
@@ -43,29 +42,7 @@ class JournalEntryTemplate(Document):
]
# end: auto-generated types
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
)
)
pass
@frappe.whitelist()

View File

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

View File

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

View File

@@ -400,16 +400,6 @@ frappe.ui.form.on("Payment Entry", {
);
frm.refresh_fields();
const party_currency =
frm.doc.payment_type === "Receive" ? "paid_from_account_currency" : "paid_to_account_currency";
var reference_grid = frm.fields_dict["references"].grid;
["total_amount", "outstanding_amount", "allocated_amount"].forEach((fieldname) => {
reference_grid.update_docfield_property(fieldname, "options", party_currency);
});
reference_grid.refresh();
},
show_general_ledger: function (frm) {
@@ -512,16 +502,12 @@ frappe.ui.form.on("Payment Entry", {
frm.set_value("contact_email", "");
frm.set_value("contact_person", "");
}
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
if (!frm.doc.posting_date) {
frappe.msgprint(__("Please select Posting Date before selecting Party"));
frm.set_value("party", "");
return;
}
erpnext.utils.get_employee_contact_details(frm);
frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
@@ -1118,7 +1104,7 @@ frappe.ui.form.on("Payment Entry", {
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", {
paid_amount: flt(paid_amount),
paid_amount: paid_amount,
paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
});
@@ -1454,15 +1440,16 @@ frappe.ui.form.on("Payment Entry", {
callback: function (r) {
if (!r.exc && r.message) {
// set taxes table
let taxes = r.message;
taxes.forEach((tax) => {
if (tax.charge_type === "On Net Total") {
tax.charge_type = "On Paid Amount";
if (r.message) {
for (let tax of r.message) {
if (tax.charge_type === "On Net Total") {
tax.charge_type = "On Paid Amount";
}
frm.add_child("taxes", tax);
}
});
frm.set_value("taxes", taxes);
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
frm.events.apply_taxes(frm);
frm.events.set_unallocated_amount(frm);
}
}
},
});

View File

@@ -701,6 +701,7 @@
"fetch_from": "company.book_advance_payments_in_separate_party_account",
"fieldname": "book_advance_payments_in_separate_party_account",
"fieldtype": "Check",
"hidden": 1,
"label": "Book Advance Payments in Separate Party Account",
"no_copy": 1,
"read_only": 1
@@ -792,7 +793,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-02-03 16:08:49.800381",
"modified": "2025-12-18 13:56:40.206038",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -1082,32 +1082,20 @@ class PaymentEntry(AccountsController):
self.base_paid_amount + deductions_to_consider
):
self.unallocated_amount = (
flt(
(
self.base_paid_amount
+ deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
),
self.precision("unallocated_amount"),
)
/ self.source_exchange_rate
)
self.base_paid_amount
+ deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
) / self.source_exchange_rate
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
self.base_received_amount - deductions_to_consider
):
self.unallocated_amount = (
flt(
(
self.base_received_amount
- deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
),
self.precision("unallocated_amount"),
)
/ self.target_exchange_rate
)
self.base_received_amount
- deductions_to_consider
- self.base_total_allocated_amount
- included_taxes
) / self.target_exchange_rate
def set_exchange_gain_loss(self):
exchange_gain_loss = flt(

View File

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

View File

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

View File

@@ -746,7 +746,7 @@ class PaymentReconciliation(Document):
ple = qb.DocType("Payment Ledger Entry")
for x in self.dimensions:
dimension = x.fieldname
if self.get(dimension) and frappe.db.has_column("Payment Ledger Entry", dimension):
if 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):

View File

@@ -1,88 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-12-02 17:50:08.648006",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_term",
"column_break_lnjp",
"payment_schedule",
"section_break_fjhh",
"description",
"section_break_mjlv",
"due_date",
"column_break_qghl",
"amount"
],
"fields": [
{
"fieldname": "payment_term",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Payment Term",
"options": "Payment Term"
},
{
"collapsible": 1,
"fieldname": "section_break_fjhh",
"fieldtype": "Section Break",
"label": "Description"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"fieldname": "section_break_mjlv",
"fieldtype": "Section Break"
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Due Date"
},
{
"fieldname": "column_break_qghl",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"precision": "2"
},
{
"fieldname": "column_break_lnjp",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "payment_schedule",
"fieldtype": "Link",
"label": "Payment Schedule",
"options": "Payment Schedule",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-01-19 02:21:36.455830",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reference",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,27 +0,0 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PaymentReference(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Currency
description: DF.SmallText | None
due_date: DF.Date | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
payment_schedule: DF.Link | None
payment_term: DF.Link | None
# end: auto-generated types
pass

View File

@@ -105,29 +105,3 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
});
}
});
frappe.ui.form.on("Payment Request", "calculate_total_amount_by_selected_rows", function (frm) {
if (frm.doc.docstatus !== 0) {
frappe.msgprint(__("Cannot fetch selected rows for submitted Payment Request"));
return;
}
const selected = frm.get_selected()?.payment_reference || [];
if (!selected.length) {
frappe.throw(__("No rows selected"));
}
let total = 0;
selected.forEach((name) => {
const row = frm.doc.payment_reference.find((d) => d.name === name);
if (row) {
row.manually_selected = 1;
total += row.amount;
}
});
frm.doc.payment_reference.forEach((row) => {
row.auto_selected = 0;
});
frm.set_value("grand_total", total);
frm.refresh_field("grand_total");
frm.save();
});

View File

@@ -19,8 +19,6 @@
"column_break_4",
"reference_doctype",
"reference_name",
"payment_reference_section",
"payment_reference",
"transaction_details",
"grand_total",
"currency",
@@ -159,7 +157,6 @@
"label": "Amount",
"non_negative": 1,
"options": "currency",
"read_only_depends_on": "eval:doc.payment_reference.length>0",
"reqd": 1
},
{
@@ -460,17 +457,6 @@
"fieldname": "phone_number",
"fieldtype": "Data",
"label": "Phone Number"
},
{
"fieldname": "payment_reference_section",
"fieldtype": "Section Break"
},
{
"fieldname": "payment_reference",
"fieldtype": "Table",
"label": "Payment Reference",
"options": "Payment Reference",
"read_only": 1
}
],
"grid_page_length": 50,
@@ -478,7 +464,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-01-13 12:53:00.963274",
"modified": "2025-08-29 11:52:48.555415",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",

View File

@@ -45,7 +45,6 @@ class PaymentRequest(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.payment_reference.payment_reference import PaymentReference
from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import (
SubscriptionPlanDetail,
)
@@ -79,7 +78,6 @@ class PaymentRequest(Document):
payment_gateway: DF.ReadOnly | None
payment_gateway_account: DF.Link | None
payment_order: DF.Link | None
payment_reference: DF.Table[PaymentReference]
payment_request_type: DF.Literal["Outward", "Inward"]
payment_url: DF.Data | None
phone_number: DF.Data | None
@@ -111,36 +109,15 @@ class PaymentRequest(Document):
if self.get("__islocal"):
self.status = "Draft"
self.validate_reference_document()
self.validate_against_payment_reference()
self.validate_payment_request_amount()
# self.validate_currency()
self.validate_subscription_details()
def validate_against_payment_reference(self):
if not self.payment_reference:
return
expected = sum(flt(r.amount) for r in self.payment_reference)
if flt(expected, self.precision("grand_total")) != flt(self.grand_total):
frappe.throw(_("Grand Total must match sum of Payment References"))
seen = set()
for r in self.payment_reference:
if not r.payment_schedule:
continue # legacy mode → skip
if r.payment_schedule in seen:
frappe.throw(_("Duplicate Payment Schedule selected"))
seen.add(r.payment_schedule)
def validate_reference_document(self):
if not self.reference_doctype or not self.reference_name:
frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self):
if self.payment_reference:
return
if self.grand_total == 0:
frappe.throw(
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
@@ -558,7 +535,7 @@ class PaymentRequest(Document):
row_number += TO_SKIP_NEW_ROW
@frappe.whitelist()
@frappe.whitelist(allow_guest=True)
def make_payment_request(**args):
"""Make payment request"""
@@ -569,69 +546,12 @@ def make_payment_request(**args):
if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
frappe.has_permission("Payment Request", "create", throw=True)
frappe.has_permission(args.dt, "read", args.dn, throw=True)
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company
gateway_account = get_gateway_details(args) or frappe._dict()
# Schedule-based PRs are allowed only if no Payment Entry exists for this document.
# Any existing Payment Entry forces legacy (amount-based) flow.
selected_payment_schedules = json.loads(args.get("schedules")) if args.get("schedules") else []
# Backend guard:
# If any Payment Entry exists, schedule-based PRs are not allowed.
if selected_payment_schedules and get_existing_payment_entry(ref_doc.name):
frappe.throw(
_(
"Payment Schedule based Payment Requests cannot be created because a Payment Entry already exists for this document."
)
)
has_payment_entry = bool(get_existing_payment_entry(ref_doc.name))
payment_reference = []
if selected_payment_schedules:
existing_payment_references = get_existing_payment_references(ref_doc.name)
if existing_payment_references:
existing_ids = {r["payment_schedule"] for r in existing_payment_references}
selected_ids = {r["name"] for r in selected_payment_schedules}
duplicate_ids = existing_ids & selected_ids
if duplicate_ids:
duplicate_schedules = []
for row in selected_payment_schedules:
if row["name"] in duplicate_ids:
existing_ref = next(
(r for r in existing_payment_references if r["payment_schedule"] == row["name"]),
{},
)
existing_pr = existing_ref.get("parent")
duplicate_schedules.append(
f"Payment Term: {row.get('payment_term')}, "
f"Due Date: {row.get('due_date')}, "
f"Amount: {row.get('payment_amount')} "
f"(already requested in PR {existing_pr})"
)
frappe.throw(
_("The following payment schedule(s) already exist:\n{0}").format(
"\n".join(duplicate_schedules)
)
)
payment_reference = set_payment_references(args.get("schedules"))
# Determine grand_total
if selected_payment_schedules and not has_payment_entry:
grand_total = sum(row.get("payment_amount") for row in selected_payment_schedules)
else:
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if not grand_total:
frappe.throw(_("Payment Entry is already created"))
@@ -641,6 +561,7 @@ def make_payment_request(**args):
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points)) # sets fields on ref_doc
ref_doc.db_update()
grand_total = grand_total - loyalty_amount
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc)
@@ -660,20 +581,19 @@ def make_payment_request(**args):
else:
# If PR's are processed, cancel all of them.
cancel_old_payment_requests(ref_doc.doctype, ref_doc.name)
elif not selected_payment_schedules:
else:
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
draft_payment_request = frappe.db.get_value(
"Payment Request",
{"reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "docstatus": 0},
)
if draft_payment_request:
frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
)
pr = frappe.get_doc("Payment Request", draft_payment_request)
if selected_payment_schedules:
apply_payment_references(pr, payment_reference)
pr.save()
else:
bank_account = (
get_party_bank_account(args.get("party_type"), args.get("party"))
@@ -728,10 +648,7 @@ def make_payment_request(**args):
}
)
if selected_payment_schedules:
apply_payment_references(pr, payment_reference)
# Dimensions
# Update dimensions
pr.update(
{
"cost_center": ref_doc.get("cost_center"),
@@ -760,51 +677,6 @@ def make_payment_request(**args):
return pr.as_dict()
def apply_payment_references(pr, payment_reference):
existing_refs = pr.get("payment_reference") or []
existing_ids = {r.get("payment_schedule") for r in existing_refs if r.get("payment_schedule")}
new_refs = [r for r in (payment_reference or []) if r.get("payment_schedule") not in existing_ids]
pr.set("payment_reference", existing_refs + new_refs)
pr.set("grand_total", sum(flt(r.get("amount")) for r in pr.get("payment_reference")))
def set_payment_references(payment_schedules):
payment_schedules = json.loads(payment_schedules) if payment_schedules else []
payment_reference = []
for row in payment_schedules:
payment_reference.append(
{
"payment_term": row.get("payment_term"),
"payment_schedule": row.get("name"),
"description": row.get("description"),
"due_date": row.get("due_date"),
"amount": row.get("payment_amount"),
}
)
return payment_reference
def get_existing_payment_entry(ref_docname):
pe = frappe.qb.DocType("Payment Entry")
per = frappe.qb.DocType("Payment Entry Reference")
existing_pe = (
frappe.qb.from_(pe)
.join(per)
.on(per.parent == pe.name)
.select(pe.name)
.where(pe.docstatus < 2)
.where(per.reference_name == ref_docname)
.limit(1)
.run()
)
return existing_pe
def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype"""
grand_total = 0
@@ -947,7 +819,7 @@ def get_print_format_list(ref_doctype):
return {"print_format": print_format_list}
@frappe.whitelist()
@frappe.whitelist(allow_guest=True)
def resend_payment_email(docname):
return frappe.get_doc("Payment Request", docname).send_email()
@@ -1149,44 +1021,3 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
},
)
return res
@frappe.whitelist()
def get_available_payment_schedules(reference_doctype, reference_name):
ref_doc = frappe.get_doc(reference_doctype, reference_name)
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
return []
if get_existing_payment_entry(reference_name):
return []
existing_refs = get_existing_payment_references(reference_name)
existing_ids = {r["payment_schedule"] for r in existing_refs if r.get("payment_schedule")}
return [r for r in ref_doc.payment_schedule if r.name not in existing_ids]
def get_existing_payment_references(reference_name):
PR = frappe.qb.DocType("Payment Request")
PRF = frappe.qb.DocType("Payment Reference")
result = (
frappe.qb.from_(PR)
.join(PRF)
.on(PR.name == PRF.parent)
.select(
PRF.payment_term,
PRF.due_date,
PRF.amount.as_("payment_amount"),
PRF.payment_schedule,
PRF.parent,
)
.where(PR.reference_name == reference_name)
.where(PR.docstatus < 2)
.where(
PR.status.isin(["Draft", "Requested", "Initiated", "Partially Paid", "Payment Ordered", "Paid"])
)
).run(as_dict=True)
return result

View File

@@ -1,14 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import json
import re
import unittest
from unittest.mock import patch
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
@@ -853,130 +851,3 @@ class TestPaymentRequest(IntegrationTestCase):
pr.load_from_db()
self.assertEqual(pr.grand_total, pi.outstanding_amount)
def test_payment_request_grand_total_from_selected_schedules(self):
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 30})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 30})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 2), "payment_amount": 40})
po.save()
po.submit()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[0], po.payment_schedule[2]]
]
)
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
pr.submit()
self.assertEqual(pr.grand_total, 70)
self.assertEqual(len(pr.payment_reference), 2)
def test_draft_pr_reuse_merges_payment_references(self):
from frappe.utils import add_days, nowdate
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 50})
po.append("payment_schedule", {"due_date": add_days(nowdate(), 1), "payment_amount": 50})
po.save()
po.submit()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[0]]
]
)
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
pr.save()
schedules = json.dumps(
[
{
"payment_term": row.payment_term,
"name": row.name,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
for row in [po.payment_schedule[1]]
]
)
# call make_payment_request again → reuse draft
pr_reused = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)
self.assertEqual(pr.name, pr_reused.name)
self.assertEqual(pr_reused.grand_total, 100)
self.assertEqual(len(pr_reused.payment_reference), 2)
def test_schedule_pr_not_allowed_if_payment_entry_exists(self):
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=100)
po.payment_schedule = []
row = po.append("payment_schedule", {"due_date": nowdate(), "payment_amount": 100})
po.save()
po.submit()
# create PE first
pr = make_payment_request(dt="Purchase Order", dn=po.name, mute_email=1, submit_doc=1, return_doc=1)
pr.create_payment_entry()
schedules = json.dumps(
[
{
"name": row.name,
"payment_term": row.payment_term,
"due_date": row.due_date,
"payment_amount": row.payment_amount,
"description": row.description,
}
]
)
with self.assertRaises(frappe.ValidationError):
make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=False,
return_doc=True,
schedules=schedules,
)

View File

@@ -1610,14 +1610,13 @@
"hidden": 1,
"label": "Item Wise Tax Details",
"no_copy": 1,
"options": "Item Wise Tax Detail",
"print_hide": 1
"options": "Item Wise Tax Detail"
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-01-29 21:20:51.376875",
"modified": "2025-08-04 22:22:31.471752",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,19 +18,8 @@ class TestProcessStatementOfAccounts(AccountsTestMixin, IntegrationTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
letterhead.is_default = 0
letterhead.save()
cls.enterClassContext(cls.change_settings("Selling Settings", validate_selling_price=0))
@classmethod
def tearDownClass(cls):
super().tearDownClass()
letterhead = frappe.get_doc("Letter Head", "Company Letterhead - Grey")
letterhead.is_default = 1
letterhead.save()
frappe.db.commit() # nosemgrep
def setUp(self):
self.create_company()
self.create_customer()

View File

@@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
me.make_payment_request();
},
__("Create")
);

View File

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

View File

@@ -36,7 +36,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
update_billed_amount_based_on_po,
@@ -1745,6 +1745,10 @@ class PurchaseInvoice(BuyingController):
project_doc.db_update()
def validate_supplier_invoice(self):
if self.bill_date:
if getdate(self.bill_date) > getdate(self.posting_date):
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
if self.bill_no:
if cint(frappe.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
@@ -2001,17 +2005,9 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
args = json.loads(args)
def post_parent_process(source_parent, target_parent):
remove_items_with_zero_qty(target_parent)
set_missing_values(source_parent, target_parent)
def remove_items_with_zero_qty(target_parent):
target_parent.items = [row for row in target_parent.get("items") if row.get("qty") != 0]
def set_missing_values(source_parent, target_parent):
target_parent.run_method("set_missing_values")
if args and args.get("merge_taxes"):
merge_taxes(source_parent, target_parent)
target_parent.run_method("calculate_taxes_and_totals")
for row in target_parent.get("items"):
if row.get("qty") == 0:
target_parent.remove(row)
def update_item(obj, target, source_parent):
from erpnext.controllers.sales_and_purchase_return import get_returned_qty_map_for_row
@@ -2063,11 +2059,7 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
"reset_value": not (args and args.get("merge_taxes")),
"ignore": args.get("merge_taxes") if args else 0,
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
},
target_doc,
post_parent_process,

View File

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

View File

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

View File

@@ -7,12 +7,12 @@
"engine": "InnoDB",
"field_order": [
"customer_section",
"company",
"company_tax_id",
"naming_series",
"customer",
"customer_name",
"tax_id",
"company",
"company_tax_id",
"column_break1",
"posting_date",
"posting_time",
@@ -77,38 +77,34 @@
"base_total_taxes_and_charges",
"column_break_47",
"total_taxes_and_charges",
"totals_section",
"use_company_roundoff_cost_center",
"grand_total",
"in_words",
"column_break5",
"disable_rounded_total",
"rounding_adjustment",
"rounded_total",
"base_totals_section",
"totals",
"base_grand_total",
"base_in_words",
"column_break_xjag",
"base_rounding_adjustment",
"base_rounded_total",
"section_break_vacb",
"base_in_words",
"column_break5",
"grand_total",
"rounding_adjustment",
"use_company_roundoff_cost_center",
"rounded_total",
"in_words",
"total_advance",
"column_break_rdks",
"outstanding_amount",
"disable_rounded_total",
"section_tax_withholding_entry",
"tax_withholding_group",
"ignore_tax_withholding_threshold",
"override_tax_withholding_entries",
"tax_withholding_entries",
"additional_discount_section",
"section_break_49",
"apply_discount_on",
"base_discount_amount",
"coupon_code",
"is_cash_or_non_trade_discount",
"additional_discount_account",
"column_break_51",
"additional_discount_percentage",
"discount_amount",
"is_cash_or_non_trade_discount",
"additional_discount_account",
"sec_tax_breakup",
"other_charges_calculation",
"item_wise_tax_details",
@@ -198,13 +194,13 @@
"column_break8",
"unrealized_profit_loss_account",
"against_income_account",
"commission_section",
"sales_team_section_break",
"sales_partner",
"amount_eligible_for_commission",
"column_break10",
"commission_rate",
"total_commission",
"sales_team_section",
"section_break2",
"sales_team",
"edit_printing_settings",
"letter_head",
@@ -221,7 +217,8 @@
"update_auto_repeat_reference",
"more_information",
"status",
"remarks",
"inter_company_invoice_reference",
"represents_company",
"customer_group",
"column_break_imbx",
"utm_source",
@@ -230,9 +227,8 @@
"utm_content",
"col_break23",
"is_internal_customer",
"represents_company",
"inter_company_invoice_reference",
"is_discounted",
"remarks",
"connections_tab"
],
"fields": [
@@ -707,7 +703,6 @@
},
{
"default": "0",
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
"fieldname": "update_stock",
"fieldtype": "Check",
"hide_days": 1,
@@ -783,7 +778,8 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
"depends_on": "eval:!doc.is_return",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hide_border": 1,
@@ -797,9 +793,9 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Time Sheets",
"no_copy": 1,
"options": "Sales Invoice Timesheet",
"print_hide": 1,
"read_only": 1
"print_hide": 1
},
{
"default": "0",
@@ -1078,6 +1074,14 @@
"no_copy": 1,
"options": "Cost Center"
},
{
"collapsible": 1,
"fieldname": "section_break_49",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Additional Discount"
},
{
"default": "Grand Total",
"fieldname": "apply_discount_on",
@@ -1122,12 +1126,22 @@
"options": "currency",
"print_hide": 1
},
{
"fieldname": "totals",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Totals",
"oldfieldtype": "Section Break",
"options": "fa fa-money",
"print_hide": 1
},
{
"fieldname": "base_grand_total",
"fieldtype": "Currency",
"hide_days": 1,
"hide_seconds": 1,
"label": "Grand Total (Company Currency",
"label": "Grand Total (Company Currency)",
"oldfieldname": "grand_total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
@@ -1141,8 +1155,9 @@
"fieldtype": "Currency",
"hide_days": 1,
"hide_seconds": 1,
"label": "Rounding Adjustment",
"label": "Rounding Adjustment (Company Currency)",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1152,9 +1167,10 @@
"fieldtype": "Currency",
"hide_days": 1,
"hide_seconds": 1,
"label": "Rounded Total",
"label": "Rounded Total (Company Currency)",
"oldfieldname": "rounded_total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1164,7 +1180,7 @@
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words",
"label": "In Words (Company Currency)",
"length": 240,
"oldfieldname": "in_words",
"oldfieldtype": "Data",
@@ -1257,6 +1273,7 @@
"read_only": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "advances",
"fieldname": "advances_section",
"fieldtype": "Section Break",
@@ -1690,10 +1707,10 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "No",
"fieldname": "is_opening",
"fieldtype": "Select",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Opening Entry",
@@ -1722,6 +1739,18 @@
"oldfieldtype": "Text",
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "sales_partner",
"fieldname": "sales_team_section_break",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Commission",
"oldfieldtype": "Section Break",
"options": "fa fa-group",
"print_hide": 1
},
{
"fieldname": "sales_partner",
"fieldtype": "Link",
@@ -1765,6 +1794,16 @@
"options": "Company:company:default_currency",
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "sales_team",
"fieldname": "section_break2",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Team",
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "sales_team",
@@ -2053,7 +2092,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)",
"fieldname": "section_break_104",
"fieldtype": "Section Break"
},
@@ -2213,8 +2252,7 @@
"hidden": 1,
"label": "Item Wise Tax Details",
"no_copy": 1,
"options": "Item Wise Tax Detail",
"print_hide": 1
"options": "Item Wise Tax Detail"
},
{
"default": "0",
@@ -2255,64 +2293,6 @@
"fieldname": "override_tax_withholding_entries",
"fieldtype": "Check",
"label": "Edit Tax Withholding Entries"
},
{
"fieldname": "totals_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Totals",
"oldfieldtype": "Section Break",
"options": "fa fa-money",
"print_hide": 1
},
{
"fieldname": "base_totals_section",
"fieldtype": "Section Break",
"label": "Totals (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fieldname": "column_break_xjag",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "additional_discount_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Additional Discount"
},
{
"collapsible": 1,
"collapsible_depends_on": "sales_team",
"fieldname": "sales_team_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Team",
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "sales_partner",
"fieldname": "commission_section",
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
"label": "Commission",
"oldfieldtype": "Section Break",
"options": "fa fa-group",
"print_hide": 1
},
{
"fieldname": "section_break_vacb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdks",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
@@ -2326,7 +2306,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-02-23 14:29:00.301842",
"modified": "2025-10-09 14:48:59.472826",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -353,22 +353,10 @@ class SalesInvoice(SellingController):
self.is_opening = "No"
self.set_against_income_account()
if self.is_return and not self.return_against and self.timesheets:
frappe.throw(_("Direct return is not allowed for Timesheet."))
if not self.is_return:
self.validate_time_sheets_are_submitted()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if self.is_return and self.return_against:
for row in self.timesheets:
if row.billing_hours:
row.billing_hours = -abs(row.billing_hours)
if row.billing_amount:
row.billing_amount = -abs(row.billing_amount)
if self.is_return:
self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@@ -499,7 +487,7 @@ class SalesInvoice(SellingController):
if cint(self.is_pos) != 1 and not self.is_return:
self.update_against_document_in_jv()
self.update_time_sheet(None if (self.is_return and self.return_against) else self.name)
self.update_time_sheet(self.name)
if frappe.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
update_company_current_month_sales(self.company)
@@ -579,7 +567,7 @@ class SalesInvoice(SellingController):
self.check_if_consolidated_invoice()
super().before_cancel()
self.update_time_sheet(self.return_against if (self.is_return and self.return_against) else None)
self.update_time_sheet(None)
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
@@ -819,20 +807,8 @@ class SalesInvoice(SellingController):
for data in timesheet.time_logs:
if (
(self.project and args.timesheet_detail == data.name)
or (not self.project and not data.sales_invoice and args.timesheet_detail == data.name)
or (
not sales_invoice
and data.sales_invoice == self.name
and args.timesheet_detail == data.name
)
or (
self.is_return
and self.return_against
and data.sales_invoice
and data.sales_invoice == self.return_against
and not sales_invoice
and args.timesheet_detail == data.name
)
or (not self.project and not data.sales_invoice)
or (not sales_invoice and data.sales_invoice == self.name)
):
data.sales_invoice = sales_invoice
@@ -872,26 +848,11 @@ class SalesInvoice(SellingController):
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self):
# Note: This validation is skipped for return invoices
# to allow returns to reference already-billed timesheet details
for data in self.timesheets:
# Handle invoice duplication
if data.time_sheet and data.timesheet_detail:
if sales_invoice := frappe.db.get_value(
"Timesheet Detail", data.timesheet_detail, "sales_invoice"
):
frappe.throw(
_("Row {0}: Sales Invoice {1} is already created for {2}").format(
data.idx, frappe.bold(sales_invoice), frappe.bold(data.time_sheet)
)
)
if data.time_sheet:
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
if status not in ["Submitted", "Payslip", "Partially Billed"]:
frappe.throw(
_("Timesheet {0} cannot be invoiced in its current state").format(data.time_sheet)
)
if status not in ["Submitted", "Payslip"]:
frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet))
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
@@ -1325,12 +1286,7 @@ class SalesInvoice(SellingController):
timesheet.billing_amount = ts_doc.total_billable_amount
def update_timesheet_billing_for_project(self):
if (
not self.is_return
and not self.timesheets
and self.project
and self.is_auto_fetch_timesheet_enabled()
):
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
self.add_timesheet_data()
else:
self.calculate_billing_amount_for_timesheet()
@@ -2470,10 +2426,7 @@ def make_delivery_note(source_name, target_doc=None):
"cost_center": "cost_center",
},
"postprocess": update_item,
"condition": lambda doc: doc.delivered_by_supplier != 1
and not doc.scio_detail
and not doc.dn_detail
and doc.qty - doc.delivered_qty > 0,
"condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {

View File

@@ -2951,60 +2951,6 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
def test_item_tax_template_change_with_grand_total_discount(self):
"""
Test that when item tax template changes due to discount on Grand Total,
the tax calculations are consistent.
"""
item = create_item("Test Item With Multiple Tax Templates")
item.set("taxes", [])
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500,
},
)
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000,
},
)
item.save()
si = create_sales_invoice(item=item.name, rate=700, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Excise Duty - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"rate": 0,
},
)
si.insert()
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
si.apply_discount_on = "Grand Total"
si.discount_amount = 300
si.save()
# Verify template changed to 10%
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(si.taxes[0].tax_amount, 70) # 10% of 700
self.assertEqual(si.grand_total, 470) # 700 + 70 - 300
si.submit()
@IntegrationTestCase.change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self):
discount_account = create_account(
@@ -4745,66 +4691,6 @@ class TestSalesInvoice(ERPNextTestSuite):
doc.db_set("do_not_use_batchwise_valuation", original_value)
@change_settings("Selling Settings", {"set_zero_rate_for_expired_batch": True})
def test_zero_valuation_for_standalone_credit_note_with_expired_batch(self):
item_code = "_Test Item for Expiry Batch Zero Valuation"
make_item_for_si(
item_code,
{
"is_stock_item": 1,
"has_batch_no": 1,
"has_expiry_date": 1,
"shelf_life_in_days": 2,
"create_new_batch": 1,
"batch_number_series": "TBATCH-EBZV.####",
},
)
se = make_stock_entry(
item_code=item_code,
qty=10,
target="_Test Warehouse - _TC",
rate=100,
)
# fetch batch no from bundle
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
si = create_sales_invoice(
posting_date=add_days(nowdate(), 3),
item=item_code,
qty=-10,
rate=100,
is_return=1,
update_stock=1,
use_serial_batch_fields=1,
do_not_save=1,
do_not_submit=1,
)
si.items[0].batch_no = batch_no
si.save()
si.submit()
si.reload()
# check zero incoming rate in voucher
self.assertEqual(si.items[0].incoming_rate, 0.0)
# chekc zero incoming rate in stock ledger
stock_ledger_entry = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Sales Invoice",
"voucher_no": si.name,
"item_code": item_code,
"warehouse": "_Test Warehouse - _TC",
},
["incoming_rate", "valuation_rate"],
as_dict=True,
)
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item

View File

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

View File

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

View File

@@ -52,6 +52,7 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Timesheet Detail",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -116,16 +117,15 @@
],
"istable": 1,
"links": [],
"modified": "2025-12-23 13:54:17.677187",
"modified": "2024-03-27 13:10:36.562795",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

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

View File

@@ -1,41 +0,0 @@
{
"allow_roles": [
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
],
"creation": "2026-02-22 18:26:42.015787",
"docstatus": 0,
"doctype": "Module Onboarding",
"idx": 4,
"is_complete": 0,
"modified": "2026-02-23 22:51:34.267812",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Onboarding",
"owner": "Administrator",
"steps": [
{
"step": "Chart of Accounts"
},
{
"step": "Setup Sales taxes"
},
{
"step": "Create Sales Invoice"
},
{
"step": "Create Payment Entry"
},
{
"step": "View Balance Sheet"
},
{
"step": "Review Accounts Settings"
}
],
"title": "Accounting Onboarding"
}

View File

@@ -1,43 +1,3 @@
<h4>{{ _("New Fiscal Year - {0}").format(doc.name) }}</h4>
<h3>{{ _("Fiscal Year") }}</h3>
<p>{{ _("A new fiscal year has been automatically created.") }}</p>
<p>{{ _("Fiscal Year Details") }}</p>
<table style="margin-bottom: 1rem; width: 70%">
<tr>
<td style="font-weight:bold; width: 40%">{{ _("Year Name") }}</td>
<td>{{ doc.name }}</td>
</tr>
<tr>
<td style="font-weight:bold; width: 40%">{{ _("Start Date") }}</td>
<td>{{ frappe.format_value(doc.year_start_date) }}</td>
</tr>
<tr>
<td style="font-weight:bold; width: 40%">{{ _("End Date") }}</td>
<td>{{ frappe.format_value(doc.year_end_date) }}</td>
</tr>
{% if doc.companies|length > 0 %}
<tr>
<td style="vertical-align: top; font-weight: bold; width: 40%" rowspan="{{ doc.companies|length }}">
{% if doc.companies|length < 2 %}
{{ _("Company") }}
{% else %}
{{ _("Companies") }}
{% endif %}
</td>
<td>{{ doc.companies[0].company }}</td>
</tr>
{% for idx in range(1, doc.companies|length) %}
<tr>
<td>{{ doc.companies[idx].company }}</td>
</tr>
{% endfor %}
{% endif %}
</table>
{% if doc.disabled %}
<p>{{ _("The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.") }}</p>
{% endif %}
<p>{{ _("Please review the {0} configuration and complete any required financial setup activities.").format(frappe.utils.get_link_to_form("Fiscal Year", doc.name, frappe.bold("Fiscal Year"))) }}</p>
<p>{{ _("New fiscal year created :- ") }} {{ doc.name }}</p>

View File

@@ -1,8 +1,7 @@
{
"attach_print": 0,
"channel": "Email",
"condition": "doc.auto_created == 1",
"condition_type": "Python",
"condition": "doc.auto_created",
"creation": "2018-04-25 14:19:05.440361",
"days_in_advance": 0,
"docstatus": 0,
@@ -12,10 +11,8 @@
"event": "New",
"idx": 0,
"is_standard": 1,
"message": "<h4>{{ _(\"New Fiscal Year - {0}\").format(doc.name) }}</h4>\n\n<p>{{ _(\"A new fiscal year has been automatically created.\") }}</p>\n\n<p>{{ _(\"Fiscal Year Details\") }}</p>\n\n<table style=\"margin-bottom: 1rem; width: 70%\">\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Year Name\") }}</td>\n <td>{{ doc.name }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Start Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_start_date) }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"End Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_end_date) }}</td>\n </tr>\n {% if doc.companies|length > 0 %}\n <tr>\n <td style=\"vertical-align: top; font-weight: bold; width: 40%\" rowspan=\"{{ doc.companies|length }}\">\n {% if doc.companies|length < 2 %}\n {{ _(\"Company\") }}\n {% else %}\n {{ _(\"Companies\") }}\n {% endif %}\n </td>\n <td>{{ doc.companies[0].company }}</td>\n </tr>\n {% for idx in range(1, doc.companies|length) %}\n <tr>\n <td>{{ doc.companies[idx].company }}</td>\n </tr>\n {% endfor %}\n {% endif %}\n</table>\n\n{% if doc.disabled %}\n<p>{{ _(\"The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.\") }}</p>\n{% endif %}\n\n<p>{{ _(\"Please review the {0} configuration and complete any required financial setup activities.\").format(frappe.utils.get_link_to_form(\"Fiscal Year\", doc.name, frappe.bold(\"Fiscal Year\"))) }}</p>",
"message_type": "HTML",
"minutes_offset": 0,
"modified": "2026-02-23 17:37:03.755394",
"modified": "2023-11-17 08:54:51.532104",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Notification for new fiscal year",
@@ -30,5 +27,5 @@
],
"send_system_notification": 0,
"send_to_all_assignees": 0,
"subject": "New Fiscal Year {{ doc.name }} - Review Required"
"subject": "Notification for new fiscal year {{ doc.name }}"
}

View File

@@ -1,20 +0,0 @@
{
"action": "Go to Page",
"action_label": "Configure Chart of Accounts",
"creation": "2026-02-22 18:28:15.401383",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:45.540780",
"modified_by": "Administrator",
"name": "Chart of Accounts",
"owner": "Administrator",
"path": "Tree/Account",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Chart of Accounts",
"validate_action": 1
}

View File

@@ -1,21 +0,0 @@
{
"action": "Create Entry",
"action_label": "Create Payment Entry",
"creation": "2026-02-23 19:22:12.005360",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 20:19:56.482245",
"modified_by": "Administrator",
"name": "Create Payment Entry",
"owner": "Administrator",
"reference_document": "Payment Entry",
"route_options": "{\n \"payment_type\": \"Receive\",\n \"party_type\": \"Customer\"\n}",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Payment Entry",
"validate_action": 1
}

View File

@@ -1,20 +0,0 @@
{
"action": "Create Entry",
"action_label": "Create Sales Invoice",
"creation": "2026-02-20 13:42:38.439574",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 2,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:16:40.931428",
"modified_by": "Administrator",
"name": "Create Sales Invoice",
"owner": "Administrator",
"reference_document": "Sales Invoice",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Sales Invoice",
"validate_action": 1
}

View File

@@ -1,21 +0,0 @@
{
"action": "Update Settings",
"action_label": "Review Accounts Settings",
"creation": "2026-02-23 19:27:06.055104",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2026-02-23 22:16:40.855407",
"modified_by": "Administrator",
"name": "Review Accounts Settings",
"owner": "Administrator",
"path": "desk/accounts-settings",
"reference_document": "Accounts Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Accounts Settings",
"validate_action": 0
}

View File

@@ -1,22 +0,0 @@
{
"action": "Go to Page",
"action_label": "Setup Sales Taxes",
"creation": "2026-02-22 18:30:18.750391",
"docstatus": 0,
"doctype": "Onboarding Step",
"form_tour": "",
"idx": 1,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:42.373227",
"modified_by": "Administrator",
"name": "Setup Sales taxes",
"owner": "Administrator",
"path": "/desk/sales-taxes-and-charges-template",
"reference_document": "Sales Taxes and Charges Template",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Setup Sales taxes",
"validate_action": 1
}

View File

@@ -1,23 +0,0 @@
{
"action": "View Report",
"action_label": "View Balance Sheet",
"creation": "2026-02-23 19:22:57.651194",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2026-02-23 22:44:39.178107",
"modified_by": "Administrator",
"name": "View Balance Sheet",
"owner": "Administrator",
"reference_report": "Balance Sheet",
"report_description": "View Balance Sheet",
"report_reference_doctype": "GL Entry",
"report_type": "Script Report",
"show_form_tour": 0,
"show_full_form": 0,
"title": "View Balance Sheet",
"validate_action": 1
}

View File

@@ -7,16 +7,18 @@ from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Abs, Date, Sum
from frappe.query_builder.functions import Abs, Count, Date, Sum
from frappe.utils import (
add_days,
add_months,
add_years,
cint,
cstr,
date_diff,
flt,
formatdate,
get_last_day,
get_timestamp,
getdate,
nowdate,
)
@@ -296,9 +298,19 @@ def complete_contact_details(party_details):
contact_details = frappe._dict()
if party_details.party_type == "Employee":
from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
contact_details = frappe.db.get_value(
"Employee",
party_details.party,
[
"employee_name as contact_display",
"prefered_email as contact_email",
"cell_number as contact_mobile",
"designation as contact_designation",
"department as contact_department",
],
as_dict=True,
)
contact_details = get_employee_contact(party_details.party)
contact_details.update({"contact_person": None, "contact_phone": None})
elif party_details.contact_person:
contact_details = frappe.db.get_value(

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -877,15 +877,11 @@ class ReceivablePayableReport:
else:
entry_date = row.posting_date
row.range0 = 0.0
self.get_ageing_data(entry_date, row)
# ageing buckets should not have amounts if due date is not reached
if getdate(entry_date) > getdate(self.age_as_on):
row.range0 = row.outstanding
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
row.total_due = 0
return
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
@@ -1285,8 +1281,6 @@ class ReceivablePayableReport:
ranges = [*self.ranges, _("Above")]
prev_range_value = 0
self.add_column(label=_("<0"), fieldname="range0", fieldtype="Currency")
self.ageing_column_labels.append(_("<0"))
for idx, curr_range_value in enumerate(ranges):
label = f"{prev_range_value}-{curr_range_value}"
self.add_column(label=label, fieldname="range" + str(idx + 1))
@@ -1302,9 +1296,7 @@ class ReceivablePayableReport:
for row in self.data:
row = frappe._dict(row)
if not cint(row.bold):
values = [flt(row.get("range0", 0), precision)] + [
flt(row.get(f"range{i}", 0), precision) for i in self.range_numbers
]
values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
rows.append({"values": values})
self.chart = {

View File

@@ -102,7 +102,7 @@ def execute(filters=None):
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
)
chart = get_chart_data(filters, period_list, asset, liability, equity, currency)
chart = get_chart_data(filters, columns, asset, liability, equity, currency)
report_summary, primitive_summary = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
@@ -231,19 +231,18 @@ def get_report_summary(
], (net_asset - net_liability + net_equity)
def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
labels = [col.get("label") for col in chart_columns]
def get_chart_data(filters, columns, asset, liability, equity, currency):
labels = [d.get("label") for d in columns[2:]]
asset_data, liability_data, equity_data = [], [], []
for col in chart_columns:
key = col.get("key") or col.get("fieldname")
for p in columns[2:]:
if asset:
asset_data.append(asset[-2].get(key))
asset_data.append(asset[-2].get(p.get("fieldname")))
if liability:
liability_data.append(liability[-2].get(key))
liability_data.append(liability[-2].get(p.get("fieldname")))
if equity:
equity_data.append(equity[-2].get(key))
equity_data.append(equity[-2].get(p.get("fieldname")))
datasets = []
if asset_data:

View File

@@ -18,8 +18,6 @@ def execute(filters=None):
dimensions = filters.get("budget_against_filter")
else:
dimensions = get_budget_dimensions(filters)
if not dimensions:
return columns, [], None, None
budget_records = get_budget_records(filters, dimensions)
budget_map = build_budget_map(budget_records, filters)

View File

@@ -145,7 +145,7 @@ def execute(filters=None):
True,
)
chart = get_chart_data(period_list, data, company_currency)
chart = get_chart_data(columns, data, company_currency)
report_summary = get_report_summary(summary_data, company_currency)
@@ -417,12 +417,12 @@ def get_report_summary(summary_data, currency):
return report_summary
def get_chart_data(period_list, data, currency):
labels = [period.get("label") for period in period_list]
def get_chart_data(columns, data, currency):
labels = [d.get("label") for d in columns[2:]]
datasets = [
{
"name": section.get("section").replace("'", ""),
"values": [section.get(period.get("key")) for period in period_list],
"values": [section.get(d.get("fieldname")) for d in columns[2:]],
}
for section in data
if section.get("parent_section") is None and section.get("currency")

View File

@@ -48,25 +48,22 @@ def execute(filters=None):
return columns, data, message, chart
fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year"))
company_list, companies = get_companies(filters)
company_columns = get_company_columns(company_list, filters)
columns = get_columns(company_columns)
companies_column, companies = get_companies(filters)
columns = get_columns(companies_column, filters)
if filters.get("report") == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data(
fiscal_year, companies, company_columns, filters
fiscal_year, companies, columns, filters
)
elif filters.get("report") == "Profit and Loss Statement":
data, message, chart, report_summary = get_profit_loss_data(
fiscal_year, companies, company_columns, filters
)
data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters)
else:
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
return columns, data, message, chart, report_summary
def get_balance_sheet_data(fiscal_year, companies, company_columns, filters):
def get_balance_sheet_data(fiscal_year, companies, columns, filters):
asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters)
liability = get_data(companies, "Liability", "Credit", fiscal_year, filters=filters)
@@ -119,7 +116,7 @@ def get_balance_sheet_data(fiscal_year, companies, company_columns, filters):
True,
)
chart = get_chart_data(filters, company_columns, asset, liability, equity, company_currency)
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency)
return data, message, chart, report_summary
@@ -167,7 +164,7 @@ def get_root_account_name(root_type, company):
return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, company_columns, filters):
def get_profit_loss_data(fiscal_year, companies, columns, filters):
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(filters)
@@ -177,7 +174,7 @@ def get_profit_loss_data(fiscal_year, companies, company_columns, filters):
if net_profit_loss:
data.append(net_profit_loss)
chart = get_pl_chart_data(filters, company_columns, income, expense, net_profit_loss, company_currency)
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency)
report_summary, primitive_summary = get_pl_summary(
companies, "", income, expense, net_profit_loss, company_currency, filters, True
@@ -283,30 +280,7 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
return data
def get_company_columns(companies, filters):
company_columns = []
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
company_columns.append(
{
"fieldname": company,
"label": f"{company} ({currency})",
"fieldtype": "Currency",
"options": "currency",
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company,
}
)
return company_columns
def get_columns(company_columns):
def get_columns(companies, filters):
columns = [
{
"fieldname": "account",
@@ -324,7 +298,23 @@ def get_columns(company_columns):
},
]
columns.extend(company_columns)
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
columns.append(
{
"fieldname": company,
"label": f"{company} ({currency})",
"fieldtype": "Currency",
"options": "currency",
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company,
}
)
return columns

View File

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

View File

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

View File

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

View File

@@ -219,18 +219,13 @@ def get_net_profit(
has_value = False
gross_income_roots = [row for row in (gross_income or []) if not flt(row.get("indent"))]
non_gross_income_roots = [row for row in (non_gross_income or []) if not flt(row.get("indent"))]
gross_expense_roots = [row for row in (gross_expense or []) if not flt(row.get("indent"))]
non_gross_expense_roots = [row for row in (non_gross_expense or []) if not flt(row.get("indent"))]
for period in period_list:
key = period if consolidated else period.key
gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0
non_gross_income_for_period = flt(non_gross_income[0].get(key, 0)) if non_gross_income else 0
gross_income_for_period = sum(flt(row.get(key, 0)) for row in gross_income_roots)
non_gross_income_for_period = sum(flt(row.get(key, 0)) for row in non_gross_income_roots)
gross_expense_for_period = sum(flt(row.get(key, 0)) for row in gross_expense_roots)
non_gross_expense_for_period = sum(flt(row.get(key, 0)) for row in non_gross_expense_roots)
gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0
non_gross_expense_for_period = flt(non_gross_expense[0].get(key, 0)) if non_gross_expense else 0
total_income = gross_income_for_period + non_gross_income_for_period
total_expense = gross_expense_for_period + non_gross_expense_for_period

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,7 @@ def execute(filters=None):
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
chart = get_chart_data(filters, period_list, income, expense, net_profit_loss, currency)
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency)
report_summary, primitive_summary = get_report_summary(
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
@@ -162,19 +162,18 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
return net_profit_loss
def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, currency):
labels = [col.get("label") for col in chart_columns]
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
labels = [d.get("label") for d in columns[2:]]
income_data, expense_data, net_profit = [], [], []
for col in chart_columns:
key = col.get("key") or col.get("fieldname")
for p in columns[2:]:
if income:
income_data.append(income[-2].get(key))
income_data.append(income[-2].get(p.get("fieldname")))
if expense:
expense_data.append(expense[-2].get(key))
expense_data.append(expense[-2].get(p.get("fieldname")))
if net_profit_loss:
net_profit.append(net_profit_loss.get(key))
net_profit.append(net_profit_loss.get(p.get("fieldname")))
datasets = []
if income_data:

View File

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

View File

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

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